040b0a3cdb
PR Checks / test-and-build (pull_request) Failing after 13m15s
- Добавлены миграции 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>
95 lines
3.8 KiB
C#
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);
|
|
}
|