Add channel system with CLI, Telegram, and Web channel implementations
Define channel abstractions (IChannel, IChannelManager) with event-driven message and connection handling. Implement ChannelManager for multi-channel orchestration, CliChannel for terminal I/O, TelegramChannel with adapter pattern for bot API integration, and WebChannel stub. Includes DI registration and channel type enumeration.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
int? currentMessageId = null;
|
||||
|
||||
var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
var typingTask = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user