8bcd16fbc9
PR Checks / test-and-build (pull_request) Successful in 12m35s
Introduce platform-neutral PlatformKind, PlatformUser, PlatformGroup, and IPlatformMessenger contracts in GmRelay.Shared. Route Telegram session schedule updates, direct notifications, interaction replies, and calendar export through TelegramPlatformMessenger while preserving existing Telegram behavior. Bump version -> 2.0.1
114 lines
4.4 KiB
C#
114 lines
4.4 KiB
C#
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<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 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<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 actions = subscriptionUrl is not null
|
|
? new[]
|
|
{
|
|
new PlatformMessageAction(
|
|
"calendar-subscription",
|
|
"🔗 Подписаться на календарь",
|
|
subscriptionUrl)
|
|
}
|
|
: Array.Empty<PlatformMessageAction>();
|
|
|
|
await messenger.SendCalendarFileAsync(
|
|
new PlatformCalendarFile(
|
|
TelegramPlatformIds.Group(message.Chat.Id, message.MessageThreadId),
|
|
"schedule.ics",
|
|
bytes,
|
|
"📅 <b>Ваш календарь игр!</b>\nОткройте файл на устройстве, чтобы добавить события в свой календарь.",
|
|
actions),
|
|
cancellationToken);
|
|
}
|
|
}
|