feat: implement Blazor web interface for GM session management
Deploy Telegram Bot / deploy (push) Has been cancelled
Deploy Telegram Bot / deploy (push) Has been cancelled
- Created GmRelay.Web project (Blazor Server) - Created GmRelay.Shared library for domain models and rendering - Refactored GmRelay.Bot to use the Shared library - Integrated Telegram Login widget with server-side HMAC verification - Added Dashboard, Group Details, and Edit Session pages - Enabled bot notifications and in-place message updates from web actions - Updated .NET Aspire orchestration and Docker Compose configuration
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
|
||||
namespace GmRelay.Web.Services;
|
||||
|
||||
public sealed record WebGameGroup(Guid Id, long TelegramChatId, string Name, long GmTelegramId);
|
||||
public sealed record WebSession(Guid Id, Guid GroupId, string Title, DateTime ScheduledAt, string Status, string JoinLink, Guid BatchId, int? BatchMessageId, long TelegramChatId);
|
||||
|
||||
public sealed class SessionService(
|
||||
NpgsqlDataSource dataSource,
|
||||
ITelegramBotClient bot)
|
||||
{
|
||||
public async Task<List<WebGameGroup>> GetGroupsForGmAsync(long gmId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return (await conn.QueryAsync<WebGameGroup>(
|
||||
"SELECT id, telegram_chat_id AS TelegramChatId, name, gm_telegram_id AS GmTelegramId FROM game_groups WHERE gm_telegram_id = @GmId",
|
||||
new { GmId = gmId })).ToList();
|
||||
}
|
||||
|
||||
public async Task<List<WebSession>> GetUpcomingSessionsAsync(Guid groupId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return (await conn.QueryAsync<WebSession>(
|
||||
@"SELECT s.id, s.group_id AS GroupId, s.title, s.scheduled_at AS ScheduledAt, s.status, s.join_link AS JoinLink,
|
||||
s.batch_id AS BatchId, s.batch_message_id AS BatchMessageId,
|
||||
g.telegram_chat_id AS TelegramChatId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
WHERE s.group_id = @GroupId AND s.scheduled_at > now() - interval '4 hours'
|
||||
ORDER BY s.scheduled_at",
|
||||
new { GroupId = groupId })).ToList();
|
||||
}
|
||||
|
||||
public async Task<WebSession?> GetSessionAsync(Guid sessionId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return await conn.QuerySingleOrDefaultAsync<WebSession>(
|
||||
@"SELECT s.id, s.group_id AS GroupId, s.title, s.scheduled_at AS ScheduledAt, s.status, s.join_link AS JoinLink,
|
||||
s.batch_id AS BatchId, s.batch_message_id AS BatchMessageId,
|
||||
g.telegram_chat_id AS TelegramChatId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
WHERE s.id = @SessionId",
|
||||
new { SessionId = sessionId });
|
||||
}
|
||||
|
||||
public async Task UpdateSessionAsync(Guid sessionId, string title, DateTime scheduledAt, string joinLink)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await using var transaction = await conn.BeginTransactionAsync();
|
||||
|
||||
// 1. Update Session
|
||||
var oldSession = await conn.QuerySingleAsync<WebSession>(
|
||||
"SELECT title, scheduled_at AS ScheduledAt, join_link AS JoinLink, batch_id AS BatchId, batch_message_id AS BatchMessageId FROM sessions WHERE id = @Id",
|
||||
new { Id = sessionId }, transaction);
|
||||
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE sessions SET title = @Title, scheduled_at = @ScheduledAt, join_link = @JoinLink, updated_at = now()
|
||||
WHERE id = @Id",
|
||||
new { Id = sessionId, Title = title, ScheduledAt = scheduledAt, JoinLink = joinLink },
|
||||
transaction);
|
||||
|
||||
// 2. Fetch context for notification
|
||||
var group = await conn.QuerySingleAsync<WebGameGroup>(
|
||||
@"SELECT telegram_chat_id AS TelegramChatId FROM game_groups g
|
||||
JOIN sessions s ON s.group_id = g.id WHERE s.id = @Id",
|
||||
new { Id = sessionId }, transaction);
|
||||
|
||||
// 3. Update all sessions in the same batch with new title (optional, usually batch shares title)
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE sessions SET title = @Title WHERE batch_id = @BatchId",
|
||||
new { Title = title, BatchId = oldSession.BatchId },
|
||||
transaction);
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// 4. Send Telegram Notification
|
||||
var timeChanged = oldSession.ScheduledAt != scheduledAt;
|
||||
var notification = $"🔄 <b>Мастер обновил игру!</b>\n\n" +
|
||||
$"📌 <b>{System.Net.WebUtility.HtmlEncode(title)}</b>\n" +
|
||||
$"📅 Время: <b>{scheduledAt.FormatMoscow()}</b> (МСК)" + (timeChanged ? " (изменено)" : "");
|
||||
|
||||
await bot.SendMessage(group.TelegramChatId, notification, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
|
||||
// 5. Update Original Batch Message
|
||||
if (oldSession.BatchMessageId.HasValue)
|
||||
{
|
||||
await TryUpdateBatchMessageAsync(oldSession.BatchId, group.TelegramChatId, oldSession.BatchMessageId.Value, title);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryUpdateBatchMessageAsync(Guid batchId, long chatId, int messageId, string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
|
||||
var sessions = (await conn.QueryAsync<SessionBatchDto>(
|
||||
"SELECT id AS SessionId, scheduled_at AS ScheduledAt, status AS Status FROM sessions WHERE batch_id = @BatchId ORDER BY scheduled_at",
|
||||
new { BatchId = batchId })).ToList();
|
||||
|
||||
var participants = (await conn.QueryAsync<ParticipantBatchDto>(
|
||||
@"SELECT sp.session_id AS SessionId, p.display_name AS DisplayName, p.telegram_username AS TelegramUsername
|
||||
FROM session_participants sp
|
||||
JOIN players p ON sp.player_id = p.id
|
||||
JOIN sessions s ON sp.session_id = s.id
|
||||
WHERE s.batch_id = @BatchId AND sp.is_gm = false
|
||||
ORDER BY sp.responded_at ASC, p.created_at ASC",
|
||||
new { BatchId = batchId })).ToList();
|
||||
|
||||
var renderResult = SessionBatchRenderer.Render(title, sessions, participants);
|
||||
|
||||
await bot.EditMessageText(
|
||||
chatId: chatId,
|
||||
messageId: messageId,
|
||||
text: renderResult.Text,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
replyMarkup: renderResult.Markup);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore if message too old or same content
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user