feat: add web batch bulk operations
This commit is contained in:
@@ -22,6 +22,26 @@ public sealed record WebSession(
|
||||
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,
|
||||
@@ -115,6 +135,25 @@ public sealed class SessionService(
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -282,6 +321,206 @@ public sealed class SessionService(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -318,4 +557,29 @@ public sealed class SessionService(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user