Enhance Telegram bot functionality with message management and new API methods
ci / build-backend (push) Successful in 3m27s
ci / build-frontend (push) Successful in 1m5s
ci / tests (push) Successful in 4m0s
ci / sonar (push) Successful in 8m22s

Updated the ITelegramApi interface to include a method for deleting messages and modified SendMessageAsync to return the message ID. Enhanced the TelegramBotService to manage message visibility by replacing previous messages with new ones, ensuring a cleaner user experience. Introduced properties in TelegramSubscriber to track the latest menu and notice message IDs. Updated related tests to verify the new message management behavior and adjusted documentation to reflect these changes.
This commit is contained in:
Leonid Pershin
2026-07-31 08:24:42 +03:00
parent 8a439f1351
commit d6bc2b15ac
9 changed files with 1835 additions and 24 deletions
@@ -60,9 +60,13 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
// Любое другое сообщение — это «я не знаю, что нажать»: показываем меню, если чат известен.
var subscriber = await FindAsync(chatId, cancellationToken);
if (subscriber is null)
await api.SendMessageAsync(settings, chatId, Texts.NeedLink, null, cancellationToken);
else
await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
{
await NoticeAsync(settings, null, chatId, Texts.NeedLink, cancellationToken);
return;
}
await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
/// <summary>
@@ -89,20 +93,14 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
// Уже привязанный чат просто открывает меню: заставлять его снова ходить за кодом незачем.
if (subscriber is null)
{
await api.SendMessageAsync(
settings,
chatId,
Texts.NeedLink,
null,
cancellationToken
);
await NoticeAsync(settings, null, chatId, Texts.NeedLink, cancellationToken);
return;
}
subscriber.Resume();
subscriber.Touch(now);
await dbContext.SaveChangesAsync(cancellationToken);
await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return;
}
@@ -112,7 +110,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
);
if (link is null || !link.IsUsable(now))
{
await api.SendMessageAsync(settings, chatId, Texts.BadCode, null, cancellationToken);
await NoticeAsync(settings, subscriber, chatId, Texts.BadCode, cancellationToken);
return;
}
@@ -128,9 +126,9 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
subscriber.Rebind(link.UserId, from, now);
}
await dbContext.SaveChangesAsync(cancellationToken);
await api.SendMessageAsync(settings, chatId, Texts.Linked, null, cancellationToken);
await NoticeAsync(settings, subscriber, chatId, Texts.Linked, cancellationToken);
await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
private async Task HandleStopAsync(
@@ -145,8 +143,8 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
// Подписки не трогаем: вернувшись по /start, зритель получит свои каналы, а не пустой список.
subscriber.Stop();
await NoticeAsync(settings, subscriber, chatId, Texts.Stopped, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
await api.SendMessageAsync(settings, chatId, Texts.Stopped, null, cancellationToken);
}
private async Task HandleCallbackAsync(
@@ -188,7 +186,8 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
subscriber.Touch(now);
await dbContext.SaveChangesAsync(cancellationToken);
await AnswerAsync(settings, update, null, cancellationToken);
await SendScheduleAsync(settings, channel, chatId, now, cancellationToken);
await SendScheduleAsync(settings, subscriber, channel, chatId, now, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return;
}
@@ -220,6 +219,28 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
update.CallbackMessageId,
cancellationToken
);
await dbContext.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// Одноразовое сообщение бота: программа передач, подтверждение привязки, отказ. Предыдущее
/// такое же снимается — три программы на разное время в чате не нужны никому.
/// </summary>
private async Task NoticeAsync(
TelegramSettings settings,
TelegramSubscriber? subscriber,
long chatId,
string text,
CancellationToken cancellationToken
)
{
if (subscriber?.NoticeMessageId is { } previous)
await api.DeleteMessageAsync(settings, chatId, previous, cancellationToken);
var messageId = await api.SendMessageAsync(settings, chatId, text, null, cancellationToken);
// Незнакомому чату помнить нечего: подписчика ещё нет, и следующее сообщение будет первым.
subscriber?.SetNoticeMessage(messageId);
}
private Task AnswerAsync(
@@ -254,7 +275,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
if (channels.Count == 0)
{
await api.SendMessageAsync(settings, chatId, Texts.NoChannels, null, cancellationToken);
await NoticeAsync(settings, subscriber, chatId, Texts.NoChannels, cancellationToken);
return;
}
@@ -273,7 +294,10 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
]);
}
// Нажали кнопку — правим то же сообщение. Пришли заново — старое меню снимаем: две панели
// с разными галочками в одном чате означают, что одна из них врёт.
if (messageId is { } id)
{
await api.EditMessageAsync(
settings,
chatId,
@@ -282,8 +306,21 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
keyboard,
cancellationToken
);
else
await api.SendMessageAsync(settings, chatId, Texts.Menu, keyboard, cancellationToken);
subscriber.SetMenuMessage(id);
return;
}
if (subscriber.MenuMessageId is { } previous)
await api.DeleteMessageAsync(settings, chatId, previous, cancellationToken);
var sent = await api.SendMessageAsync(
settings,
chatId,
Texts.Menu,
keyboard,
cancellationToken
);
subscriber.SetMenuMessage(sent);
}
private static TelegramButton Button(
@@ -310,6 +347,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
/// </summary>
private async Task SendScheduleAsync(
TelegramSettings settings,
TelegramSubscriber subscriber,
Channel channel,
long chatId,
DateTimeOffset now,
@@ -340,7 +378,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
if (entries.Count == 0)
{
await api.SendMessageAsync(settings, chatId, Texts.NoSchedule, null, cancellationToken);
await NoticeAsync(settings, subscriber, chatId, Texts.NoSchedule, cancellationToken);
return;
}
@@ -396,6 +434,9 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
public const string Menu =
"<b>Оповещения</b>\nНажмите, чтобы включить или выключить. ✅ — включено.\n"
// Кнопка-заголовок ничем не выдаёт, что она кнопка: про программу передач надо сказать
// словами, иначе её найдут только промахнувшись мимо тумблера.
+ "Программа передач — нажмите на название канала.\n"
+ "Остановить всё — командой /stop.";
public const string NoChannels = "Пока нет ни одного включённого канала.";