using System.Text; using Dapper; using GmRelay.Bot.Infrastructure.Telegram; using GmRelay.Shared.Domain; using GmRelay.Shared.Platform; using Microsoft.Extensions.Configuration; using Npgsql; using Telegram.Bot.Types; namespace GmRelay.Bot.Features.Sessions.ExportCalendar; internal sealed record CalendarSessionDto(Guid Id, string Title, DateTime ScheduledAt); public sealed class ExportCalendarHandler( NpgsqlDataSource dataSource, IPlatformMessenger messenger, IConfiguration configuration) { public async Task HandleAsync(Message message, CancellationToken cancellationToken) { await using var connection = await dataSource.OpenConnectionAsync(cancellationToken); var sessions = await connection.QueryAsync( @"SELECT s.id as Id, s.title as Title, s.scheduled_at as ScheduledAt" + " FROM sessions s" + " JOIN game_groups g ON s.group_id = g.id" + " WHERE g.telegram_chat_id = @ChatId" + " AND s.status = @Planned" + " AND s.scheduled_at > NOW()" + " ORDER BY s.scheduled_at ASC", new { ChatId = message.Chat.Id, Planned = SessionStatus.Planned }); var sessionsList = sessions.ToList(); if (sessionsList.Count == 0) { await messenger.SendGroupMessageAsync( TelegramPlatformIds.Group(message.Chat.Id, message.MessageThreadId), "📭 У этой группы нет запланированных сессий для экспорта.", cancellationToken); return; } var sb = new StringBuilder(); sb.AppendLine("BEGIN:VCALENDAR"); sb.AppendLine("VERSION:2.0"); sb.AppendLine("PRODID:-//GM-Relay//TTRPG Schedule//EN"); foreach (var s in sessionsList) { var dtStart = s.ScheduledAt.ToString("yyyyMMddTHHmmssZ"); var dtEnd = s.ScheduledAt.AddHours(4).ToString("yyyyMMddTHHmmssZ"); sb.AppendLine("BEGIN:VEVENT"); sb.AppendLine($"UID:{s.Id}@gmrelay"); sb.AppendLine($"DTSTAMP:{DateTime.UtcNow:yyyyMMddTHHmmssZ}"); sb.AppendLine($"DTSTART:{dtStart}"); sb.AppendLine($"DTEND:{dtEnd}"); sb.AppendLine($"SUMMARY:{s.Title}"); sb.AppendLine("END:VEVENT"); } sb.AppendLine("END:VCALENDAR"); var bytes = Encoding.UTF8.GetBytes(sb.ToString()); // Create calendar subscription string? subscriptionUrl = null; var baseUrl = configuration["Web:BaseUrl"]; var senderId = message.From?.Id; if (!string.IsNullOrWhiteSpace(baseUrl) && senderId.HasValue) { try { var token = Guid.NewGuid().ToString("N"); var groupId = await connection.QueryFirstOrDefaultAsync( @"SELECT id FROM game_groups WHERE telegram_chat_id = @ChatId", new { ChatId = message.Chat.Id }); 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 = senderId.Value, groupId, filterType = (int)CalendarSubscriptionFilter.SpecificGroup }); subscriptionUrl = $"{baseUrl.TrimEnd('/')}/calendar/{token}.ics"; } catch { // Non-critical: if subscription creation fails, still send the file } } var actions = subscriptionUrl is not null ? new[] { new PlatformMessageAction( "calendar-subscription", "🔗 Подписаться на календарь", subscriptionUrl) } : Array.Empty(); await messenger.SendCalendarFileAsync( new PlatformCalendarFile( TelegramPlatformIds.Group(message.Chat.Id, message.MessageThreadId), "schedule.ics", bytes, "📅 Ваш календарь игр!\nОткройте файл на устройстве, чтобы добавить события в свой календарь.", actions), cancellationToken); } }