diff --git a/backend/src/TeleWave.Application/Notifications/TelegramNotifier.cs b/backend/src/TeleWave.Application/Notifications/TelegramNotifier.cs
index f3bc59a..237e9d0 100644
--- a/backend/src/TeleWave.Application/Notifications/TelegramNotifier.cs
+++ b/backend/src/TeleWave.Application/Notifications/TelegramNotifier.cs
@@ -37,20 +37,36 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
? marker
: now - MaxCatchUp;
+ // Имена каналов — одним запросом на тик: их десятки, а оповещений за тик единицы, и ходить
+ // за названием по каждому значило бы столько же одинаковых запросов.
+ var channels = await dbContext
+ .Channels.AsNoTracking()
+ .ToDictionaryAsync(c => c.Id, c => c.Name, cancellationToken);
+
var sent = 0;
- sent += await NotifyStartedAsync(settings, from, now, cancellationToken);
- sent += await NotifyUpNextAsync(settings, from, now, cancellationToken);
+ sent += await NotifyStartedAsync(settings, channels, from, now, cancellationToken);
+ sent += await NotifyUpNextAsync(settings, channels, from, now, cancellationToken);
settings.MarkNotifiedUntil(now);
return sent;
}
+ ///
+ /// Подпись канала под заголовком программы. Каналов у зрителя несколько, а «Сейчас в эфире»
+ /// без названия не отвечает на вопрос «где» — по такому сообщению непонятно, куда переключаться.
+ ///
+ private static string ChannelLine(IReadOnlyDictionary channels, Guid channelId) =>
+ channels.GetValueOrDefault(channelId) is { } name
+ ? $" · {TelegramText.Escape(name)}"
+ : string.Empty;
+
///
/// Начавшиеся программы. Оповещаем о смене шоу, а не о каждой записи: четыре серии подряд —
/// это одна программа в глазах зрителя, и четыре сообщения о ней читаются как спам.
///
private async Task NotifyStartedAsync(
TelegramSettings settings,
+ IReadOnlyDictionary channels,
DateTimeOffset from,
DateTimeOffset now,
CancellationToken cancellationToken
@@ -81,7 +97,7 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
settings,
entry.ChannelId,
TelegramNotificationKind.ShowStart,
- $"▶️ {TelegramText.Escape(title)}\nСейчас в эфире",
+ $"▶️ {TelegramText.Escape(title)}\nСейчас в эфире{ChannelLine(channels, entry.ChannelId)}",
cancellationToken
);
}
@@ -92,6 +108,7 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
/// Что начнётся через — предупреждаем один раз, в своё окно.
private async Task NotifyUpNextAsync(
TelegramSettings settings,
+ IReadOnlyDictionary channels,
DateTimeOffset from,
DateTimeOffset now,
CancellationToken cancellationToken
@@ -123,7 +140,7 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
settings,
entry.ChannelId,
TelegramNotificationKind.UpNext,
- $"🕔 Через {minutes} мин: {TelegramText.Escape(title)}",
+ $"🕔 {TelegramText.Escape(title)}\nЧерез {minutes} мин{ChannelLine(channels, entry.ChannelId)}",
cancellationToken
);
}
diff --git a/backend/tests/TeleWave.Application.Tests/Notifications/TelegramNotifierTests.cs b/backend/tests/TeleWave.Application.Tests/Notifications/TelegramNotifierTests.cs
index 382235c..0edb2b3 100644
--- a/backend/tests/TeleWave.Application.Tests/Notifications/TelegramNotifierTests.cs
+++ b/backend/tests/TeleWave.Application.Tests/Notifications/TelegramNotifierTests.cs
@@ -136,9 +136,11 @@ public class TelegramNotifierTests
.SendMessageAsync(
Arg.Any(),
100,
+ // Канал назван в самом сообщении: их у зрителя несколько, и «сейчас в эфире»
+ // без названия не отвечает на вопрос, куда переключаться.
// Предикат NSubstitute принимает аргумент как допускающий null — само сообщение
// объявлено обязательным, потому и разыменовываем без проверки.
- Arg.Is(s => s!.Contains("Футурама")),
+ Arg.Is(s => s!.Contains("Футурама") && s.Contains("Мультреалити")),
null,
Arg.Any()
);
@@ -147,6 +149,50 @@ public class TelegramNotifierTests
Assert.Equal(Now, settings.NotifiedUntil);
}
+ [Fact]
+ public async Task Notifies_UpNext_TellsWhenAndWhere()
+ {
+ var fixture = new TestDb();
+ var channel = Channel.Create("Мультреалити", "mult", Now);
+ var soon = Show.Create("Гриффины", ShowKind.Series);
+
+ var subscriber = TelegramSubscriber.Create(100, Guid.NewGuid(), "viewer", Now);
+ subscriber.Toggle(channel.Id, TelegramNotificationKind.UpNext);
+
+ await using (var seed = fixture.New())
+ {
+ seed.Channels.Add(channel);
+ seed.Shows.Add(soon);
+ seed.TelegramSubscribers.Add(subscriber);
+ seed.ScheduleEntries.Add(Program(channel.Id, soon.Id, Now.AddMinutes(4), 25));
+ await seed.SaveChangesAsync(CancellationToken.None);
+ }
+
+ var api = Substitute.For();
+ var settings = Settings();
+ // Окно предупреждения — свои пять минут: разобранное кончается на Now, а смотрим до Now+5.
+ settings.MarkNotifiedUntil(Now.AddMinutes(-5));
+
+ await using var db = fixture.New();
+ var sent = await new TelegramNotifier(db, api).RunAsync(
+ settings,
+ Now,
+ CancellationToken.None
+ );
+
+ Assert.Equal(1, sent);
+ await api.Received(1)
+ .SendMessageAsync(
+ Arg.Any(),
+ 100,
+ Arg.Is(s =>
+ s!.Contains("Гриффины") && s.Contains("4 мин") && s.Contains("Мультреалити")
+ ),
+ null,
+ Arg.Any()
+ );
+ }
+
[Fact]
public async Task Notifies_ReplacingItsPreviousAlert()
{
diff --git a/docs/telegram-bot.md b/docs/telegram-bot.md
index 02b7a66..1758f98 100644
--- a/docs/telegram-bot.md
+++ b/docs/telegram-bot.md
@@ -43,6 +43,9 @@ NAT и через прокси, и это единственный вариан
в глазах зрителя, и четыре сообщения о ней читаются как спам.
- **Скоро в эфире** — за пять минут до следующей программы.
+Оба называют канал: подписок у зрителя несколько, и «Сейчас в эфире» без названия не отвечает
+на вопрос «где» — по такому сообщению непонятно, куда переключаться.
+
Граница разобранного (`TelegramSettings.NotifiedUntil`) двигается на каждом тике, даже когда
подписчиков нет: иначе первый же подписавшийся получил бы пачку сообщений про всё, что успело
пройти. Догон ограничен пятнадцатью минутами — после долгого простоя бот не станет пересказывать