feat: add campaign templates and recurring schedules
This commit is contained in:
@@ -63,6 +63,7 @@ internal sealed record WebBatchSessionRow(
|
||||
long TelegramChatId,
|
||||
int? ThreadId,
|
||||
string NotificationMode);
|
||||
internal sealed record WebTemplateGroupDto(long TelegramChatId);
|
||||
|
||||
public sealed class SessionService(
|
||||
NpgsqlDataSource dataSource,
|
||||
@@ -753,6 +754,213 @@ public sealed class SessionService(
|
||||
sourceSessions[0].NotificationMode);
|
||||
}
|
||||
|
||||
public async Task<List<WebCampaignTemplate>> GetCampaignTemplatesAsync(Guid groupId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return (await conn.QueryAsync<WebCampaignTemplate>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
group_id AS GroupId,
|
||||
name AS Name,
|
||||
title AS Title,
|
||||
join_link AS JoinLink,
|
||||
session_count AS SessionCount,
|
||||
interval_days AS IntervalDays,
|
||||
max_players AS MaxPlayers,
|
||||
notification_mode AS NotificationMode,
|
||||
created_at AS CreatedAt,
|
||||
updated_at AS UpdatedAt
|
||||
FROM campaign_templates
|
||||
WHERE group_id = @GroupId
|
||||
ORDER BY created_at DESC, name
|
||||
""",
|
||||
new { GroupId = groupId })).ToList();
|
||||
}
|
||||
|
||||
public async Task<WebCampaignTemplate?> GetCampaignTemplateAsync(Guid templateId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return await conn.QuerySingleOrDefaultAsync<WebCampaignTemplate>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
group_id AS GroupId,
|
||||
name AS Name,
|
||||
title AS Title,
|
||||
join_link AS JoinLink,
|
||||
session_count AS SessionCount,
|
||||
interval_days AS IntervalDays,
|
||||
max_players AS MaxPlayers,
|
||||
notification_mode AS NotificationMode,
|
||||
created_at AS CreatedAt,
|
||||
updated_at AS UpdatedAt
|
||||
FROM campaign_templates
|
||||
WHERE id = @TemplateId
|
||||
""",
|
||||
new { TemplateId = templateId });
|
||||
}
|
||||
|
||||
public async Task<WebCampaignTemplate> CreateCampaignTemplateAsync(Guid groupId, CreateCampaignTemplateRequest request)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
return await conn.QuerySingleAsync<WebCampaignTemplate>(
|
||||
"""
|
||||
INSERT INTO campaign_templates (
|
||||
group_id,
|
||||
name,
|
||||
title,
|
||||
join_link,
|
||||
session_count,
|
||||
interval_days,
|
||||
max_players,
|
||||
notification_mode
|
||||
)
|
||||
VALUES (
|
||||
@GroupId,
|
||||
@Name,
|
||||
@Title,
|
||||
@JoinLink,
|
||||
@SessionCount,
|
||||
@IntervalDays,
|
||||
@MaxPlayers,
|
||||
@NotificationMode
|
||||
)
|
||||
ON CONFLICT (group_id, name) DO UPDATE
|
||||
SET title = EXCLUDED.title,
|
||||
join_link = EXCLUDED.join_link,
|
||||
session_count = EXCLUDED.session_count,
|
||||
interval_days = EXCLUDED.interval_days,
|
||||
max_players = EXCLUDED.max_players,
|
||||
notification_mode = EXCLUDED.notification_mode,
|
||||
updated_at = now()
|
||||
RETURNING id AS Id,
|
||||
group_id AS GroupId,
|
||||
name AS Name,
|
||||
title AS Title,
|
||||
join_link AS JoinLink,
|
||||
session_count AS SessionCount,
|
||||
interval_days AS IntervalDays,
|
||||
max_players AS MaxPlayers,
|
||||
notification_mode AS NotificationMode,
|
||||
created_at AS CreatedAt,
|
||||
updated_at AS UpdatedAt
|
||||
""",
|
||||
new
|
||||
{
|
||||
GroupId = groupId,
|
||||
request.Name,
|
||||
request.Title,
|
||||
request.JoinLink,
|
||||
request.SessionCount,
|
||||
request.IntervalDays,
|
||||
request.MaxPlayers,
|
||||
NotificationMode = request.NotificationMode.ToDatabaseValue()
|
||||
});
|
||||
}
|
||||
|
||||
public async Task DeleteCampaignTemplateAsync(Guid templateId, Guid groupId)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await conn.ExecuteAsync(
|
||||
"DELETE FROM campaign_templates WHERE id = @TemplateId AND group_id = @GroupId",
|
||||
new { TemplateId = templateId, GroupId = groupId });
|
||||
}
|
||||
|
||||
public async Task<WebSessionBatch> CreateBatchFromTemplateAsync(Guid templateId, Guid groupId, DateTime firstScheduledAt)
|
||||
{
|
||||
await using var conn = await dataSource.OpenConnectionAsync();
|
||||
await using var transaction = await conn.BeginTransactionAsync();
|
||||
|
||||
var template = await conn.QuerySingleOrDefaultAsync<WebCampaignTemplate>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
group_id AS GroupId,
|
||||
name AS Name,
|
||||
title AS Title,
|
||||
join_link AS JoinLink,
|
||||
session_count AS SessionCount,
|
||||
interval_days AS IntervalDays,
|
||||
max_players AS MaxPlayers,
|
||||
notification_mode AS NotificationMode,
|
||||
created_at AS CreatedAt,
|
||||
updated_at AS UpdatedAt
|
||||
FROM campaign_templates
|
||||
WHERE id = @TemplateId
|
||||
AND group_id = @GroupId
|
||||
FOR UPDATE
|
||||
""",
|
||||
new { TemplateId = templateId, GroupId = groupId },
|
||||
transaction);
|
||||
|
||||
if (template is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(templateId, 0);
|
||||
}
|
||||
|
||||
var group = await conn.QuerySingleOrDefaultAsync<WebTemplateGroupDto>(
|
||||
"SELECT telegram_chat_id AS TelegramChatId FROM game_groups WHERE id = @GroupId",
|
||||
new { GroupId = groupId },
|
||||
transaction);
|
||||
|
||||
if (group is null)
|
||||
{
|
||||
throw new SessionAccessDeniedException(groupId, 0);
|
||||
}
|
||||
|
||||
var schedule = BatchSchedulePlanner.BuildRecurringSchedule(
|
||||
firstScheduledAt,
|
||||
template.SessionCount,
|
||||
template.IntervalDays);
|
||||
var batchId = Guid.NewGuid();
|
||||
var renderedSessions = new List<SessionBatchDto>();
|
||||
|
||||
foreach (var scheduledAt in schedule)
|
||||
{
|
||||
var sessionId = await conn.ExecuteScalarAsync<Guid>(
|
||||
"""
|
||||
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)
|
||||
RETURNING id
|
||||
""",
|
||||
new
|
||||
{
|
||||
BatchId = batchId,
|
||||
GroupId = groupId,
|
||||
template.Title,
|
||||
template.JoinLink,
|
||||
ScheduledAt = scheduledAt,
|
||||
Status = SessionStatus.Planned,
|
||||
template.MaxPlayers,
|
||||
template.NotificationMode
|
||||
},
|
||||
transaction);
|
||||
|
||||
renderedSessions.Add(new SessionBatchDto(sessionId, scheduledAt, SessionStatus.Planned, template.MaxPlayers));
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
var renderResult = SessionBatchRenderer.Render(template.Title, renderedSessions, Array.Empty<ParticipantBatchDto>());
|
||||
var batchMessage = await bot.SendMessage(
|
||||
chatId: group.TelegramChatId,
|
||||
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 = batchId });
|
||||
|
||||
return new WebSessionBatch(
|
||||
batchId,
|
||||
groupId,
|
||||
template.Title,
|
||||
template.JoinLink,
|
||||
renderedSessions.Min(session => session.ScheduledAt),
|
||||
renderedSessions.Max(session => session.ScheduledAt),
|
||||
renderedSessions.Count,
|
||||
template.NotificationMode);
|
||||
}
|
||||
|
||||
private async Task<List<WebDirectNotificationRecipient>> LoadSessionDirectRecipientsAsync(
|
||||
Npgsql.NpgsqlConnection conn,
|
||||
Guid sessionId)
|
||||
|
||||
Reference in New Issue
Block a user