Files
Luna/Luna.Channels/Telegram/TelegramChannel.cs
T
2026-04-04 05:01:51 +02:00

161 lines
5.1 KiB
C#

using System.Text;
using Luna.Channels.Abstractions;
using Luna.Configuration;
using Luna.Shared;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;
namespace Luna.Channels.Telegram;
public class TelegramChannel(
string chatId,
ITelegramBotClient botClient,
ILogger<TelegramChannel> logger,
TelegramOptions options) : IChannel
{
private const int TelegramMessageCharLimit = 4096;
public string ChannelId { get; } = chatId;
public string ChannelType => Abstractions.ChannelType.Telegram;
public string DisplayName { get; } = $"Telegram Chat {chatId}";
public bool IsConnected { get; private set; } = true;
public event ChannelMessageReceivedEventHandler? MessageReceived;
public event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
public async Task SendMessageAsync(ChannelMessage message, CancellationToken cancellationToken = default)
{
try
{
var targetChatId = long.Parse(ChannelId);
await botClient.SendMessage(targetChatId, message.Content, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send message to Telegram chat {ChatId}", ChannelId);
}
}
public async Task SendStreamingMessageAsync(
IAsyncEnumerable<ChatStreamUpdate> messageStream,
CancellationToken cancellationToken = default)
{
var targetChatId = long.Parse(ChannelId);
var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var _ = KeepTypingAsync(targetChatId, typingCts.Token);
var messageBuilder = new StringBuilder();
await foreach (var update in messageStream.WithCancellation(cancellationToken))
{
if (update.Content is not null)
messageBuilder.Append(update.Content);
if (messageBuilder.Length <= TelegramMessageCharLimit)
continue;
try
{
var currentMessageContent = messageBuilder.ToString();
var splitByParagraph = currentMessageContent.Split('\n');
var i = 0;
var builder = new StringBuilder();
foreach (var paragraph in splitByParagraph)
{
if (builder.Length + paragraph.Length < TelegramMessageCharLimit)
{
builder.AppendLine(paragraph);
i++;
continue;
}
await botClient.SendMessage(
targetChatId,
builder.ToString(),
cancellationToken: cancellationToken);
messageBuilder.Clear();
messageBuilder.Append(string.Join('\n', splitByParagraph[i..]));
break;
}
}
catch (Exception ex)
{
await typingCts.CancelAsync();
logger.LogError(ex, "Failed to send streaming update to Telegram chat {ChatId}", ChannelId);
}
}
try
{
if (messageBuilder.Length > 0)
{
await botClient.SendMessage(
targetChatId,
messageBuilder.ToString(),
cancellationToken: cancellationToken);
}
}
catch (Exception ex)
{
await typingCts.CancelAsync();
logger.LogError(ex, "Failed to send streaming update to Telegram chat {ChatId}", ChannelId);
}
await typingCts.CancelAsync();
}
/// <summary>
/// Called by TelegramAdapter when an incoming message is received for this chat.
/// Applies AllowedUserIds filtering and null/empty text filtering before raising the event.
/// </summary>
public void RaiseMessageReceived(ChannelMessage message)
{
if (string.IsNullOrEmpty(message.Content))
return;
if (options.AllowedUserIds.Length > 0 &&
!options.AllowedUserIds.Contains(message.SenderId))
return;
MessageReceived?.Invoke(this, new ChannelMessageReceivedEventArgs
{
Channel = this,
Message = message,
IsHandled = false
});
}
public void Dispose()
{
IsConnected = false;
ConnectionStateChanged?.Invoke(this, new ChannelConnectionEventArgs
{
Channel = this,
IsConnected = false
});
}
private async Task KeepTypingAsync(long chatId, CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
await botClient.SendChatAction(chatId, ChatAction.Typing, null, null, ct);
await Task.Delay(4000, ct);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send typing action to Telegram chat {ChatId}", chatId);
}
}
}
}