using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types; using Volcanic.AgenticCore.Extensions.GoogleCalendar; using Volcanic.AgenticCore.Telegram.Chats; using Volcanic.AgenticCore.Telegram.Rendering; namespace Volcanic.AgenticCore.Telegram; /// /// Best-effort start-up calls: log who the bot is and publish the command menu, retried in the background until the /// first success so a host started without network still comes up. Then, once per process, tell the owner /// (AllowedUserIds[0]) the bot is back. That message is never retried: a 403 (the owner never started the bot) /// is logged by and that's all. /// public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions options, TelegramSender sender, ILogger logger, GoogleCalendarOptions? calendar = null) : BackgroundService { public const string OnlineText = "🟢 Online."; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (!await SetUpAsync(stoppingToken).ConfigureAwait(false) || options.AllowedUserIds.Count == 0) return; try { await sender.SendTextAsync(options.AllowedUserIds[0], OnlineText, cancellationToken: stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) { } } /// /// The admin's chat also lists /impersonate. Best-effort and not retried: it fails while the admin has never /// started the bot, and the command works without the menu entry. /// private async Task SetAdminMenuAsync(IEnumerable menu, CancellationToken stoppingToken) { if (options.AdminUserId is not { } admin) return; try { await bot.SetMyCommands([.. menu, ChatCommands.ImpersonateCommand], BotCommandScope.Chat(admin), cancellationToken: stoppingToken).ConfigureAwait(false); } catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning(ex, "Setting the admin's command menu failed"); } } /// True once getMe and setMyCommands succeeded; false when stopped first. private async Task SetUpAsync(CancellationToken stoppingToken) { var delay = options.ReconnectInitialDelay; while (!stoppingToken.IsCancellationRequested) { try { var me = await bot.GetMe(stoppingToken).ConfigureAwait(false); var menu = GoogleCalendarSetup.MenuFor(ChatCommands.Menu, calendar); await bot.SetMyCommands(menu, cancellationToken: stoppingToken).ConfigureAwait(false); logger.LogInformation("Bot @{Username} ({Id}) is ready", me.Username, me.Id); if (options.Triggers.IsEnabled && !me.CanReadAllGroupMessages) logger.LogWarning("Privacy mode is on: in groups the bot only sees commands, mentions and replies, so group triggers miss most messages. " + "Turn it off with @BotFather (/setprivacy → Disable), then remove and re-add the bot to each group, or make it a group admin."); await SetAdminMenuAsync(menu, stoppingToken).ConfigureAwait(false); return true; } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { return false; } catch (Exception ex) { logger.LogDebug(ex, "Bot setup failed; retrying in {Delay}", delay); } try { await Task.Delay(delay, stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) { return false; } delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, options.ReconnectMaxDelay.Ticks)); } return false; } }