feat(telegram): send 🟢 Online. to the owner once after bot setup
This commit is contained in:
@@ -2,16 +2,34 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
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.
|
||||
/// 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, ILogger<BotSetupService> logger) : BackgroundService
|
||||
public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions options, TelegramSender sender, ILogger<BotSetupService> logger) : 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>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)
|
||||
@@ -21,11 +39,11 @@ public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions opti
|
||||
var me = await bot.GetMe(stoppingToken).ConfigureAwait(false);
|
||||
await bot.SetMyCommands(ChatCommands.Menu, cancellationToken: stoppingToken).ConfigureAwait(false);
|
||||
logger.LogInformation("Bot @{Username} ({Id}) is ready", me.Username, me.Id);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -37,9 +55,10 @@ public sealed class BotSetupService(ITelegramBotClient bot, TelegramOptions opti
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
delay = TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, options.ReconnectMaxDelay.Ticks));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Telegram.Bot.Exceptions;
|
||||
using Telegram.Bot.Requests;
|
||||
using Telegram.Bot.Requests.Abstractions;
|
||||
using Volcanic.AgenticCore.Telegram.Rendering;
|
||||
using Volcanic.AgenticCore.Telegram.Tests.Support;
|
||||
|
||||
namespace Volcanic.AgenticCore.Telegram.Tests;
|
||||
|
||||
public sealed class BotSetupServiceTests
|
||||
{
|
||||
private static (BotSetupService Service, FakeBotClient Bot) Create(Action<IRequest>? onSend)
|
||||
{
|
||||
var bot = new FakeBotClient { OnSend = onSend };
|
||||
var options = new TelegramOptions
|
||||
{
|
||||
BotToken = "t",
|
||||
AllowedUserIds = [Updates.AllowedUser, 2002],
|
||||
ReconnectInitialDelay = TimeSpan.FromMilliseconds(5),
|
||||
ReconnectMaxDelay = TimeSpan.FromMilliseconds(20),
|
||||
};
|
||||
var sender = new TelegramSender(bot, NullLogger<TelegramSender>.Instance);
|
||||
return (new BotSetupService(bot, options, sender, NullLogger<BotSetupService>.Instance), bot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Online_goes_once_to_the_owner_after_setup_succeeds_on_the_second_try()
|
||||
{
|
||||
var getMeCalls = 0;
|
||||
var (service, bot) = Create(r =>
|
||||
{
|
||||
if (r is GetMeRequest && Interlocked.Increment(ref getMeCalls) == 1) throw FakeBotClient.NetworkDown();
|
||||
});
|
||||
|
||||
await service.StartAsync(CancellationToken.None);
|
||||
await service.ExecuteTask!.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await service.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, bot.Requests.OfType<GetMeRequest>().Count());
|
||||
var online = Assert.Single(bot.Sent);
|
||||
Assert.Equal(Updates.AllowedUser, online.ChatId.Identifier);
|
||||
Assert.Equal("🟢 Online.", online.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_forbidden_online_message_is_not_retried_and_does_not_stop_the_service()
|
||||
{
|
||||
var (service, bot) = Create(r =>
|
||||
{
|
||||
if (r is SendMessageRequest) throw new ApiRequestException("Forbidden: bot can't initiate conversation with a user", 403);
|
||||
});
|
||||
|
||||
await service.StartAsync(CancellationToken.None);
|
||||
await service.ExecuteTask!.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.True(service.ExecuteTask.IsCompletedSuccessfully);
|
||||
Assert.Single(bot.Requests.OfType<SendMessageRequest>());
|
||||
Assert.Single(bot.Requests.OfType<GetMeRequest>());
|
||||
await service.StopAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user