113 lines
4.5 KiB
C#
113 lines
4.5 KiB
C#
using System.Text;
|
|
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Npgsql;
|
|
using Telegram.Bot;
|
|
using Telegram.Bot.Types;
|
|
using Telegram.Bot.Types.ReplyMarkups;
|
|
|
|
namespace GmRelay.Bot.Features.Sessions.ExportCalendar;
|
|
|
|
internal sealed record CalendarSessionDto(Guid Id, string Title, DateTime ScheduledAt);
|
|
|
|
public sealed class ExportCalendarHandler(
|
|
NpgsqlDataSource dataSource,
|
|
ITelegramBotClient botClient,
|
|
IConfiguration configuration)
|
|
{
|
|
public async Task HandleAsync(Message message, CancellationToken cancellationToken)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
|
|
|
var sessions = await connection.QueryAsync<CalendarSessionDto>(
|
|
@"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 botClient.SendMessage(
|
|
chatId: message.Chat.Id,
|
|
text: "📭 У этой группы нет запланированных сессий для экспорта.",
|
|
cancellationToken: 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());
|
|
using var stream = new MemoryStream(bytes);
|
|
|
|
var inputFile = InputFile.FromStream(stream, "schedule.ics");
|
|
|
|
// 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<Guid?>(
|
|
@"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 replyMarkup = subscriptionUrl is not null
|
|
? new InlineKeyboardMarkup(new[]
|
|
{
|
|
new[] { InlineKeyboardButton.WithUrl("🔗 Подписаться на календарь", subscriptionUrl) }
|
|
})
|
|
: null;
|
|
|
|
await botClient.SendDocument(
|
|
chatId: message.Chat.Id,
|
|
document: inputFile,
|
|
caption: "📅 <b>Ваш календарь игр!</b>\nОткройте файл на устройстве, чтобы добавить события в свой календарь.",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Html,
|
|
replyMarkup: replyMarkup,
|
|
messageThreadId: message.MessageThreadId,
|
|
cancellationToken: cancellationToken);
|
|
}
|
|
}
|