feat(#21): support selected telegram topics for schedules
PR Checks / test-and-build (pull_request) Failing after 3m18s
PR Checks / test-and-build (pull_request) Failing after 3m18s
Route new schedules to an existing forum topic when /newsession is sent inside one, create bot-owned topics only from the forum root, and keep group notifications/dashboard updates threaded to the stored topic. Persist topic ownership so deletion only removes empty bot-created topics, add topic routing tests and smoke coverage, and bump release metadata to 1.14.0.
This commit is contained in:
@@ -21,7 +21,8 @@ internal sealed record SessionContext(
|
||||
DateTime ScheduledAt,
|
||||
string Status,
|
||||
long GmTelegramId,
|
||||
long TelegramChatId);
|
||||
long TelegramChatId,
|
||||
int? ThreadId);
|
||||
|
||||
internal sealed record ParticipantRsvp(
|
||||
long TelegramId,
|
||||
@@ -95,7 +96,8 @@ public sealed class HandleRsvpHandler(
|
||||
s.scheduled_at AS ScheduledAt,
|
||||
s.status AS Status,
|
||||
g.gm_telegram_id AS GmTelegramId,
|
||||
g.telegram_chat_id AS TelegramChatId
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
s.thread_id AS ThreadId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
WHERE s.id = @SessionId
|
||||
@@ -191,6 +193,7 @@ public sealed class HandleRsvpHandler(
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: session.TelegramChatId,
|
||||
messageThreadId: session.ThreadId,
|
||||
text: $"🎉 Игра «{session.Title}» подтверждена! Все участники на месте.",
|
||||
cancellationToken: ct);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ internal sealed record SessionInfo(
|
||||
DateTime ScheduledAt,
|
||||
Guid GroupId,
|
||||
long TelegramChatId,
|
||||
int? ThreadId,
|
||||
string NotificationMode);
|
||||
|
||||
internal sealed record ParticipantInfo(
|
||||
@@ -43,6 +44,7 @@ public sealed class SendConfirmationHandler(
|
||||
"""
|
||||
SELECT s.id, s.title, s.scheduled_at AS ScheduledAt, s.group_id AS GroupId,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
s.thread_id AS ThreadId,
|
||||
s.notification_mode AS NotificationMode
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
@@ -99,6 +101,7 @@ public sealed class SendConfirmationHandler(
|
||||
// 4. Send to group
|
||||
var message = await bot.SendMessage(
|
||||
chatId: session.TelegramChatId,
|
||||
messageThreadId: session.ThreadId,
|
||||
text: text,
|
||||
replyMarkup: keyboard,
|
||||
cancellationToken: ct);
|
||||
|
||||
@@ -14,6 +14,7 @@ internal sealed record JoinLinkSession(
|
||||
string JoinLink,
|
||||
DateTime ScheduledAt,
|
||||
long TelegramChatId,
|
||||
int? ThreadId,
|
||||
string NotificationMode);
|
||||
|
||||
internal sealed record ConfirmedPlayer(
|
||||
@@ -42,6 +43,7 @@ public sealed class SendJoinLinkHandler(
|
||||
"""
|
||||
SELECT s.id, s.title, s.join_link AS JoinLink, s.scheduled_at AS ScheduledAt,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
s.thread_id AS ThreadId,
|
||||
s.notification_mode AS NotificationMode
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
@@ -94,6 +96,7 @@ public sealed class SendJoinLinkHandler(
|
||||
// 4. Send
|
||||
var message = await bot.SendMessage(
|
||||
chatId: session.TelegramChatId,
|
||||
messageThreadId: session.ThreadId,
|
||||
text: text,
|
||||
cancellationToken: ct);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed record CancelSessionCommand(
|
||||
long TelegramUserId,
|
||||
string CallbackQueryId,
|
||||
long ChatId,
|
||||
int? MessageThreadId,
|
||||
int MessageId);
|
||||
|
||||
// DTOs for AOT compilation
|
||||
@@ -29,7 +30,7 @@ public sealed class CancelSessionHandler(
|
||||
{
|
||||
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
||||
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||||
|
||||
|
||||
// 1. Проверяем, что запрос делает управляющий данной группы.
|
||||
var session = await connection.QuerySingleOrDefaultAsync<CancelSessionInfoDto>(
|
||||
"""
|
||||
@@ -117,9 +118,14 @@ public sealed class CancelSessionHandler(
|
||||
ct);
|
||||
|
||||
await bot.AnswerCallbackQuery(command.CallbackQueryId, "Сессия отменена!", cancellationToken: ct);
|
||||
|
||||
|
||||
// Опционально: написать отдельное сообщение в чат
|
||||
await bot.SendMessage(command.ChatId, $"❌ <b>Внимание!</b> Сессия \"{System.Net.WebUtility.HtmlEncode(session.Title)}\" отменена.", parseMode: Telegram.Bot.Types.Enums.ParseMode.Html, cancellationToken: ct);
|
||||
await bot.SendMessage(
|
||||
chatId: command.ChatId,
|
||||
messageThreadId: command.MessageThreadId,
|
||||
text: $"❌ <b>Внимание!</b> Сессия \"{System.Net.WebUtility.HtmlEncode(session.Title)}\" отменена.",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
|
||||
var mode = SessionNotificationModeExtensions.FromDatabaseValue(session.NotificationMode);
|
||||
if (mode.ShouldSendDirectMessages())
|
||||
|
||||
@@ -144,14 +144,31 @@ public sealed class CreateSessionHandler(
|
||||
transaction);
|
||||
}
|
||||
|
||||
int? messageThreadId = null;
|
||||
if (message.Chat.IsForum)
|
||||
var topicDestination = TelegramTopicRouting.ResolveNewScheduleDestination(
|
||||
message.Chat.IsForum,
|
||||
message.MessageThreadId);
|
||||
var messageThreadId = topicDestination.MessageThreadId;
|
||||
var topicCreatedByBot = topicDestination.TopicCreatedByBot;
|
||||
if (topicDestination.ShouldCreateForumTopic)
|
||||
{
|
||||
var topic = await botClient.CreateForumTopic(
|
||||
chatId: chatId,
|
||||
name: $"🎲 Игры: {title}",
|
||||
cancellationToken: cancellationToken);
|
||||
messageThreadId = topic.MessageThreadId;
|
||||
try
|
||||
{
|
||||
var topic = await botClient.CreateForumTopic(
|
||||
chatId: chatId,
|
||||
name: $"🎲 Игры: {title}",
|
||||
cancellationToken: cancellationToken);
|
||||
messageThreadId = topic.MessageThreadId;
|
||||
}
|
||||
catch (Telegram.Bot.Exceptions.ApiRequestException ex)
|
||||
when (TelegramTopicRouting.IsMissingForumTopicRightsError(ex.Message))
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
await botClient.SendMessage(
|
||||
chatId,
|
||||
TelegramTopicRouting.MissingForumTopicRightsMessage,
|
||||
cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var batchId = Guid.NewGuid();
|
||||
@@ -161,8 +178,8 @@ public sealed class CreateSessionHandler(
|
||||
{
|
||||
var sessionId = await connection.ExecuteScalarAsync<Guid>(
|
||||
"""
|
||||
INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id, max_players)
|
||||
VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, @Status, @ThreadId, @MaxPlayers)
|
||||
INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id, topic_created_by_bot, max_players)
|
||||
VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, @Status, @ThreadId, @TopicCreatedByBot, @MaxPlayers)
|
||||
RETURNING id;
|
||||
""",
|
||||
new
|
||||
@@ -173,6 +190,7 @@ public sealed class CreateSessionHandler(
|
||||
Link = link,
|
||||
ScheduledAt = scheduledAt,
|
||||
ThreadId = messageThreadId,
|
||||
TopicCreatedByBot = topicCreatedByBot,
|
||||
MaxPlayers = parseResult.MaxPlayers,
|
||||
Status = SessionStatus.Planned
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
using GmRelay.Bot.Infrastructure.Telegram;
|
||||
using GmRelay.Shared.Domain;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.ListSessions;
|
||||
@@ -12,7 +13,13 @@ public sealed record DeleteSessionCommand(
|
||||
long ChatId,
|
||||
int MessageId);
|
||||
|
||||
internal sealed record DeleteSessionInfoDto(string Title, Guid BatchId, bool CanManage, int? ThreadId);
|
||||
internal sealed record DeleteSessionInfoDto(
|
||||
string Title,
|
||||
Guid BatchId,
|
||||
Guid GroupId,
|
||||
bool CanManage,
|
||||
int? ThreadId,
|
||||
bool TopicCreatedByBot);
|
||||
|
||||
public sealed class DeleteSessionHandler(
|
||||
NpgsqlDataSource dataSource,
|
||||
@@ -29,7 +36,9 @@ public sealed class DeleteSessionHandler(
|
||||
"""
|
||||
SELECT s.title AS Title,
|
||||
s.batch_id AS BatchId,
|
||||
s.group_id AS GroupId,
|
||||
s.thread_id AS ThreadId,
|
||||
s.topic_created_by_bot AS TopicCreatedByBot,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM group_managers gm
|
||||
@@ -57,15 +66,23 @@ public sealed class DeleteSessionHandler(
|
||||
// 2. Delete session
|
||||
await connection.ExecuteAsync("DELETE FROM sessions WHERE id = @Id", new { Id = command.SessionId }, transaction);
|
||||
|
||||
// 3. Check if any sessions are left in the batch
|
||||
var remainingInBatch = await connection.ExecuteScalarAsync<int>(
|
||||
"SELECT COUNT(*) FROM sessions WHERE batch_id = @BatchId",
|
||||
new { BatchId = session.BatchId }, transaction);
|
||||
var remainingInTopic = session.ThreadId.HasValue
|
||||
? await connection.ExecuteScalarAsync<int>(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM sessions
|
||||
WHERE group_id = @GroupId
|
||||
AND thread_id = @ThreadId
|
||||
""",
|
||||
new { session.GroupId, ThreadId = session.ThreadId.Value },
|
||||
transaction)
|
||||
: 0;
|
||||
|
||||
await transaction.CommitAsync(ct);
|
||||
|
||||
// 4. If no sessions left and we have a forum topic, delete the topic
|
||||
if (remainingInBatch == 0 && session.ThreadId.HasValue)
|
||||
// 4. If no sessions are left in a bot-owned forum topic, delete the topic.
|
||||
if (session.ThreadId.HasValue &&
|
||||
TelegramTopicRouting.ShouldDeleteForumTopic(session.TopicCreatedByBot, remainingInTopic))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -113,7 +130,7 @@ public sealed class DeleteSessionHandler(
|
||||
|
||||
if (sessionsList.Count == 0)
|
||||
{
|
||||
try { await bot.EditMessageText(command.ChatId, command.MessageId, "📭 В этой группе нет предстоящих игр.", cancellationToken: ct); } catch {}
|
||||
try { await bot.EditMessageText(command.ChatId, command.MessageId, "📭 В этой группе нет предстоящих игр.", cancellationToken: ct); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -14,7 +14,7 @@ namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||
|
||||
internal sealed record AwaitingProposalDto(
|
||||
Guid Id, Guid SessionId, string Title, DateTime CurrentScheduledAt,
|
||||
Guid BatchId, int? BatchMessageId, long TelegramChatId, string NotificationMode);
|
||||
Guid BatchId, int? BatchMessageId, long TelegramChatId, int? ThreadId, string NotificationMode);
|
||||
|
||||
internal sealed record VoteParticipantDto(
|
||||
Guid PlayerId,
|
||||
@@ -57,6 +57,7 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
SELECT rp.id AS Id, rp.session_id AS SessionId, s.title AS Title, s.scheduled_at AS CurrentScheduledAt,
|
||||
s.batch_id AS BatchId, s.batch_message_id AS BatchMessageId,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
s.thread_id AS ThreadId,
|
||||
s.notification_mode AS NotificationMode
|
||||
FROM reschedule_proposals rp
|
||||
JOIN sessions s ON s.id = rp.session_id
|
||||
@@ -84,6 +85,7 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: chatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: $"⚠️ {parseError}\n\nИспользуйте формат:\n<code>25.04.2026 19:30\n26.04.2026 18:00\nДедлайн: 25.04.2026 12:00</code>",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
@@ -161,6 +163,7 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
|
||||
var voteMsg = await bot.SendMessage(
|
||||
chatId: chatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: voteText,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
replyMarkup: keyboard,
|
||||
@@ -242,6 +245,7 @@ public sealed class HandleRescheduleTimeInputHandler(
|
||||
|
||||
await bot.SendMessage(
|
||||
chatId: chatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: $"✅ Сессия «{proposal.Title}» перенесена!\n\n📅 Новое время: <b>{newTime.ToOffset(TimeSpan.FromHours(3)).ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU"))}</b> (МСК)\n\n<i>Участников нет — голосование не требуется.</i>",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed record InitiateRescheduleCommand(
|
||||
long TelegramUserId,
|
||||
string CallbackQueryId,
|
||||
long ChatId,
|
||||
int? MessageThreadId,
|
||||
int MessageId);
|
||||
|
||||
// ── DTOs ─────────────────────────────────────────────────────────────
|
||||
@@ -96,6 +97,7 @@ public sealed class InitiateRescheduleHandler(
|
||||
|
||||
await bot.SendMessage(
|
||||
chatId: command.ChatId,
|
||||
messageThreadId: command.MessageThreadId,
|
||||
text: $"""
|
||||
⏰ Укажите 2-3 варианта времени для сессии «{session.Title}» и дедлайн голосования.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ internal sealed record DueRescheduleProposalDto(
|
||||
int? BatchMessageId,
|
||||
int? VoteMessageId,
|
||||
long TelegramChatId,
|
||||
int? ThreadId,
|
||||
string NotificationMode);
|
||||
|
||||
public sealed class RescheduleVotingDeadlineService(
|
||||
@@ -93,6 +94,7 @@ public sealed class RescheduleVotingDeadlineService(
|
||||
s.batch_id AS BatchId,
|
||||
s.batch_message_id AS BatchMessageId,
|
||||
s.notification_mode AS NotificationMode,
|
||||
s.thread_id AS ThreadId,
|
||||
g.telegram_chat_id AS TelegramChatId
|
||||
FROM reschedule_proposals rp
|
||||
JOIN sessions s ON s.id = rp.session_id
|
||||
@@ -324,6 +326,7 @@ public sealed class RescheduleVotingDeadlineService(
|
||||
{
|
||||
await bot.SendMessage(
|
||||
chatId: proposal.TelegramChatId,
|
||||
messageThreadId: proposal.ThreadId,
|
||||
text: $"📣 Расписание обновлено после голосования за перенос сессии «{System.Net.WebUtility.HtmlEncode(proposal.Title)}».",
|
||||
parseMode: ParseMode.Html,
|
||||
cancellationToken: ct);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace GmRelay.Bot.Infrastructure.Telegram;
|
||||
|
||||
public sealed record TelegramTopicDestination(
|
||||
int? MessageThreadId,
|
||||
bool ShouldCreateForumTopic,
|
||||
bool TopicCreatedByBot);
|
||||
|
||||
public static class TelegramTopicRouting
|
||||
{
|
||||
public const string MissingForumTopicRightsMessage =
|
||||
"Не удалось создать Telegram topic. Сделайте бота admin и включите право Manage Topics, затем повторите команду.";
|
||||
|
||||
public static TelegramTopicDestination ResolveNewScheduleDestination(
|
||||
bool chatIsForum,
|
||||
int? incomingMessageThreadId)
|
||||
{
|
||||
if (!chatIsForum)
|
||||
{
|
||||
return new TelegramTopicDestination(null, ShouldCreateForumTopic: false, TopicCreatedByBot: false);
|
||||
}
|
||||
|
||||
if (incomingMessageThreadId.HasValue)
|
||||
{
|
||||
return new TelegramTopicDestination(
|
||||
incomingMessageThreadId,
|
||||
ShouldCreateForumTopic: false,
|
||||
TopicCreatedByBot: false);
|
||||
}
|
||||
|
||||
return new TelegramTopicDestination(null, ShouldCreateForumTopic: true, TopicCreatedByBot: true);
|
||||
}
|
||||
|
||||
public static bool ShouldDeleteForumTopic(bool topicCreatedByBot, int remainingSessionsInTopic) =>
|
||||
topicCreatedByBot && remainingSessionsInTopic == 0;
|
||||
|
||||
public 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);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public sealed class UpdateRouter(
|
||||
CallbackQueryId: query.Id,
|
||||
ChatId: message.Chat.Id,
|
||||
MessageId: message.MessageId);
|
||||
|
||||
|
||||
await joinSessionHandler.HandleAsync(command, ct);
|
||||
return;
|
||||
}
|
||||
@@ -105,6 +105,7 @@ public sealed class UpdateRouter(
|
||||
TelegramUserId: query.From.Id,
|
||||
CallbackQueryId: query.Id,
|
||||
ChatId: message.Chat.Id,
|
||||
MessageThreadId: message.MessageThreadId,
|
||||
MessageId: message.MessageId);
|
||||
|
||||
await cancelSessionHandler.HandleAsync(command, ct);
|
||||
@@ -144,6 +145,7 @@ public sealed class UpdateRouter(
|
||||
TelegramUserId: query.From.Id,
|
||||
CallbackQueryId: query.Id,
|
||||
ChatId: message.Chat.Id,
|
||||
MessageThreadId: message.MessageThreadId,
|
||||
MessageId: message.MessageId);
|
||||
|
||||
await initiateRescheduleHandler.HandleAsync(command, ct);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE sessions
|
||||
ADD COLUMN topic_created_by_bot BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
UPDATE sessions
|
||||
SET topic_created_by_bot = TRUE
|
||||
WHERE thread_id IS NOT NULL;
|
||||
@@ -21,7 +21,7 @@ public static class MoscowTime
|
||||
|
||||
public static bool TryParseMoscow(string text, out DateTimeOffset utcTime)
|
||||
{
|
||||
if (DateTime.TryParseExact(text, new[] { "dd.MM.yyyy HH:mm", "dd.MM.yyyy H:mm", "d.MM.yyyy HH:mm" },
|
||||
if (DateTime.TryParseExact(text, new[] { "dd.MM.yyyy HH:mm", "dd.MM.yyyy H:mm", "d.MM.yyyy HH:mm" },
|
||||
System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var localDt))
|
||||
{
|
||||
utcTime = new DateTimeOffset(localDt, MoscowOffset).ToUniversalTime();
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="nav-version">v1.13.0</div>
|
||||
<div class="nav-version">v1.14.0</div>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
@@ -79,4 +79,4 @@
|
||||
|
||||
private void ToggleMenu() => isOpen = !isOpen;
|
||||
private void CloseMenu() => isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ public sealed record WebSession(
|
||||
int? MaxPlayers,
|
||||
int ActivePlayerCount,
|
||||
int WaitlistedPlayerCount,
|
||||
string NotificationMode = SessionNotificationModeExtensions.GroupAndDirectValue);
|
||||
string NotificationMode = SessionNotificationModeExtensions.GroupAndDirectValue,
|
||||
int? ThreadId = null);
|
||||
|
||||
public sealed record WebParticipant(
|
||||
Guid Id,
|
||||
@@ -73,7 +74,8 @@ internal sealed record WebBatchSessionRow(
|
||||
int? BatchMessageId,
|
||||
long TelegramChatId,
|
||||
int? ThreadId,
|
||||
string NotificationMode);
|
||||
string NotificationMode,
|
||||
bool TopicCreatedByBot = false);
|
||||
internal sealed record WebTemplateGroupDto(long TelegramChatId);
|
||||
|
||||
public sealed class SessionService(
|
||||
@@ -314,7 +316,8 @@ public sealed class SessionService(
|
||||
s.max_players AS MaxPlayers,
|
||||
COALESCE(active_counts.count, 0)::int AS ActivePlayerCount,
|
||||
COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount,
|
||||
s.notification_mode AS NotificationMode
|
||||
s.notification_mode AS NotificationMode,
|
||||
s.thread_id AS ThreadId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
LEFT JOIN LATERAL (
|
||||
@@ -351,7 +354,8 @@ public sealed class SessionService(
|
||||
s.max_players AS MaxPlayers,
|
||||
COALESCE(active_counts.count, 0)::int AS ActivePlayerCount,
|
||||
COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount,
|
||||
s.notification_mode AS NotificationMode
|
||||
s.notification_mode AS NotificationMode,
|
||||
s.thread_id AS ThreadId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
LEFT JOIN LATERAL (
|
||||
@@ -409,7 +413,8 @@ public sealed class SessionService(
|
||||
s.max_players AS MaxPlayers,
|
||||
0 AS ActivePlayerCount,
|
||||
0 AS WaitlistedPlayerCount,
|
||||
s.notification_mode AS NotificationMode
|
||||
s.notification_mode AS NotificationMode,
|
||||
s.thread_id AS ThreadId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
WHERE s.id = @Id AND s.group_id = @GroupId",
|
||||
@@ -463,7 +468,11 @@ public sealed class SessionService(
|
||||
"\n" +
|
||||
$"👥 Мест: <b>{(maxPlayers.HasValue ? maxPlayers.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : "без лимита")}</b>";
|
||||
|
||||
await bot.SendMessage(oldSession.TelegramChatId, notification, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
await bot.SendMessage(
|
||||
chatId: oldSession.TelegramChatId,
|
||||
messageThreadId: oldSession.ThreadId,
|
||||
text: notification,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
|
||||
var mode = SessionNotificationModeExtensions.FromDatabaseValue(oldSession.NotificationMode);
|
||||
if (mode.ShouldSendDirectMessages())
|
||||
@@ -490,7 +499,8 @@ public sealed class SessionService(
|
||||
s.max_players AS MaxPlayers,
|
||||
0 AS ActivePlayerCount,
|
||||
0 AS WaitlistedPlayerCount,
|
||||
s.notification_mode AS NotificationMode
|
||||
s.notification_mode AS NotificationMode,
|
||||
s.thread_id AS ThreadId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
WHERE s.id = @SessionId AND s.group_id = @GroupId
|
||||
@@ -567,8 +577,9 @@ public sealed class SessionService(
|
||||
await transaction.CommitAsync();
|
||||
|
||||
await bot.SendMessage(
|
||||
session.TelegramChatId,
|
||||
$"⬆️ <b>{System.Net.WebUtility.HtmlEncode(promoted.DisplayName)}</b> переведен(а) из листа ожидания в основной состав «{System.Net.WebUtility.HtmlEncode(session.Title)}».",
|
||||
chatId: session.TelegramChatId,
|
||||
messageThreadId: session.ThreadId,
|
||||
text: $"⬆️ <b>{System.Net.WebUtility.HtmlEncode(promoted.DisplayName)}</b> переведен(а) из листа ожидания в основной состав «{System.Net.WebUtility.HtmlEncode(session.Title)}».",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
|
||||
if (session.BatchMessageId.HasValue)
|
||||
@@ -612,7 +623,8 @@ public sealed class SessionService(
|
||||
s.max_players AS MaxPlayers,
|
||||
0 AS ActivePlayerCount,
|
||||
0 AS WaitlistedPlayerCount,
|
||||
s.notification_mode AS NotificationMode
|
||||
s.notification_mode AS NotificationMode,
|
||||
s.thread_id AS ThreadId
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
WHERE s.id = @SessionId AND s.group_id = @GroupId
|
||||
@@ -697,15 +709,17 @@ public sealed class SessionService(
|
||||
await transaction.CommitAsync();
|
||||
|
||||
await bot.SendMessage(
|
||||
session.TelegramChatId,
|
||||
$"🚪 <b>{System.Net.WebUtility.HtmlEncode(participant.DisplayName)}</b> удален(а) из сессии «{System.Net.WebUtility.HtmlEncode(session.Title)}».",
|
||||
chatId: session.TelegramChatId,
|
||||
messageThreadId: session.ThreadId,
|
||||
text: $"🚪 <b>{System.Net.WebUtility.HtmlEncode(participant.DisplayName)}</b> удален(а) из сессии «{System.Net.WebUtility.HtmlEncode(session.Title)}».",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
|
||||
if (promoted is not null)
|
||||
{
|
||||
await bot.SendMessage(
|
||||
session.TelegramChatId,
|
||||
$"⬆️ <b>{System.Net.WebUtility.HtmlEncode(promoted.DisplayName)}</b> переведен(а) из листа ожидания в основной состав «{System.Net.WebUtility.HtmlEncode(session.Title)}».",
|
||||
chatId: session.TelegramChatId,
|
||||
messageThreadId: session.ThreadId,
|
||||
text: $"⬆️ <b>{System.Net.WebUtility.HtmlEncode(promoted.DisplayName)}</b> переведен(а) из листа ожидания в основной состав «{System.Net.WebUtility.HtmlEncode(session.Title)}».",
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
|
||||
if (session.BatchMessageId.HasValue)
|
||||
@@ -813,6 +827,7 @@ public sealed class SessionService(
|
||||
s.batch_message_id AS BatchMessageId,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
s.thread_id AS ThreadId,
|
||||
s.topic_created_by_bot AS TopicCreatedByBot,
|
||||
s.notification_mode AS NotificationMode
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
@@ -865,7 +880,11 @@ public sealed class SessionService(
|
||||
$"🗓 Новое начало: <b>{firstScheduledAt.FormatMoscow()}</b> (МСК)\n" +
|
||||
$"↔️ Шаг: <b>{intervalDays} дн.</b>";
|
||||
|
||||
await bot.SendMessage(firstSession.TelegramChatId, notification, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
await bot.SendMessage(
|
||||
chatId: firstSession.TelegramChatId,
|
||||
messageThreadId: firstSession.ThreadId,
|
||||
text: notification,
|
||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||||
|
||||
var mode = SessionNotificationModeExtensions.FromDatabaseValue(firstSession.NotificationMode);
|
||||
if (mode.ShouldSendDirectMessages())
|
||||
@@ -892,6 +911,7 @@ public sealed class SessionService(
|
||||
s.batch_message_id AS BatchMessageId,
|
||||
g.telegram_chat_id AS TelegramChatId,
|
||||
s.thread_id AS ThreadId,
|
||||
s.topic_created_by_bot AS TopicCreatedByBot,
|
||||
s.notification_mode AS NotificationMode
|
||||
FROM sessions s
|
||||
JOIN game_groups g ON g.id = s.group_id
|
||||
@@ -920,8 +940,8 @@ public sealed class SessionService(
|
||||
var scheduledAt = BatchSchedulePlanner.ShiftForClone(sourceSession.ScheduledAt, interval);
|
||||
var sessionId = await conn.ExecuteScalarAsync<Guid>(
|
||||
"""
|
||||
INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id, max_players, notification_mode)
|
||||
VALUES (@BatchId, @GroupId, @Title, @JoinLink, @ScheduledAt, @Status, @ThreadId, @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
|
||||
@@ -933,6 +953,7 @@ public sealed class SessionService(
|
||||
ScheduledAt = scheduledAt,
|
||||
Status = SessionStatus.Planned,
|
||||
ThreadId = threadId,
|
||||
sourceSession.TopicCreatedByBot,
|
||||
sourceSession.MaxPlayers,
|
||||
sourceSession.NotificationMode
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user