586 lines
23 KiB
C#
586 lines
23 KiB
C#
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,
|
||
int? MaxPlayers,
|
||
int ActivePlayerCount,
|
||
int WaitlistedPlayerCount);
|
||
|
||
internal sealed record WebPromotedParticipantDto(Guid ParticipantRowId, string DisplayName);
|
||
internal sealed record WebBatchInfo(
|
||
Guid BatchId,
|
||
Guid GroupId,
|
||
string Title,
|
||
string JoinLink,
|
||
long TelegramChatId,
|
||
int? BatchMessageId,
|
||
int? ThreadId);
|
||
|
||
internal sealed record WebBatchSessionRow(
|
||
Guid Id,
|
||
Guid GroupId,
|
||
string Title,
|
||
string JoinLink,
|
||
DateTime ScheduledAt,
|
||
string Status,
|
||
int? MaxPlayers,
|
||
int? BatchMessageId,
|
||
long TelegramChatId,
|
||
int? ThreadId);
|
||
|
||
public sealed class SessionService(
|
||
NpgsqlDataSource dataSource,
|
||
ITelegramBotClient bot,
|
||
ILogger<SessionService> logger) : ISessionStore
|
||
{
|
||
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<WebGameGroup?> GetGroupAsync(Guid groupId)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
return await conn.QuerySingleOrDefaultAsync<WebGameGroup>(
|
||
"SELECT id, telegram_chat_id AS TelegramChatId, name, gm_telegram_id AS GmTelegramId FROM game_groups WHERE id = @GroupId",
|
||
new { GroupId = groupId });
|
||
}
|
||
|
||
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,
|
||
s.max_players AS MaxPlayers,
|
||
COALESCE(active_counts.count, 0)::int AS ActivePlayerCount,
|
||
COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount
|
||
FROM sessions s
|
||
JOIN game_groups g ON g.id = s.group_id
|
||
LEFT JOIN LATERAL (
|
||
SELECT COUNT(*) AS count
|
||
FROM session_participants sp
|
||
WHERE sp.session_id = s.id
|
||
AND sp.is_gm = false
|
||
AND sp.registration_status = @Active
|
||
) active_counts ON true
|
||
LEFT JOIN LATERAL (
|
||
SELECT COUNT(*) AS count
|
||
FROM session_participants sp
|
||
WHERE sp.session_id = s.id
|
||
AND sp.is_gm = false
|
||
AND sp.registration_status = @Waitlisted
|
||
) waitlist_counts ON true
|
||
WHERE s.group_id = @GroupId AND s.scheduled_at > now() - interval '4 hours'
|
||
ORDER BY s.scheduled_at",
|
||
new
|
||
{
|
||
GroupId = groupId,
|
||
Active = ParticipantRegistrationStatus.Active,
|
||
Waitlisted = ParticipantRegistrationStatus.Waitlisted
|
||
})).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,
|
||
s.max_players AS MaxPlayers,
|
||
COALESCE(active_counts.count, 0)::int AS ActivePlayerCount,
|
||
COALESCE(waitlist_counts.count, 0)::int AS WaitlistedPlayerCount
|
||
FROM sessions s
|
||
JOIN game_groups g ON g.id = s.group_id
|
||
LEFT JOIN LATERAL (
|
||
SELECT COUNT(*) AS count
|
||
FROM session_participants sp
|
||
WHERE sp.session_id = s.id
|
||
AND sp.is_gm = false
|
||
AND sp.registration_status = @Active
|
||
) active_counts ON true
|
||
LEFT JOIN LATERAL (
|
||
SELECT COUNT(*) AS count
|
||
FROM session_participants sp
|
||
WHERE sp.session_id = s.id
|
||
AND sp.is_gm = false
|
||
AND sp.registration_status = @Waitlisted
|
||
) waitlist_counts ON true
|
||
WHERE s.id = @SessionId",
|
||
new
|
||
{
|
||
SessionId = sessionId,
|
||
Active = ParticipantRegistrationStatus.Active,
|
||
Waitlisted = ParticipantRegistrationStatus.Waitlisted
|
||
});
|
||
}
|
||
|
||
public async Task<WebSessionBatch?> GetBatchAsync(Guid batchId)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
return await conn.QuerySingleOrDefaultAsync<WebSessionBatch>(
|
||
"""
|
||
SELECT s.batch_id AS Id,
|
||
s.group_id AS GroupId,
|
||
(array_agg(s.title ORDER BY s.scheduled_at))[1] AS Title,
|
||
(array_agg(s.join_link ORDER BY s.scheduled_at))[1] AS JoinLink,
|
||
MIN(s.scheduled_at) AS FirstScheduledAt,
|
||
MAX(s.scheduled_at) AS LastScheduledAt,
|
||
COUNT(*)::int AS SessionCount
|
||
FROM sessions s
|
||
WHERE s.batch_id = @BatchId
|
||
GROUP BY s.batch_id, s.group_id
|
||
""",
|
||
new { BatchId = batchId });
|
||
}
|
||
|
||
public async Task UpdateSessionAsync(Guid sessionId, Guid groupId, string title, DateTime scheduledAt, string joinLink, int? maxPlayers)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
await using var transaction = await conn.BeginTransactionAsync();
|
||
|
||
var oldSession = 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,
|
||
s.max_players AS MaxPlayers,
|
||
0 AS ActivePlayerCount,
|
||
0 AS WaitlistedPlayerCount
|
||
FROM sessions s
|
||
JOIN game_groups g ON g.id = s.group_id
|
||
WHERE s.id = @Id AND s.group_id = @GroupId",
|
||
new { Id = sessionId, GroupId = groupId },
|
||
transaction);
|
||
|
||
if (oldSession is null)
|
||
{
|
||
throw new SessionAccessDeniedException(sessionId, 0);
|
||
}
|
||
|
||
var updatedRows = await conn.ExecuteAsync(
|
||
@"UPDATE sessions
|
||
SET title = @Title,
|
||
scheduled_at = @ScheduledAt,
|
||
join_link = @JoinLink,
|
||
max_players = @MaxPlayers,
|
||
updated_at = now()
|
||
WHERE id = @Id AND group_id = @GroupId",
|
||
new
|
||
{
|
||
Id = sessionId,
|
||
GroupId = groupId,
|
||
Title = title,
|
||
ScheduledAt = scheduledAt,
|
||
JoinLink = joinLink,
|
||
MaxPlayers = maxPlayers
|
||
},
|
||
transaction);
|
||
|
||
if (updatedRows == 0)
|
||
{
|
||
throw new SessionAccessDeniedException(sessionId, 0);
|
||
}
|
||
|
||
await conn.ExecuteAsync(
|
||
"UPDATE sessions SET title = @Title WHERE batch_id = @BatchId",
|
||
new { Title = title, BatchId = oldSession.BatchId },
|
||
transaction);
|
||
|
||
await transaction.CommitAsync();
|
||
|
||
var timeChanged = oldSession.ScheduledAt != scheduledAt;
|
||
var notification = $"🔄 <b>Мастер обновил игру!</b>\n\n" +
|
||
$"📌 <b>{System.Net.WebUtility.HtmlEncode(title)}</b>\n" +
|
||
$"📅 Время: <b>{scheduledAt.FormatMoscow()}</b> (МСК)" + (timeChanged ? " (изменено)" : "") +
|
||
"\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);
|
||
|
||
if (oldSession.BatchMessageId.HasValue)
|
||
{
|
||
await TryUpdateBatchMessageAsync(oldSession.BatchId, oldSession.TelegramChatId, oldSession.BatchMessageId.Value, title);
|
||
}
|
||
}
|
||
|
||
public async Task PromoteWaitlistedPlayerAsync(Guid sessionId, Guid groupId)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
await using var transaction = await conn.BeginTransactionAsync();
|
||
|
||
var session = 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,
|
||
s.max_players AS MaxPlayers,
|
||
0 AS ActivePlayerCount,
|
||
0 AS WaitlistedPlayerCount
|
||
FROM sessions s
|
||
JOIN game_groups g ON g.id = s.group_id
|
||
WHERE s.id = @SessionId AND s.group_id = @GroupId
|
||
FOR UPDATE",
|
||
new { SessionId = sessionId, GroupId = groupId },
|
||
transaction);
|
||
|
||
if (session is null)
|
||
{
|
||
throw new SessionAccessDeniedException(sessionId, 0);
|
||
}
|
||
|
||
var activeParticipants = await conn.ExecuteScalarAsync<int>(
|
||
"""
|
||
SELECT COUNT(*)
|
||
FROM session_participants
|
||
WHERE session_id = @SessionId
|
||
AND is_gm = false
|
||
AND registration_status = @Active
|
||
""",
|
||
new { SessionId = sessionId, Active = ParticipantRegistrationStatus.Active },
|
||
transaction);
|
||
|
||
var waitlistedParticipants = await conn.ExecuteScalarAsync<int>(
|
||
"""
|
||
SELECT COUNT(*)
|
||
FROM session_participants
|
||
WHERE session_id = @SessionId
|
||
AND is_gm = false
|
||
AND registration_status = @Waitlisted
|
||
""",
|
||
new { SessionId = sessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
|
||
transaction);
|
||
|
||
if (!SessionCapacityRules.CanPromoteWaitlistedPlayer(session.MaxPlayers, activeParticipants, waitlistedParticipants))
|
||
{
|
||
throw new InvalidOperationException(waitlistedParticipants == 0
|
||
? "Лист ожидания пуст."
|
||
: "Нет свободных мест для повышения игрока.");
|
||
}
|
||
|
||
var promoted = await conn.QuerySingleAsync<WebPromotedParticipantDto>(
|
||
"""
|
||
SELECT sp.id AS ParticipantRowId,
|
||
p.display_name AS DisplayName
|
||
FROM session_participants sp
|
||
JOIN players p ON p.id = sp.player_id
|
||
WHERE sp.session_id = @SessionId
|
||
AND sp.is_gm = false
|
||
AND sp.registration_status = @Waitlisted
|
||
ORDER BY sp.created_at ASC, sp.id ASC
|
||
LIMIT 1
|
||
FOR UPDATE OF sp
|
||
""",
|
||
new { SessionId = sessionId, Waitlisted = ParticipantRegistrationStatus.Waitlisted },
|
||
transaction);
|
||
|
||
await conn.ExecuteAsync(
|
||
"""
|
||
UPDATE session_participants
|
||
SET registration_status = @Active,
|
||
rsvp_status = @Pending,
|
||
responded_at = NULL
|
||
WHERE id = @ParticipantRowId
|
||
""",
|
||
new
|
||
{
|
||
promoted.ParticipantRowId,
|
||
Active = ParticipantRegistrationStatus.Active,
|
||
Pending = RsvpStatus.Pending
|
||
},
|
||
transaction);
|
||
|
||
await transaction.CommitAsync();
|
||
|
||
await bot.SendMessage(
|
||
session.TelegramChatId,
|
||
$"⬆️ <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)
|
||
{
|
||
await TryUpdateBatchMessageAsync(session.BatchId, session.TelegramChatId, session.BatchMessageId.Value, session.Title);
|
||
}
|
||
}
|
||
|
||
public async Task UpdateBatchDetailsAsync(Guid batchId, Guid groupId, string title, string joinLink)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
await using var transaction = await conn.BeginTransactionAsync();
|
||
|
||
var batch = await GetBatchInfoAsync(conn, batchId, groupId, transaction);
|
||
if (batch is null)
|
||
{
|
||
throw new SessionAccessDeniedException(batchId, 0);
|
||
}
|
||
|
||
var updatedRows = await conn.ExecuteAsync(
|
||
"""
|
||
UPDATE sessions
|
||
SET title = @Title,
|
||
join_link = @JoinLink,
|
||
updated_at = now()
|
||
WHERE batch_id = @BatchId
|
||
AND group_id = @GroupId
|
||
""",
|
||
new
|
||
{
|
||
BatchId = batchId,
|
||
GroupId = groupId,
|
||
Title = title,
|
||
JoinLink = joinLink
|
||
},
|
||
transaction);
|
||
|
||
if (updatedRows == 0)
|
||
{
|
||
throw new SessionAccessDeniedException(batchId, 0);
|
||
}
|
||
|
||
await transaction.CommitAsync();
|
||
|
||
if (batch.BatchMessageId.HasValue)
|
||
{
|
||
await TryUpdateBatchMessageAsync(batchId, batch.TelegramChatId, batch.BatchMessageId.Value, title);
|
||
}
|
||
}
|
||
|
||
public async Task RescheduleBatchAsync(Guid batchId, Guid groupId, DateTime firstScheduledAt, int intervalDays)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
await using var transaction = await conn.BeginTransactionAsync();
|
||
|
||
var batchSessions = (await conn.QueryAsync<WebBatchSessionRow>(
|
||
"""
|
||
SELECT s.id AS Id,
|
||
s.group_id AS GroupId,
|
||
s.title AS Title,
|
||
s.join_link AS JoinLink,
|
||
s.scheduled_at AS ScheduledAt,
|
||
s.status AS Status,
|
||
s.max_players AS MaxPlayers,
|
||
s.batch_message_id AS BatchMessageId,
|
||
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.batch_id = @BatchId
|
||
AND s.group_id = @GroupId
|
||
ORDER BY s.scheduled_at
|
||
FOR UPDATE
|
||
""",
|
||
new { BatchId = batchId, GroupId = groupId },
|
||
transaction)).ToList();
|
||
|
||
if (batchSessions.Count == 0)
|
||
{
|
||
throw new SessionAccessDeniedException(batchId, 0);
|
||
}
|
||
|
||
var newSchedule = BatchSchedulePlanner.BuildFixedIntervalSchedule(
|
||
batchSessions.Select(session => session.ScheduledAt),
|
||
firstScheduledAt,
|
||
intervalDays);
|
||
|
||
for (var index = 0; index < batchSessions.Count; index++)
|
||
{
|
||
await conn.ExecuteAsync(
|
||
"""
|
||
UPDATE sessions
|
||
SET scheduled_at = @ScheduledAt,
|
||
updated_at = now()
|
||
WHERE id = @SessionId
|
||
""",
|
||
new
|
||
{
|
||
SessionId = batchSessions[index].Id,
|
||
ScheduledAt = newSchedule[index]
|
||
},
|
||
transaction);
|
||
}
|
||
|
||
await transaction.CommitAsync();
|
||
|
||
var firstSession = batchSessions[0];
|
||
if (firstSession.BatchMessageId.HasValue)
|
||
{
|
||
await TryUpdateBatchMessageAsync(batchId, firstSession.TelegramChatId, firstSession.BatchMessageId.Value, firstSession.Title);
|
||
}
|
||
|
||
var notification = $"🔄 <b>Мастер обновил расписание пачки</b>\n\n" +
|
||
$"📌 <b>{System.Net.WebUtility.HtmlEncode(firstSession.Title)}</b>\n" +
|
||
$"🗓 Новое начало: <b>{firstScheduledAt.FormatMoscow()}</b> (МСК)\n" +
|
||
$"↔️ Шаг: <b>{intervalDays} дн.</b>";
|
||
|
||
await bot.SendMessage(firstSession.TelegramChatId, notification, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
|
||
}
|
||
|
||
public async Task<WebSessionBatch> CloneBatchAsync(Guid batchId, Guid groupId, BatchCloneInterval interval)
|
||
{
|
||
await using var conn = await dataSource.OpenConnectionAsync();
|
||
await using var transaction = await conn.BeginTransactionAsync();
|
||
|
||
var sourceSessions = (await conn.QueryAsync<WebBatchSessionRow>(
|
||
"""
|
||
SELECT s.id AS Id,
|
||
s.group_id AS GroupId,
|
||
s.title AS Title,
|
||
s.join_link AS JoinLink,
|
||
s.scheduled_at AS ScheduledAt,
|
||
s.status AS Status,
|
||
s.max_players AS MaxPlayers,
|
||
s.batch_message_id AS BatchMessageId,
|
||
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.batch_id = @BatchId
|
||
AND s.group_id = @GroupId
|
||
ORDER BY s.scheduled_at
|
||
FOR UPDATE
|
||
""",
|
||
new { BatchId = batchId, GroupId = groupId },
|
||
transaction)).ToList();
|
||
|
||
if (sourceSessions.Count == 0)
|
||
{
|
||
throw new SessionAccessDeniedException(batchId, 0);
|
||
}
|
||
|
||
var newBatchId = Guid.NewGuid();
|
||
var batchTitle = sourceSessions[0].Title;
|
||
var batchJoinLink = sourceSessions[0].JoinLink;
|
||
var chatId = sourceSessions[0].TelegramChatId;
|
||
var threadId = sourceSessions[0].ThreadId;
|
||
var renderedSessions = new List<SessionBatchDto>();
|
||
|
||
foreach (var sourceSession in sourceSessions)
|
||
{
|
||
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)
|
||
VALUES (@BatchId, @GroupId, @Title, @JoinLink, @ScheduledAt, @Status, @ThreadId, @MaxPlayers)
|
||
RETURNING id
|
||
""",
|
||
new
|
||
{
|
||
BatchId = newBatchId,
|
||
sourceSession.GroupId,
|
||
Title = batchTitle,
|
||
JoinLink = batchJoinLink,
|
||
ScheduledAt = scheduledAt,
|
||
Status = SessionStatus.Planned,
|
||
ThreadId = threadId,
|
||
sourceSession.MaxPlayers
|
||
},
|
||
transaction);
|
||
|
||
renderedSessions.Add(new SessionBatchDto(sessionId, scheduledAt, SessionStatus.Planned, sourceSession.MaxPlayers));
|
||
}
|
||
|
||
await transaction.CommitAsync();
|
||
|
||
var renderResult = SessionBatchRenderer.Render(batchTitle, renderedSessions, Array.Empty<ParticipantBatchDto>());
|
||
var batchMessage = await bot.SendMessage(
|
||
chatId: chatId,
|
||
messageThreadId: threadId,
|
||
text: renderResult.Text,
|
||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
||
replyMarkup: renderResult.Markup);
|
||
|
||
await conn.ExecuteAsync(
|
||
"UPDATE sessions SET batch_message_id = @MessageId WHERE batch_id = @BatchId",
|
||
new { MessageId = batchMessage.MessageId, BatchId = newBatchId });
|
||
|
||
return new WebSessionBatch(
|
||
newBatchId,
|
||
groupId,
|
||
batchTitle,
|
||
batchJoinLink,
|
||
renderedSessions.Min(session => session.ScheduledAt),
|
||
renderedSessions.Max(session => session.ScheduledAt),
|
||
renderedSessions.Count);
|
||
}
|
||
|
||
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, max_players AS MaxPlayers 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,
|
||
sp.registration_status AS RegistrationStatus
|
||
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.registration_status ASC, sp.created_at ASC, 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 ex)
|
||
{
|
||
logger.LogWarning(ex, "Failed to update batch message {MessageId} in chat {ChatId}", messageId, chatId);
|
||
}
|
||
}
|
||
|
||
private static async Task<WebBatchInfo?> GetBatchInfoAsync(
|
||
Npgsql.NpgsqlConnection conn,
|
||
Guid batchId,
|
||
Guid groupId,
|
||
Npgsql.NpgsqlTransaction transaction)
|
||
{
|
||
return await conn.QuerySingleOrDefaultAsync<WebBatchInfo>(
|
||
"""
|
||
SELECT s.batch_id AS BatchId,
|
||
s.group_id AS GroupId,
|
||
(array_agg(s.title ORDER BY s.scheduled_at))[1] AS Title,
|
||
(array_agg(s.join_link ORDER BY s.scheduled_at))[1] AS JoinLink,
|
||
g.telegram_chat_id AS TelegramChatId,
|
||
(array_agg(s.batch_message_id ORDER BY s.scheduled_at))[1] AS BatchMessageId,
|
||
(array_agg(s.thread_id ORDER BY s.scheduled_at))[1] AS ThreadId
|
||
FROM sessions s
|
||
JOIN game_groups g ON g.id = s.group_id
|
||
WHERE s.batch_id = @BatchId
|
||
AND s.group_id = @GroupId
|
||
GROUP BY s.batch_id, s.group_id, g.telegram_chat_id
|
||
""",
|
||
new { BatchId = batchId, GroupId = groupId },
|
||
transaction);
|
||
}
|
||
}
|