forked from admin/VolcanicAgenticCore
90 lines
4.0 KiB
C#
90 lines
4.0 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// (<c>AllowedUserIds[0]</c>) the bot is back. That message is never retried: a 403 (the owner never started the bot)
|
|
/// is logged by <see cref="TelegramSender"/> and that's all.
|
|
/// </summary>
|
|
public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions options, TelegramSender sender, ILogger<BotSetupService> 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)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The admin's chat also lists <c>/impersonate</c>. Best-effort and not retried: it fails while the admin has never
|
|
/// started the bot, and the command works without the menu entry.
|
|
/// </summary>
|
|
private async Task SetAdminMenuAsync(IEnumerable<BotCommand> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>True once <c>getMe</c> and <c>setMyCommands</c> succeeded; false when stopped first.</summary>
|
|
private async Task<bool> 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;
|
|
}
|
|
}
|