94 lines
3.8 KiB
C#
94 lines
3.8 KiB
C#
using System.Text;
|
|
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Web.Services;
|
|
|
|
public sealed class CalendarSubscriptionService(NpgsqlDataSource dataSource)
|
|
{
|
|
private const string IcsProdId = "-//GM-Relay//TTRPG Schedule//EN";
|
|
|
|
public string GenerateToken() => Guid.NewGuid().ToString("N");
|
|
|
|
public async Task<string> CreateSubscriptionAsync(
|
|
long userTelegramId,
|
|
Guid? groupId,
|
|
CalendarSubscriptionFilter filter,
|
|
CancellationToken ct = default)
|
|
{
|
|
var token = GenerateToken();
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await connection.ExecuteAsync(
|
|
@"INSERT INTO calendar_subscriptions (id, token, user_telegram_id, group_id, filter_type, created_at, expires_at)
|
|
VALUES (gen_random_uuid(), @token, @userTelegramId, @groupId, @filterType, now(), NULL)",
|
|
new { token, userTelegramId, groupId, filterType = (int)filter });
|
|
return token;
|
|
}
|
|
|
|
public async Task<string> GetIcsAsync(string token, CancellationToken ct = default)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
|
|
var subscription = await connection.QueryFirstOrDefaultAsync<SubscriptionRecord>(
|
|
@"SELECT id, user_telegram_id as UserTelegramId, group_id as GroupId, filter_type as FilterType
|
|
FROM calendar_subscriptions
|
|
WHERE token = @token
|
|
AND (expires_at IS NULL OR expires_at > now())",
|
|
new { token });
|
|
|
|
if (subscription is null)
|
|
throw new SubscriptionNotFoundException();
|
|
|
|
var sessions = await connection.QueryAsync<CalendarSessionDto>(
|
|
subscription.FilterType == (int)CalendarSubscriptionFilter.SpecificGroup && subscription.GroupId.HasValue
|
|
? @"SELECT s.id as Id, s.title as Title, s.scheduled_at as ScheduledAt
|
|
FROM sessions s
|
|
WHERE s.group_id = @GroupId
|
|
AND s.status = @Planned
|
|
AND s.scheduled_at > NOW()
|
|
ORDER BY s.scheduled_at ASC"
|
|
: @"SELECT s.id as Id, s.title as Title, s.scheduled_at as ScheduledAt
|
|
FROM sessions s
|
|
WHERE s.status = @Planned
|
|
AND s.scheduled_at > NOW()
|
|
ORDER BY s.scheduled_at ASC",
|
|
new { subscription.GroupId, Planned = SessionStatus.Planned });
|
|
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("BEGIN:VCALENDAR");
|
|
sb.AppendLine("VERSION:2.0");
|
|
sb.AppendLine($"PRODID:{IcsProdId}");
|
|
sb.AppendLine("CALSCALE:GREGORIAN");
|
|
sb.AppendLine("METHOD:PUBLISH");
|
|
|
|
foreach (var s in sessions)
|
|
{
|
|
var dtStart = FormatIcsDate(s.ScheduledAt);
|
|
var dtEnd = FormatIcsDate(s.ScheduledAt.AddHours(4));
|
|
sb.AppendLine("BEGIN:VEVENT");
|
|
sb.AppendLine($"UID:{s.Id}@gmrelay");
|
|
sb.AppendLine($"DTSTAMP:{FormatIcsDate(DateTime.UtcNow)}");
|
|
sb.AppendLine($"DTSTART:{dtStart}");
|
|
sb.AppendLine($"DTEND:{dtEnd}");
|
|
sb.AppendLine($"SUMMARY:{EscapeIcsText(s.Title)}");
|
|
sb.AppendLine("END:VEVENT");
|
|
}
|
|
|
|
sb.AppendLine("END:VCALENDAR");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string FormatIcsDate(DateTime dt) => dt.ToUniversalTime().ToString("yyyyMMddTHHmmssZ");
|
|
|
|
private static string EscapeIcsText(string text) => text
|
|
.Replace("\\", "\\\\")
|
|
.Replace(";", "\\;")
|
|
.Replace(",", "\\,")
|
|
.Replace("\n", "\\n")
|
|
.Replace("\r", "");
|
|
|
|
private sealed record SubscriptionRecord(Guid Id, long UserTelegramId, Guid? GroupId, int FilterType);
|
|
private sealed record CalendarSessionDto(Guid Id, string Title, DateTime ScheduledAt);
|
|
}
|