Files
GmRelayBot/src/GmRelay.Web/Services/CalendarSubscriptionService.cs
T
Toutsu 040b0a3cdb
PR Checks / test-and-build (pull_request) Failing after 13m15s
refactor: завершить platform migration и удалить deprecated telegram_* scaffolding
- Добавлены миграции V024 (backfill + deprecation comments + calendar_subscriptions platform identity) и V025 (backfill proposed_by_external_user_id)
- Все Bot handlers переведены с telegram_id/chat_id на platform + external_*
- Shared handlers очищены от COALESCE fallback с telegram_* колонками
- DiscordBot очищен от COALESCE fallback
- Web SessionService и CalendarSubscriptionService переведены на external_*
- HandleRsvpHandler: убран legacy UNION с gm_telegram_id, теперь только group_managers
- RescheduleVotingFinalizer: переведен на external_username/external_user_id
- Tests: добавлены asserts для V024/V025
- Версия обновлена до 3.1.0

Bump version → 3.1.0

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:41:15 +03:00

95 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(
string userPlatform,
string userExternalId,
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_platform, user_external_id, group_id, filter_type, created_at, expires_at)
VALUES (gen_random_uuid(), @token, @userPlatform, @userExternalId, @groupId, @filterType, now(), NULL)",
new { token, userPlatform, userExternalId, 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, 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, Guid? GroupId, int FilterType);
private sealed record CalendarSessionDto(Guid Id, string Title, DateTime ScheduledAt);
}