Add Telegram bot integration and related features
Implemented Telegram bot functionality, including settings management, subscriber tracking, and link generation for user interaction. Updated the backend to support new Telegram-related services and database entities. Enhanced the frontend to display Telegram options and allow users to open a chat with the bot. Localization strings were added for both English and Russian to support the new features. This integration aims to improve user engagement through Telegram notifications and interactions.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Notifications;
|
||||
using TeleWave.Application.Notifications.Link;
|
||||
using TeleWave.Application.Notifications.Settings;
|
||||
using TeleWave.Application.Notifications.Subscribers;
|
||||
using TeleWave.Domain.Notifications;
|
||||
|
||||
namespace TeleWave.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Бот расписания: настройка в админке, привязка чата зрителем и приём обновлений вебхуком.
|
||||
/// </summary>
|
||||
public static class TelegramEndpoints
|
||||
{
|
||||
/// <summary>Заголовок, которым Telegram подтверждает, что вызов вебхука — его.</summary>
|
||||
private const string SecretHeader = "X-Telegram-Bot-Api-Secret-Token";
|
||||
|
||||
public static IEndpointRouteBuilder MapTelegramEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var admin = app.MapGroup("/api/admin/telegram").RequireAuthorization("admin");
|
||||
|
||||
admin.MapGet("/settings", GetSettings).Produces<TelegramSettingsDto>();
|
||||
admin.MapPut("/settings", UpdateSettings).Produces<TelegramSettingsDto>();
|
||||
admin
|
||||
.MapGet("/subscribers", ListSubscribers)
|
||||
.Produces<IReadOnlyList<TelegramSubscriberDto>>();
|
||||
|
||||
// Настроен ли бот — публично: по этому флагу зрительская страница решает, показывать ли
|
||||
// кнопку. Ничего, кроме флага и имени бота, здесь не отдаётся.
|
||||
app.MapGet("/api/telegram/status", Status).AllowAnonymous().Produces<TelegramStatusDto>();
|
||||
|
||||
// Ссылка на чат — зрителю, а не админу: код привязывает его собственную учётку.
|
||||
app.MapPost("/api/telegram/link", IssueLink)
|
||||
.RequireAuthorization()
|
||||
.Produces<TelegramLinkDto>();
|
||||
|
||||
// Вебхук открыт наружу по построению: его вызывает Telegram, а не наш фронт. Пускаем
|
||||
// только по секрету из настроек — адрес рано или поздно окажется в чужих логах.
|
||||
app.MapPost("/api/telegram/webhook", Webhook).AllowAnonymous();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> Status(
|
||||
IAppDbContext dbContext,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var settings = await dbContext
|
||||
.TelegramSettings.AsNoTracking()
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var link = settings is null ? null : TelegramSettingsService.BotLink(settings);
|
||||
return Results.Ok(new TelegramStatusDto(link is not null, settings?.BotUsername));
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetSettings(
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new GetTelegramSettingsQuery(), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdateSettings(
|
||||
TelegramSettingsBody body,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(
|
||||
new UpdateTelegramSettingsCommand(
|
||||
body.IsEnabled,
|
||||
body.Transport,
|
||||
body.BotToken,
|
||||
body.WebhookUrl,
|
||||
body.ProxyKind,
|
||||
body.ProxyHost,
|
||||
body.ProxyPort,
|
||||
body.ProxyUsername,
|
||||
body.ProxyPassword
|
||||
),
|
||||
cancellationToken
|
||||
);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListSubscribers(
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new ListTelegramSubscribersQuery(), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> IssueLink(
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new IssueTelegramLinkCommand(), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Приём обновления от Telegram. Разбираем тем же сервисом, что и опрос: транспорт разный,
|
||||
/// разговор один.
|
||||
///
|
||||
/// Отвечаем 200 всегда, когда секрет сошёлся: на ошибку Telegram повторяет доставку, и одно
|
||||
/// кривое обновление зациклило бы вебхук.
|
||||
/// </summary>
|
||||
private static async Task<IResult> Webhook(
|
||||
HttpRequest request,
|
||||
IAppDbContext dbContext,
|
||||
TelegramBotService bot,
|
||||
ITelegramApi api,
|
||||
ILoggerFactory loggers,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var settings = await dbContext.TelegramSettings.FirstOrDefaultAsync(cancellationToken);
|
||||
if (
|
||||
settings is null
|
||||
|| !settings.IsOperational
|
||||
|| settings.Transport != TelegramTransport.Webhook
|
||||
)
|
||||
return Results.NotFound();
|
||||
|
||||
var secret = request.Headers[SecretHeader].ToString();
|
||||
if (string.IsNullOrWhiteSpace(settings.WebhookSecret) || secret != settings.WebhookSecret)
|
||||
return Results.Unauthorized();
|
||||
|
||||
var update = await api.ReadUpdateAsync(request.Body, cancellationToken);
|
||||
if (update is null)
|
||||
return Results.Ok();
|
||||
|
||||
var logger = loggers.CreateLogger("Telegram.Webhook");
|
||||
try
|
||||
{
|
||||
await bot.HandleAsync(settings, update, DateTimeOffset.UtcNow, cancellationToken);
|
||||
settings.MarkContact(DateTimeOffset.UtcNow);
|
||||
settings.AdvanceUpdateId(update.UpdateId);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(exception, "Не разобрано обновление {UpdateId}", update.UpdateId);
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Что зрительской части нужно знать о боте: настроен ли и как он называется.</summary>
|
||||
public sealed record TelegramStatusDto(bool IsConfigured, string? BotUsername);
|
||||
|
||||
/// <summary>Тело правки настроек: секреты необязательны — пусто значит «не менять».</summary>
|
||||
public sealed record TelegramSettingsBody(
|
||||
bool IsEnabled,
|
||||
TelegramTransport Transport,
|
||||
string? BotToken,
|
||||
string? WebhookUrl,
|
||||
TelegramProxyKind ProxyKind,
|
||||
string? ProxyHost,
|
||||
int? ProxyPort,
|
||||
string? ProxyUsername,
|
||||
string? ProxyPassword
|
||||
);
|
||||
@@ -133,6 +133,7 @@ app.MapTemplateEndpoints();
|
||||
app.MapBumperEndpoints();
|
||||
app.MapJunctionEndpoints();
|
||||
app.MapChannelEndpoints();
|
||||
app.MapTelegramEndpoints();
|
||||
app.MapStreamingEndpoints();
|
||||
app.MapIptvEndpoints();
|
||||
app.MapMaintenanceEndpoints();
|
||||
|
||||
Reference in New Issue
Block a user