Enhance TelegramNotifier to manage previous alerts and improve notification clarity
ci / build-backend (push) Successful in 1m21s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 1m33s
ci / sonar (push) Successful in 3m51s

Updated the TelegramNotifier to delete previous alert messages before sending new notifications, ensuring that only the latest alert is visible to subscribers. Introduced a new property in TelegramSubscriber to track the latest alert message ID. Adjusted related tests to verify the new alert management behavior and updated documentation to reflect these changes, enhancing the overall user experience in message handling.
This commit is contained in:
Leonid Pershin
2026-07-31 21:20:25 +03:00
parent 4a2e6ac7f1
commit 3dbe0b7698
7 changed files with 1739 additions and 18 deletions
@@ -162,7 +162,13 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
.Select(s => s.Name)
.FirstOrDefaultAsync(cancellationToken);
/// <summary>Шлёт всем, кто подписан на этот канал и вид. Возвращает число доставленных сообщений.</summary>
/// <summary>
/// Шлёт всем, кто подписан на этот канал и вид, и убирает своё предыдущее оповещение.
/// Возвращает число доставленных сообщений.
///
/// Подписчики читаются с отслеживанием: идентификатор нового сообщения тут же ложится на них,
/// а сохраняет его тот, кто вызвал рассылку, — одной транзакцией с границей разобранного.
/// </summary>
private async Task<int> SendAsync(
TelegramSettings settings,
Guid channelId,
@@ -171,26 +177,35 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
CancellationToken cancellationToken
)
{
var chats = await (
from subscription in dbContext.TelegramSubscriptions
join subscriber in dbContext.TelegramSubscribers
on subscription.SubscriberId equals subscriber.Id
where
subscription.ChannelId == channelId
&& subscription.Kind == kind
&& !subscriber.IsStopped
select subscriber.ChatId
)
.Distinct()
var subscribers = await dbContext
.TelegramSubscribers.Where(subscriber =>
!subscriber.IsStopped
&& subscriber.Subscriptions.Any(s => s.ChannelId == channelId && s.Kind == kind)
)
.ToListAsync(cancellationToken);
var sent = 0;
foreach (var chatId in chats)
foreach (var subscriber in subscribers)
{
// Один недоступный чат (бота заблокировали) не должен срывать рассылку остальным.
try
{
await api.SendMessageAsync(settings, chatId, text, null, cancellationToken);
if (subscriber.AlertMessageId is { } previous)
await api.DeleteMessageAsync(
settings,
subscriber.ChatId,
previous,
cancellationToken
);
var messageId = await api.SendMessageAsync(
settings,
subscriber.ChatId,
text,
null,
cancellationToken
);
subscriber.SetAlertMessage(messageId);
sent++;
}
catch (Exception exception) when (exception is not OperationCanceledException)
@@ -46,6 +46,14 @@ public class TelegramSubscriber
/// </summary>
public long? NoticeMessageId { get; private set; }
/// <summary>
/// Последнее оповещение рассылки («сейчас в эфире», «через 5 минут»). Живёт до следующего:
/// оповещение — это отметка о том, что идёт сейчас, и вчерашние «сейчас в эфире» в чате
/// превращают его в ленту неправды. Отдельно от <see cref="NoticeMessageId"/> намеренно:
/// рассылка приходит сама и не должна сносить программу, которую зритель только что запросил.
/// </summary>
public long? AlertMessageId { get; private set; }
public IReadOnlyList<TelegramSubscription> Subscriptions => _subscriptions;
private TelegramSubscriber() { }
@@ -82,6 +90,8 @@ public class TelegramSubscriber
public void SetNoticeMessage(long? messageId) => NoticeMessageId = messageId;
public void SetAlertMessage(long? messageId) => AlertMessageId = messageId;
public void Stop() => IsStopped = true;
public void Resume() => IsStopped = false;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TeleWave.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class TelegramAlertMessage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "AlertMessageId",
table: "TelegramSubscribers",
type: "bigint",
nullable: true
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "AlertMessageId", table: "TelegramSubscribers");
}
}
}
@@ -834,6 +834,9 @@ namespace TeleWave.Infrastructure.Migrations
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<long?>("AlertMessageId")
.HasColumnType("bigint");
b.Property<long>("ChatId")
.HasColumnType("bigint");
@@ -146,4 +146,61 @@ public class TelegramNotifierTests
// Граница разобранного двигается всегда — иначе следующий тик оповестил бы о том же.
Assert.Equal(Now, settings.NotifiedUntil);
}
[Fact]
public async Task Notifies_ReplacingItsPreviousAlert()
{
var fixture = new TestDb();
var channel = Channel.Create("Мультреалити", "mult", Now);
var previous = Show.Create("Симпсоны", ShowKind.Series);
var started = Show.Create("Футурама", ShowKind.Series);
var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
subscriber.Toggle(channel.Id, TelegramNotificationKind.ShowStart);
subscriber.SetAlertMessage(777);
// Программа, запрошенная зрителем, — не оповещение: рассылка её трогать не должна.
subscriber.SetNoticeMessage(555);
await using (var seed = fixture.New())
{
seed.Channels.Add(channel);
seed.Shows.AddRange(previous, started);
seed.TelegramSubscribers.Add(subscriber);
seed.ScheduleEntries.Add(Program(channel.Id, previous.Id, Now.AddMinutes(-30), 25));
seed.ScheduleEntries.Add(Program(channel.Id, started.Id, Now.AddMinutes(-1), 25));
await seed.SaveChangesAsync(CancellationToken.None);
}
var api = Substitute.For<ITelegramApi>();
api.SendMessageAsync(
Arg.Any<TelegramSettings>(),
Arg.Any<long>(),
Arg.Any<string>(),
Arg.Any<IReadOnlyList<IReadOnlyList<TelegramButton>>?>(),
Arg.Any<CancellationToken>()
)
.Returns(4242L);
var settings = Settings();
settings.MarkNotifiedUntil(Now.AddMinutes(-5));
await using (var db = fixture.New())
{
await new TelegramNotifier(db, api).RunAsync(settings, Now, CancellationToken.None);
await db.SaveChangesAsync(CancellationToken.None);
}
// Вчерашнее «сейчас в эфире» — уже неправда: старое снимаем, новое запоминаем.
await api.Received(1)
.DeleteMessageAsync(
Arg.Any<TelegramSettings>(),
100,
777,
Arg.Any<CancellationToken>()
);
await using var check = fixture.New();
var stored = check.TelegramSubscribers.Single();
Assert.Equal(4242L, stored.AlertMessageId);
Assert.Equal(555L, stored.NoticeMessageId);
}
}