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 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 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(); } /// /// Called by TelegramAdapter when an incoming message is received for this chat. /// Applies AllowedUserIds filtering and null/empty text filtering before raising the event. /// 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); } } } }