diff --git a/Resources/texts.resx b/Resources/texts.resx index 60c6c5a..46e774b 100644 --- a/Resources/texts.resx +++ b/Resources/texts.resx @@ -632,4 +632,13 @@ If you'd like to get access, you can contact <a href="{0}">me here</a&g The mailing list is over. Sent {0}/{1} + + Cancel + + + Session expired. Please send the link again. + + + Cancelled + \ No newline at end of file diff --git a/Resources/texts.ru-RU.resx b/Resources/texts.ru-RU.resx index b2749b0..812852b 100644 --- a/Resources/texts.ru-RU.resx +++ b/Resources/texts.ru-RU.resx @@ -676,4 +676,13 @@ PS: квадратные скобки не нужны :) Рассылка окончена. Отправлено {0}/{1} + + Отмена + + + Сессия истекла. Отправьте ссылку заново. + + + Отменено + \ No newline at end of file diff --git a/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs b/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs index 862afdc..0e602fa 100644 --- a/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs +++ b/TelegramBot/Handlers/ICallBackQuery/CallbackNames.cs @@ -66,4 +66,12 @@ public static class CallbackNames public const string UserSetAutoSendVideoTimeTo = "user_set_auto_send_video_time_to:"; public const string UserSetVideoSendUsersParameterized = "user_set_video_send_users:"; public const string UserSetSiteStopList = "user_set_site_stop_list:"; + + // Media session parameterized callbacks + public const string SendToAllContactsSession = "send_to_all_contacts:"; + public const string SendToDefaultGroupsSession = "send_to_default_groups:"; + public const string SendToSpecifiedGroupsSession = "send_to_specified_groups:"; + public const string SendToSpecifiedUsersSession = "send_to_specified_users:"; + public const string SendOnlyToMeSession = "send_only_to_me:"; + public const string CancelMediaSession = "cancel_media:"; } diff --git a/TelegramBot/Handlers/ICallBackQuery/IMediaSessionCallbackQuery.cs b/TelegramBot/Handlers/ICallBackQuery/IMediaSessionCallbackQuery.cs new file mode 100644 index 0000000..31675bc --- /dev/null +++ b/TelegramBot/Handlers/ICallBackQuery/IMediaSessionCallbackQuery.cs @@ -0,0 +1,312 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + + +using TelegramMediaRelayBot.TelegramBot.Sessions; +using TelegramMediaRelayBot.Database; +using TelegramMediaRelayBot.Database.Interfaces; + +namespace TelegramMediaRelayBot.TelegramBot.Handlers.ICallBackQuery; + + +public class SendToAllContactsSessionCommand : IBotCallbackQueryHandlers +{ + private readonly TGBot _tgBot; + private readonly IContactGetter _contactGetterRepository; + private readonly IUserGetter _userGetter; + + public SendToAllContactsSessionCommand( + TGBot tgBot, + IContactGetter contactGetterRepository, + IUserGetter userGetter) + { + _tgBot = tgBot; + _contactGetterRepository = contactGetterRepository; + _userGetter = userGetter; + } + + public string Name => "send_to_all_contacts:"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + string sessionId = update.CallbackQuery!.Data!.Split(':')[1]; + if (!MediaSessionManager.TryGet(sessionId, out var session) || session == null) + { + await botClient.AnswerCallbackQuery(update.CallbackQuery.Id, Config.GetResourceString("SessionExpiredMessage"), showAlert: true); + return; + } + + // Cancel default action timeout + try { session.Cts.Cancel(); } catch (ObjectDisposedException) { } + + long chatId = session.ChatId; + int userId = _userGetter.GetUserIDbyTelegramID(chatId); + List mutedByUserIds = _userGetter.GetUsersIdForMuteContactId(userId); + List contactUserTGIds = await _contactGetterRepository.GetAllContactUserTGIds(userId); + List targetUserIds = contactUserTGIds.Except(mutedByUserIds).ToList(); + + int messageId = int.Parse(sessionId); + var statusMessage = update.CallbackQuery!.Message!; + + MediaSessionManager.Remove(sessionId); + + await botClient.EditMessageText( + chatId, + messageId, + Config.GetResourceString("WaitDownloadingVideo"), + cancellationToken: ct + ); + + _ = _tgBot.HandleMediaRequest(botClient, session.Url, chatId, statusMessage, targetUserIds, caption: session.Caption ?? ""); + } +} + +public class SendToDefaultGroupsSessionCommand : IBotCallbackQueryHandlers +{ + private readonly TGBot _tgBot; + private readonly IUserGetter _userGetter; + private readonly IGroupGetter _groupGetter; + + public SendToDefaultGroupsSessionCommand( + TGBot tgBot, + IUserGetter userGetter, + IGroupGetter groupGetter) + { + _tgBot = tgBot; + _userGetter = userGetter; + _groupGetter = groupGetter; + } + + public string Name => "send_to_default_groups:"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + string sessionId = update.CallbackQuery!.Data!.Split(':')[1]; + if (!MediaSessionManager.TryGet(sessionId, out var session) || session == null) + { + await botClient.AnswerCallbackQuery(update.CallbackQuery.Id, Config.GetResourceString("SessionExpiredMessage"), showAlert: true); + return; + } + + try { session.Cts.Cancel(); } catch (ObjectDisposedException) { } + + long chatId = session.ChatId; + int userId = _userGetter.GetUserIDbyTelegramID(chatId); + List mutedByUserIds = _userGetter.GetUsersIdForMuteContactId(userId); + List userIds = await _groupGetter.GetAllUsersInDefaultEnabledGroups(userId); + + List targetUserIds = userIds + .Where(contactId => !mutedByUserIds.Contains(_userGetter.GetTelegramIDbyUserID(contactId))) + .Select(_userGetter.GetTelegramIDbyUserID) + .ToList(); + + int messageId = int.Parse(sessionId); + var statusMessage = update.CallbackQuery!.Message!; + + MediaSessionManager.Remove(sessionId); + + await botClient.EditMessageText( + chatId, + messageId, + Config.GetResourceString("WaitDownloadingVideo"), + cancellationToken: ct + ); + + _ = _tgBot.HandleMediaRequest(botClient, session.Url, chatId, statusMessage, targetUserIds, caption: session.Caption ?? ""); + } +} + +public class SendOnlyToMeSessionCommand : IBotCallbackQueryHandlers +{ + private readonly TGBot _tgBot; + + public SendOnlyToMeSessionCommand(TGBot tgBot) + { + _tgBot = tgBot; + } + + public string Name => "send_only_to_me:"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + string sessionId = update.CallbackQuery!.Data!.Split(':')[1]; + if (!MediaSessionManager.TryGet(sessionId, out var session) || session == null) + { + await botClient.AnswerCallbackQuery(update.CallbackQuery.Id, Config.GetResourceString("SessionExpiredMessage"), showAlert: true); + return; + } + + try { session.Cts.Cancel(); } catch (ObjectDisposedException) { } + + long chatId = session.ChatId; + int messageId = int.Parse(sessionId); + var statusMessage = update.CallbackQuery!.Message!; + + MediaSessionManager.Remove(sessionId); + + await botClient.EditMessageText( + chatId, + messageId, + Config.GetResourceString("WaitDownloadingVideo"), + cancellationToken: ct + ); + + _ = _tgBot.HandleMediaRequest(botClient, session.Url, chatId, statusMessage, caption: session.Caption ?? ""); + } +} + +public class SendToSpecifiedGroupsSessionCommand : IBotCallbackQueryHandlers +{ + private readonly TGBot _tgBot; + private readonly IUserGetter _userGetter; + private readonly IGroupGetter _groupGetter; + private readonly IDefaultActionGetter _defaultActionGetter; + + public SendToSpecifiedGroupsSessionCommand( + TGBot tgBot, + IUserGetter userGetter, + IGroupGetter groupGetter, + IDefaultActionGetter defaultActionGetter) + { + _tgBot = tgBot; + _userGetter = userGetter; + _groupGetter = groupGetter; + _defaultActionGetter = defaultActionGetter; + } + + public string Name => "send_to_specified_groups:"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + string sessionId = update.CallbackQuery!.Data!.Split(':')[1]; + if (!MediaSessionManager.TryGet(sessionId, out var session) || session == null) + { + await botClient.AnswerCallbackQuery(update.CallbackQuery.Id, Config.GetResourceString("SessionExpiredMessage"), showAlert: true); + return; + } + + try { session.Cts.Cancel(); } catch (ObjectDisposedException) { } + + long chatId = session.ChatId; + int userId = _userGetter.GetUserIDbyTelegramID(chatId); + List mutedByUserIds = _userGetter.GetUsersIdForMuteContactId(userId); + int actionId = _defaultActionGetter.GetDefaultActionId(userId, UsersActionTypes.DEFAULT_MEDIA_DISTRIBUTION); + List groupIds = _defaultActionGetter.GetAllDefaultUsersActionTargets(userId, TargetTypes.GROUP, actionId); + List userIds = new List(); + + foreach (int groupId in groupIds) + { + userIds.AddRange(await _groupGetter.GetAllUsersIdsInGroup(groupId)); + } + + List targetUserIds = userIds + .Where(contactId => !mutedByUserIds.Contains(_userGetter.GetTelegramIDbyUserID(contactId))) + .Select(_userGetter.GetTelegramIDbyUserID) + .ToList(); + + int messageId = int.Parse(sessionId); + var statusMessage = update.CallbackQuery!.Message!; + + MediaSessionManager.Remove(sessionId); + + await botClient.EditMessageText( + chatId, + messageId, + Config.GetResourceString("WaitDownloadingVideo"), + cancellationToken: ct + ); + + _ = _tgBot.HandleMediaRequest(botClient, session.Url, chatId, statusMessage, targetUserIds, caption: session.Caption ?? ""); + } +} + +public class SendToSpecifiedUsersSessionCommand : IBotCallbackQueryHandlers +{ + private readonly TGBot _tgBot; + private readonly IUserGetter _userGetter; + private readonly IDefaultActionGetter _defaultActionGetter; + + public SendToSpecifiedUsersSessionCommand( + TGBot tgBot, + IUserGetter userGetter, + IDefaultActionGetter defaultActionGetter) + { + _tgBot = tgBot; + _userGetter = userGetter; + _defaultActionGetter = defaultActionGetter; + } + + public string Name => "send_to_specified_users:"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + string sessionId = update.CallbackQuery!.Data!.Split(':')[1]; + if (!MediaSessionManager.TryGet(sessionId, out var session) || session == null) + { + await botClient.AnswerCallbackQuery(update.CallbackQuery.Id, Config.GetResourceString("SessionExpiredMessage"), showAlert: true); + return; + } + + try { session.Cts.Cancel(); } catch (ObjectDisposedException) { } + + long chatId = session.ChatId; + int userId = _userGetter.GetUserIDbyTelegramID(chatId); + List mutedByUserIds = _userGetter.GetUsersIdForMuteContactId(userId); + int actionId = _defaultActionGetter.GetDefaultActionId(userId, UsersActionTypes.DEFAULT_MEDIA_DISTRIBUTION); + List userIds = _defaultActionGetter.GetAllDefaultUsersActionTargets(userId, TargetTypes.USER, actionId); + + List targetUserIds = userIds + .Where(contactId => !mutedByUserIds.Contains(_userGetter.GetTelegramIDbyUserID(contactId))) + .Select(_userGetter.GetTelegramIDbyUserID) + .ToList(); + + int messageId = int.Parse(sessionId); + var statusMessage = update.CallbackQuery!.Message!; + + MediaSessionManager.Remove(sessionId); + + await botClient.EditMessageText( + chatId, + messageId, + Config.GetResourceString("WaitDownloadingVideo"), + cancellationToken: ct + ); + + _ = _tgBot.HandleMediaRequest(botClient, session.Url, chatId, statusMessage, targetUserIds, caption: session.Caption ?? ""); + } +} + +public class CancelMediaSessionCommand : IBotCallbackQueryHandlers +{ + public string Name => "cancel_media:"; + + public async Task ExecuteAsync(Update update, ITelegramBotClient botClient, CancellationToken ct) + { + string sessionId = update.CallbackQuery!.Data!.Split(':')[1]; + if (!MediaSessionManager.TryGet(sessionId, out var session) || session == null) + { + await botClient.AnswerCallbackQuery(update.CallbackQuery.Id, Config.GetResourceString("SessionExpiredMessage"), showAlert: true); + return; + } + + long chatId = session.ChatId; + int messageId = int.Parse(sessionId); + + MediaSessionManager.Remove(sessionId); + + await botClient.EditMessageText( + chatId, + messageId, + Config.GetResourceString("CancelledMessage"), + cancellationToken: ct + ); + } +} diff --git a/TelegramBot/Handlers/PrivateUpdateHandler.cs b/TelegramBot/Handlers/PrivateUpdateHandler.cs index e573d3b..97477f5 100644 --- a/TelegramBot/Handlers/PrivateUpdateHandler.cs +++ b/TelegramBot/Handlers/PrivateUpdateHandler.cs @@ -11,6 +11,7 @@ using TelegramMediaRelayBot.TelegramBot.Utils; +using TelegramMediaRelayBot.TelegramBot.Sessions; using TelegramMediaRelayBot.Database; using TelegramMediaRelayBot.Database.Interfaces; @@ -71,20 +72,21 @@ public async Task ProcessMessage(ITelegramBotClient botClient, Update update, Ca cancellationToken: cancellationToken ); + string sessionId = statusMessage.MessageId.ToString(); + string? caption = string.IsNullOrWhiteSpace(text) ? null : text; + MediaSessionManager.Create(sessionId, chatId, link, caption); + await botClient.EditMessageText( statusMessage.Chat.Id, statusMessage.MessageId, Config.GetResourceString("VideoDistributionQuestion"), - replyMarkup: KeyboardUtils.GetVideoDistributionKeyboardMarkup(), + replyMarkup: KeyboardUtils.GetVideoDistributionKeyboardMarkup(sessionId), cancellationToken: cancellationToken ); int userId = _userGetter.GetUserIDbyTelegramID(chatId); string defaultActionData = _defaultActionGetter.GetDefaultActionByUserIDAndType(userId, UsersActionTypes.DEFAULT_MEDIA_DISTRIBUTION); - CancellationTokenSource timeoutCTS = new CancellationTokenSource(); - UserSessionManager.Set(chatId, new ProcessVideoDC(link, statusMessage, text, timeoutCTS, _tgBot, _contactGetterRepository, _userGetter, _groupGetter)); - if (defaultActionData == UsersAction.NO_VALUE) return; string defaultAction = defaultActionData.Split(';')[0]; @@ -93,7 +95,7 @@ await botClient.EditMessageText( if (defaultAction == UsersAction.OFF) return; var privateUtils = new PrivateUtils(_tgBot, _contactGetterRepository, _defaultActionGetter, _userGetter, _groupGetter); privateUtils.ProcessDefaultSendAction(botClient, chatId, statusMessage, defaultAction, cancellationToken, - userId, defaultCondition, timeoutCTS, link, text); + userId, defaultCondition, sessionId, link, text); } else if (update.Message.Text == "/start") { diff --git a/TelegramBot/Handlers/Utils.cs b/TelegramBot/Handlers/Utils.cs index 6643064..bbc4248 100644 --- a/TelegramBot/Handlers/Utils.cs +++ b/TelegramBot/Handlers/Utils.cs @@ -12,6 +12,7 @@ using TelegramMediaRelayBot.Database; using TelegramMediaRelayBot.Database.Interfaces; +using TelegramMediaRelayBot.TelegramBot.Sessions; namespace TelegramMediaRelayBot.TelegramBot.Handlers; @@ -40,14 +41,20 @@ IGroupGetter groupGetter } public void ProcessDefaultSendAction(ITelegramBotClient botClient, long chatId, Message statusMessage, string defaultAction, - CancellationToken cancellationToken, int userId, int defaultCondition, CancellationTokenSource timeoutCTS, + CancellationToken cancellationToken, int userId, int defaultCondition, string sessionId, string link, string text) { _ = Task.Run(async () => { try { - await Task.Delay(TimeSpan.FromSeconds(defaultCondition), timeoutCTS.Token); + var session = MediaSessionManager.Get(sessionId); + if (session == null) return; + + await Task.Delay(TimeSpan.FromSeconds(defaultCondition), session.Cts.Token); + + // Check session still exists (not cancelled by user pressing a button) + if (MediaSessionManager.Get(sessionId) == null) return; List targetUserIds = new List(); List mutedByUserIds = new List(); @@ -96,7 +103,8 @@ public void ProcessDefaultSendAction(ITelegramBotClient botClient, long chatId, break; } - if (UserSessionManager.TryGetValue(chatId, out var state) && state is ProcessVideoDC videoState) + // Remove session before processing (user didn't press any button in time) + if (MediaSessionManager.Remove(sessionId)) { await botClient.EditMessageText( statusMessage.Chat.Id, @@ -105,33 +113,6 @@ await botClient.EditMessageText( cancellationToken: cancellationToken ); _ = _tgBot.HandleMediaRequest(botClient, link, chatId, statusMessage, targetUserIds, caption: text); - - if (videoState.linkQueue.Count > 0) - { - var nextLink = videoState.linkQueue.Dequeue(); - statusMessage = await botClient.EditMessageText( - chatId, - nextLink.MessageId, - Config.GetResourceString("WaitDownloadingVideo"), - cancellationToken: cancellationToken - ); - ProcessDefaultSendAction( - botClient, - chatId, - statusMessage, - defaultAction, - cancellationToken, - userId, - defaultCondition, - timeoutCTS, - nextLink.Link, - nextLink.Text - ); - } - else - { - UserSessionManager.Remove(chatId, out _); - } } } catch (TaskCanceledException) { } diff --git a/TelegramBot/MediaDownloader.cs b/TelegramBot/MediaDownloader.cs index e59b524..107b466 100644 --- a/TelegramBot/MediaDownloader.cs +++ b/TelegramBot/MediaDownloader.cs @@ -15,6 +15,7 @@ using Telegram.Bot.Types.Enums; using TelegramMediaRelayBot.TelegramBot.Utils; using TelegramMediaRelayBot.TelegramBot.Handlers; +using TelegramMediaRelayBot.TelegramBot.Sessions; using TelegramMediaRelayBot.Database.Interfaces; using TelegramMediaRelayBot.Database; using TelegramMediaRelayBot.TelegramBot.SiteFilter; @@ -103,6 +104,13 @@ private async Task UpdateHandler(ITelegramBotClient botClient, Update update, Ca if (CommonUtilities.CheckPrivateChatType(update)) { + // Media session callbacks are handled by the factory, not by user state + if (update.CallbackQuery != null && IsMediaSessionCallback(update.CallbackQuery.Data)) + { + await _updateHandler.ProcessCallbackQuery(botClient, update, cancellationToken); + return; + } + if (UserSessionManager.ContainsKey(chatId)) { await ProcessState(botClient, update); @@ -376,6 +384,26 @@ public static void LogEvent(Update update, long chatId) Log.Information($"Event: {logMessageType}, UserId: {userId}, ChatId: {chatId}, {logMessageType}: {logMessageData}, State: {currentUserStatus}"); } + private static readonly string[] _mediaSessionPrefixes = new[] + { + "send_to_all_contacts:", + "send_to_default_groups:", + "send_to_specified_groups:", + "send_to_specified_users:", + "send_only_to_me:", + "cancel_media:", + }; + + private static bool IsMediaSessionCallback(string? data) + { + if (string.IsNullOrEmpty(data)) return false; + foreach (var prefix in _mediaSessionPrefixes) + { + if (data.StartsWith(prefix)) return true; + } + return false; + } + [GeneratedRegex(@"[^a-zA-Zа-яА-Я0-9]")] private static partial Regex MyRegex(); } \ No newline at end of file diff --git a/TelegramBot/Scheduler.cs b/TelegramBot/Scheduler.cs index 8dd9211..aaa5986 100644 --- a/TelegramBot/Scheduler.cs +++ b/TelegramBot/Scheduler.cs @@ -11,6 +11,7 @@ using System.Net; using TelegramMediaRelayBot.Database.Interfaces; +using TelegramMediaRelayBot.TelegramBot.Sessions; namespace TelegramMediaRelayBot.TelegramBot; @@ -36,6 +37,7 @@ public void Init() _unMuteTimer = new Timer(async _ => await CheckForUnmuteContacts(), null, TimeSpan.Zero, TimeSpan.FromSeconds(Config.userUnMuteCheckInterval)); if (Config.torEnabled) _torChangingChainTimer = new Timer(async _ => await TorChangingChain(), null, TimeSpan.Zero, TimeSpan.FromMinutes(Config.torChangingChainInterval)); UserSessionManager.StartCleanupTimer(); + MediaSessionManager.StartCleanupTimer(); Log.Information("Scheduler started"); } diff --git a/TelegramBot/Sessions/MediaSession.cs b/TelegramBot/Sessions/MediaSession.cs new file mode 100644 index 0000000..ae581d4 --- /dev/null +++ b/TelegramBot/Sessions/MediaSession.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + + +namespace TelegramMediaRelayBot.TelegramBot.Sessions; + +public class MediaSession +{ + public string SessionId { get; set; } + public long ChatId { get; set; } + public string Url { get; set; } + public string? Caption { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public CancellationTokenSource Cts { get; set; } = new(); + + public MediaSession(string sessionId, long chatId, string url, string? caption = null) + { + SessionId = sessionId; + ChatId = chatId; + Url = url; + Caption = caption; + } +} diff --git a/TelegramBot/Sessions/MediaSessionManager.cs b/TelegramBot/Sessions/MediaSessionManager.cs new file mode 100644 index 0000000..d97c728 --- /dev/null +++ b/TelegramBot/Sessions/MediaSessionManager.cs @@ -0,0 +1,101 @@ +// Copyright (C) 2024-2025 ZenonEl +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + + +using System.Collections.Concurrent; + + +namespace TelegramMediaRelayBot.TelegramBot.Sessions; + +public static class MediaSessionManager +{ + private static readonly ConcurrentDictionary _sessions = new(); + private static Timer? _cleanupTimer; + + public static void StartCleanupTimer() + { + _cleanupTimer = new Timer( + _ => CleanupExpired(), + null, + TimeSpan.FromMinutes(Config.sessionCleanupIntervalMinutes), + TimeSpan.FromMinutes(Config.sessionCleanupIntervalMinutes) + ); + Log.Information("MediaSession cleanup timer started (interval: {Interval} min, TTL: {TTL} min)", + Config.sessionCleanupIntervalMinutes, Config.sessionTtlMinutes); + } + + public static MediaSession Create(string sessionId, long chatId, string url, string? caption = null) + { + var session = new MediaSession(sessionId, chatId, url, caption); + _sessions[sessionId] = session; + return session; + } + + public static MediaSession? Get(string sessionId) + { + _sessions.TryGetValue(sessionId, out var session); + return session; + } + + public static bool TryGet(string sessionId, out MediaSession? session) + { + return _sessions.TryGetValue(sessionId, out session); + } + + public static bool Remove(string sessionId) + { + if (_sessions.TryRemove(sessionId, out var session)) + { + try { session.Cts.Cancel(); } + catch (ObjectDisposedException) { } + return true; + } + return false; + } + + public static bool Remove(string sessionId, out MediaSession? session) + { + if (_sessions.TryRemove(sessionId, out session)) + { + try { session.Cts.Cancel(); } + catch (ObjectDisposedException) { } + return true; + } + session = null; + return false; + } + + private static void CleanupExpired() + { + var now = DateTime.UtcNow; + var ttl = TimeSpan.FromMinutes(Config.sessionTtlMinutes); + int cleaned = 0; + + foreach (var kvp in _sessions) + { + if (now - kvp.Value.CreatedAt <= ttl) continue; + + if (_sessions.TryRemove(kvp.Key, out var session)) + { + try { session.Cts.Cancel(); } + catch (ObjectDisposedException) { } + cleaned++; + Log.Debug("Expired media session removed: {SessionId}", kvp.Key); + } + } + + if (cleaned > 0) + { + Log.Information("MediaSession cleanup: removed {Count} expired session(s), {Remaining} active", + cleaned, _sessions.Count); + } + } +} diff --git a/TelegramBot/Utils/KeyboardUtils.cs b/TelegramBot/Utils/KeyboardUtils.cs index be83501..9d2b0fb 100644 --- a/TelegramBot/Utils/KeyboardUtils.cs +++ b/TelegramBot/Utils/KeyboardUtils.cs @@ -113,29 +113,30 @@ public static Task SendInlineKeyboardMenu(ITelegramBotClient botClient, Update u return CommonUtilities.SendMessage(botClient, update, inlineKeyboard, cancellationToken, text); } - public static InlineKeyboardMarkup GetVideoDistributionKeyboardMarkup() -{ - var inlineKeyboard = new InlineKeyboardMarkup(new[] - { - new[] - { - InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToAllContactsButtonText"), "send_to_all_contacts"), - InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToDefaultGroupsButtonText"), "send_to_default_groups"), - }, - new[] - { - InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToSpecifiedGroupsButtonText"), "send_to_specified_groups"), - InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToSpecifiedUsersButtonText"), "send_to_specified_users"), - }, - new[] - { - InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendOnlyToMeButtonText"), "send_only_to_me"), - }, - new[] + public static InlineKeyboardMarkup GetVideoDistributionKeyboardMarkup(string? sessionId = null) + { + string suffix = sessionId != null ? $":{sessionId}" : ""; + var inlineKeyboard = new InlineKeyboardMarkup(new[] { - GetReturnButton() - }, - }); - return inlineKeyboard; -} + new[] + { + InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToAllContactsButtonText"), $"send_to_all_contacts{suffix}"), + InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToDefaultGroupsButtonText"), $"send_to_default_groups{suffix}"), + }, + new[] + { + InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToSpecifiedGroupsButtonText"), $"send_to_specified_groups{suffix}"), + InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendToSpecifiedUsersButtonText"), $"send_to_specified_users{suffix}"), + }, + new[] + { + InlineKeyboardButton.WithCallbackData(Config.GetResourceString("SendOnlyToMeButtonText"), $"send_only_to_me{suffix}"), + }, + new[] + { + InlineKeyboardButton.WithCallbackData(Config.GetResourceString("CancelButtonText"), $"cancel_media{suffix}"), + }, + }); + return inlineKeyboard; + } } \ No newline at end of file