Enhance TelegramBotService with schedule handling and subscription toggling
Updated the TelegramBotService to support new actions for managing subscriptions and retrieving channel schedules. Introduced constants for action types and improved the callback data structure for better clarity. Added a method to send channel schedules, consolidating upcoming program information into a user-friendly format. Updated related tests to ensure proper functionality of the new features and adjusted documentation to reflect changes in button actions.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Notifications;
|
||||
|
||||
namespace TeleWave.Application.Notifications;
|
||||
@@ -15,9 +16,18 @@ namespace TeleWave.Application.Notifications;
|
||||
/// </summary>
|
||||
public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api)
|
||||
{
|
||||
/// <summary>Разделитель в callback: идентификатор канала и вид оповещения.</summary>
|
||||
/// <summary>Разделитель полей в callback: действие, канал и (для подписки) вид оповещения.</summary>
|
||||
private const char Separator = ':';
|
||||
|
||||
/// <summary>Переключить подписку: <c>sub:канал:вид</c>.</summary>
|
||||
private const string ToggleAction = "sub";
|
||||
|
||||
/// <summary>Показать программу канала: <c>epg:канал</c>.</summary>
|
||||
private const string ScheduleAction = "epg";
|
||||
|
||||
/// <summary>Сколько программ показывать: экран телефона, а не полные сутки.</summary>
|
||||
private const int ScheduleBlocks = 8;
|
||||
|
||||
public async Task HandleAsync(
|
||||
TelegramSettings settings,
|
||||
TelegramUpdate update,
|
||||
@@ -156,23 +166,42 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
|
||||
}
|
||||
|
||||
var parts = data.Split(Separator);
|
||||
if (
|
||||
parts.Length != 2
|
||||
|| !Guid.TryParse(parts[0], out var channelId)
|
||||
|| !Enum.TryParse<TelegramNotificationKind>(parts[1], out var kind)
|
||||
)
|
||||
if (parts.Length < 2 || !Guid.TryParse(parts[1], out var channelId))
|
||||
{
|
||||
await AnswerAsync(settings, update, null, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var known = await dbContext.Channels.AnyAsync(c => c.Id == channelId, cancellationToken);
|
||||
if (!known)
|
||||
var channel = await dbContext
|
||||
.Channels.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == channelId, cancellationToken);
|
||||
if (channel is null)
|
||||
{
|
||||
await AnswerAsync(settings, update, Texts.ChannelGone, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
// Расписание приходит отдельным сообщением, а меню остаётся на месте: иначе после
|
||||
// просмотра программы пришлось бы вызывать /start, чтобы вернуть кнопки.
|
||||
if (parts[0] == ScheduleAction)
|
||||
{
|
||||
subscriber.Touch(now);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await AnswerAsync(settings, update, null, cancellationToken);
|
||||
await SendScheduleAsync(settings, channel, chatId, now, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
parts.Length != 3
|
||||
|| parts[0] != ToggleAction
|
||||
|| !Enum.TryParse<TelegramNotificationKind>(parts[2], out var kind)
|
||||
)
|
||||
{
|
||||
await AnswerAsync(settings, update, null, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var enabled = subscriber.Toggle(channelId, kind);
|
||||
subscriber.Touch(now);
|
||||
subscriber.Resume();
|
||||
@@ -233,7 +262,10 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
keyboard.Add([
|
||||
new TelegramButton($"— {channel.Name} —", $"{channel.Id}{Separator}none"),
|
||||
new TelegramButton(
|
||||
$"📋 {channel.Name}",
|
||||
$"{ScheduleAction}{Separator}{channel.Id}"
|
||||
),
|
||||
]);
|
||||
keyboard.Add([
|
||||
Button(subscriber, channel.Id, TelegramNotificationKind.ShowStart, "Начало шоу"),
|
||||
@@ -262,7 +294,87 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
|
||||
)
|
||||
{
|
||||
var on = subscriber.IsSubscribed(channelId, kind);
|
||||
return new TelegramButton($"{(on ? "✅" : "▫️")} {label}", $"{channelId}{Separator}{kind}");
|
||||
return new TelegramButton(
|
||||
$"{(on ? "✅" : "▫️")} {label}",
|
||||
$"{ToggleAction}{Separator}{channelId}{Separator}{kind}"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Программа канала: что идёт сейчас и что дальше.
|
||||
///
|
||||
/// Подряд идущие серии одного шоу схлопываются в один блок — зритель спрашивает «что сегодня»,
|
||||
/// а не «в каком порядке лежат файлы», и четыре строки «Симпсоны» подряд ответом не являются.
|
||||
/// Времена — в часовом поясе канала: сетка задаётся в нём, и переводить их зрителю в UTC
|
||||
/// значило бы показывать программу чужого телевизора.
|
||||
/// </summary>
|
||||
private async Task SendScheduleAsync(
|
||||
TelegramSettings settings,
|
||||
Channel channel,
|
||||
long chatId,
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var offset = TimeSpan.FromMinutes(channel.UtcOffsetMinutes);
|
||||
|
||||
// Берём с запасом: врезки и заставки в программу не идут, а серии схлопываются в блоки,
|
||||
// поэтому записей нужно заметно больше, чем строк на выходе.
|
||||
var entries = await dbContext
|
||||
.ScheduleEntries.AsNoTracking()
|
||||
.Where(e =>
|
||||
e.ChannelId == channel.Id
|
||||
&& e.Kind == ScheduleEntryKind.Program
|
||||
&& e.ShowId != null
|
||||
&& e.EndsAtUtc > now
|
||||
)
|
||||
.OrderBy(e => e.StartsAtUtc)
|
||||
.Take(ScheduleBlocks * 12)
|
||||
.Select(e => new
|
||||
{
|
||||
e.StartsAtUtc,
|
||||
e.EndsAtUtc,
|
||||
ShowId = e.ShowId!.Value,
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
await api.SendMessageAsync(settings, chatId, Texts.NoSchedule, null, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var names = await dbContext
|
||||
.Shows.AsNoTracking()
|
||||
.Where(s => entries.Select(e => e.ShowId).Contains(s.Id))
|
||||
.ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken);
|
||||
|
||||
var lines = new List<string> { $"<b>{TelegramText.Escape(channel.Name)}</b>" };
|
||||
Guid? previousShow = null;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.ShowId == previousShow)
|
||||
continue;
|
||||
|
||||
previousShow = entry.ShowId;
|
||||
if (lines.Count > ScheduleBlocks)
|
||||
break;
|
||||
|
||||
var local = entry.StartsAtUtc.ToOffset(offset);
|
||||
var title = TelegramText.Escape(names.GetValueOrDefault(entry.ShowId) ?? "—");
|
||||
// Идущее сейчас помечаем: без отметки первая строка читается как «начнётся в 20:15».
|
||||
var mark = entry.StartsAtUtc <= now ? "▶️" : "🕔";
|
||||
lines.Add($"{mark} {local:HH:mm} — {title}");
|
||||
}
|
||||
|
||||
await api.SendMessageAsync(
|
||||
settings,
|
||||
chatId,
|
||||
string.Join('\n', lines),
|
||||
null,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
private Task<TelegramSubscriber?> FindAsync(long chatId, CancellationToken cancellationToken) =>
|
||||
@@ -295,5 +407,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
|
||||
public const string Unsubscribed = "Выключено";
|
||||
|
||||
public const string ChannelGone = "Канала больше нет";
|
||||
|
||||
public const string NoSchedule = "Программа ещё не собрана.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
|
||||
settings,
|
||||
entry.ChannelId,
|
||||
TelegramNotificationKind.ShowStart,
|
||||
$"▶️ <b>{Escape(title)}</b>\nСейчас в эфире",
|
||||
$"▶️ <b>{TelegramText.Escape(title)}</b>\nСейчас в эфире",
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
@@ -123,7 +123,7 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
|
||||
settings,
|
||||
entry.ChannelId,
|
||||
TelegramNotificationKind.UpNext,
|
||||
$"🕔 Через {minutes} мин: <b>{Escape(title)}</b>",
|
||||
$"🕔 Через {minutes} мин: <b>{TelegramText.Escape(title)}</b>",
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
@@ -201,8 +201,4 @@ public sealed class TelegramNotifier(IAppDbContext dbContext, ITelegramApi api)
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
/// <summary>Экранирование под HTML-разметку Telegram: в названиях шоу встречаются «&» и кавычки.</summary>
|
||||
private static string Escape(string value) =>
|
||||
value.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TeleWave.Application.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Подготовка текста для Telegram. Сообщения уходят с HTML-разметкой (жирный заголовок программы),
|
||||
/// поэтому названия шоу нужно экранировать: «Tom & Jerry» иначе обрывает сообщение на месте
|
||||
/// амперсанда, а угловые скобки Telegram примет за свой тег.
|
||||
/// </summary>
|
||||
public static class TelegramText
|
||||
{
|
||||
public static string Escape(string value) =>
|
||||
value.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
}
|
||||
Reference in New Issue
Block a user