From 383e2c1d8d13f88c2d9b44681d477c220b4ed300 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Wed, 27 May 2026 13:50:18 +0300 Subject: [PATCH] fix: create Telegram topics for template batches Create a Telegram forum topic when Web creates a batch from a campaign template, persist thread ownership on the generated sessions, and send the batch schedule into that topic. Bump version -> 3.1.1 --- .gitea/workflows/deploy.yml | 2 +- Directory.Build.props | 2 +- compose.yaml | 6 +-- .../Components/Layout/NavMenu.razor | 2 +- src/GmRelay.Web/Services/SessionService.cs | 41 ++++++++++++++++++- .../Discord/DiscordProjectStructureTests.cs | 14 +++---- .../TelegramTopicIntegrationSmokeTests.cs | 13 ++++++ 7 files changed, 65 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 400f1ed..e9b190e 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 3.1.0 + VERSION: 3.1.1 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index b947ac1..e8f515c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 3.1.0 + 3.1.1 net10.0 preview enable diff --git a/compose.yaml b/compose.yaml index a69ee9e..8c87b78 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,7 +49,7 @@ services: crond -f bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:3.1.0 + image: git.codeanddice.ru/toutsu/gmrelay-bot:3.1.1 restart: always depends_on: db: @@ -67,7 +67,7 @@ services: retries: 3 discord: - image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.1.0 + image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.1.1 restart: always depends_on: db: @@ -84,7 +84,7 @@ services: retries: 3 web: - image: git.codeanddice.ru/toutsu/gmrelay-web:3.1.0 + image: git.codeanddice.ru/toutsu/gmrelay-web:3.1.1 restart: always depends_on: db: diff --git a/src/GmRelay.Web/Components/Layout/NavMenu.razor b/src/GmRelay.Web/Components/Layout/NavMenu.razor index d93b928..1e0eed4 100644 --- a/src/GmRelay.Web/Components/Layout/NavMenu.razor +++ b/src/GmRelay.Web/Components/Layout/NavMenu.razor @@ -73,7 +73,7 @@ - + diff --git a/src/GmRelay.Web/Services/SessionService.cs b/src/GmRelay.Web/Services/SessionService.cs index 68eb9b8..73d3224 100644 --- a/src/GmRelay.Web/Services/SessionService.cs +++ b/src/GmRelay.Web/Services/SessionService.cs @@ -3,6 +3,7 @@ using GmRelay.Shared.Domain; using GmRelay.Shared.Rendering; using Npgsql; using Telegram.Bot; +using Telegram.Bot.Exceptions; using GmRelay.Web.Services; namespace GmRelay.Web.Services; @@ -95,6 +96,7 @@ internal sealed record WebBatchSessionRow( string NotificationMode, bool TopicCreatedByBot = false); internal sealed record WebTemplateGroupDto(long TelegramChatId); +internal sealed record WebTemplateTopicDestination(int? MessageThreadId, bool TopicCreatedByBot); public sealed class SessionService( NpgsqlDataSource dataSource, @@ -1186,6 +1188,10 @@ public sealed class SessionService( throw new SessionAccessDeniedException(groupId, "0"); } + var topicDestination = await ResolveTemplateBatchTopicAsync(group.TelegramChatId, template.Title); + var messageThreadId = topicDestination.MessageThreadId; + var topicCreatedByBot = topicDestination.TopicCreatedByBot; + var schedule = BatchSchedulePlanner.BuildRecurringSchedule( firstScheduledAt, template.SessionCount, @@ -1197,8 +1203,8 @@ public sealed class SessionService( { var sessionId = await conn.ExecuteScalarAsync( """ - INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, max_players, notification_mode) - VALUES (@BatchId, @GroupId, @Title, @JoinLink, @ScheduledAt, @Status, @MaxPlayers, @NotificationMode) + INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id, topic_created_by_bot, max_players, notification_mode) + VALUES (@BatchId, @GroupId, @Title, @JoinLink, @ScheduledAt, @Status, @ThreadId, @TopicCreatedByBot, @MaxPlayers, @NotificationMode) RETURNING id """, new @@ -1209,6 +1215,8 @@ public sealed class SessionService( template.JoinLink, ScheduledAt = scheduledAt, Status = SessionStatus.Planned, + ThreadId = messageThreadId, + TopicCreatedByBot = topicCreatedByBot, template.MaxPlayers, template.NotificationMode }, @@ -1223,6 +1231,7 @@ public sealed class SessionService( var renderResult = TelegramSessionBatchRenderer.Render(view); var batchMessage = await bot.SendMessage( chatId: group.TelegramChatId, + messageThreadId: messageThreadId, text: renderResult.Text, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html, replyMarkup: renderResult.Markup); @@ -1242,6 +1251,34 @@ public sealed class SessionService( template.NotificationMode); } + private async Task ResolveTemplateBatchTopicAsync(long telegramChatId, string title) + { + var chat = await bot.GetChat(chatId: telegramChatId); + if (!chat.IsForum) + { + return new WebTemplateTopicDestination(null, TopicCreatedByBot: false); + } + + try + { + var topic = await bot.CreateForumTopic( + chatId: telegramChatId, + name: $"🎲 Игры: {title}"); + return new WebTemplateTopicDestination(topic.MessageThreadId, TopicCreatedByBot: true); + } + catch (ApiRequestException ex) when (IsMissingForumTopicRightsError(ex.Message)) + { + throw new InvalidOperationException( + "Не удалось создать Telegram topic. Сделайте бота admin и включите право Manage Topics, затем повторите действие.", + ex); + } + } + + private static bool IsMissingForumTopicRightsError(string apiError) => + apiError.Contains("not enough rights", StringComparison.OrdinalIgnoreCase) || + apiError.Contains("CHAT_ADMIN_REQUIRED", StringComparison.OrdinalIgnoreCase) || + apiError.Contains("not an administrator", StringComparison.OrdinalIgnoreCase); + private async Task> LoadSessionDirectRecipientsAsync( Npgsql.NpgsqlConnection conn, Guid sessionId) diff --git a/tests/GmRelay.Bot.Tests/Discord/DiscordProjectStructureTests.cs b/tests/GmRelay.Bot.Tests/Discord/DiscordProjectStructureTests.cs index 07aafb5..cf48fc2 100644 --- a/tests/GmRelay.Bot.Tests/Discord/DiscordProjectStructureTests.cs +++ b/tests/GmRelay.Bot.Tests/Discord/DiscordProjectStructureTests.cs @@ -62,7 +62,7 @@ public sealed class DiscordProjectStructureTests var prChecks = File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "pr-checks.yml")); var deploy = File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml")); - Assert.Contains("gmrelay-discord-bot:3.1.0", compose); + Assert.Contains("gmrelay-discord-bot:3.1.1", compose); Assert.Contains("Discord__Token=${DISCORD_BOT_TOKEN:?Set DISCORD_BOT_TOKEN in .env}", compose); Assert.Contains("src/GmRelay.DiscordBot/Dockerfile", deploy); Assert.Contains("DISCORD_BOT_TOKEN", deploy); @@ -76,13 +76,13 @@ public sealed class DiscordProjectStructureTests { var repoRoot = GetRepoRoot(); - Assert.Contains("3.1.0", File.ReadAllText(Path.Combine(repoRoot, "Directory.Build.props"))); - Assert.Contains("VERSION: 3.1.0", File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml"))); - Assert.Contains("gmrelay-bot:3.1.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml"))); - Assert.Contains("gmrelay-web:3.1.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml"))); - Assert.Contains("gmrelay-discord-bot:3.1.0", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml"))); + Assert.Contains("3.1.1", File.ReadAllText(Path.Combine(repoRoot, "Directory.Build.props"))); + Assert.Contains("VERSION: 3.1.1", File.ReadAllText(Path.Combine(repoRoot, ".gitea", "workflows", "deploy.yml"))); + Assert.Contains("gmrelay-bot:3.1.1", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml"))); + Assert.Contains("gmrelay-web:3.1.1", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml"))); + Assert.Contains("gmrelay-discord-bot:3.1.1", File.ReadAllText(Path.Combine(repoRoot, "compose.yaml"))); Assert.Contains( - "v3.1.0", + "v3.1.1", File.ReadAllText(Path.Combine(repoRoot, "src", "GmRelay.Web", "Components", "Layout", "NavMenu.razor"))); } diff --git a/tests/GmRelay.Bot.Tests/Infrastructure/Telegram/TelegramTopicIntegrationSmokeTests.cs b/tests/GmRelay.Bot.Tests/Infrastructure/Telegram/TelegramTopicIntegrationSmokeTests.cs index cacd78d..e39296c 100644 --- a/tests/GmRelay.Bot.Tests/Infrastructure/Telegram/TelegramTopicIntegrationSmokeTests.cs +++ b/tests/GmRelay.Bot.Tests/Infrastructure/Telegram/TelegramTopicIntegrationSmokeTests.cs @@ -60,6 +60,19 @@ public sealed class TelegramTopicIntegrationSmokeTests Assert.Contains("ExternalThreadId", telegramMessenger, StringComparison.Ordinal); } + [Fact] + public async Task WebTemplateBatches_ShouldCreateAndPersistForumTopic() + { + var sessionService = await ReadRepositoryFileAsync("src/GmRelay.Web/Services/SessionService.cs"); + + Assert.Contains("GetChat", sessionService, StringComparison.Ordinal); + Assert.Contains("CreateForumTopic", sessionService, StringComparison.Ordinal); + Assert.Contains("thread_id, topic_created_by_bot", sessionService, StringComparison.Ordinal); + Assert.Contains("ThreadId = messageThreadId", sessionService, StringComparison.Ordinal); + Assert.Contains("TopicCreatedByBot = topicCreatedByBot", sessionService, StringComparison.Ordinal); + Assert.Contains("messageThreadId: messageThreadId", sessionService, StringComparison.Ordinal); + } + private static async Task ReadRepositoryFileAsync(string relativePath) { var directory = new DirectoryInfo(AppContext.BaseDirectory);