Introduce Luna.Identity project with user identity service, pairing code generation, and channel linking. Channels (Telegram, CLI) now verify link status before routing messages and prompt users to pair via the web interface. WebChannel handles /link <channel-type> <code> commands to complete the pairing flow. Fix inverted expiration check in IsPairingCodeExpired that caused codes to be treated as expired immediately after creation.
130 lines
4.2 KiB
C#
130 lines
4.2 KiB
C#
using Luna.Channels.Abstractions;
|
|
using Luna.Configuration;
|
|
using Luna.Identity.Abstractions;
|
|
using Luna.Shared;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Telegram.Bot;
|
|
using Telegram.Bot.Polling;
|
|
using Telegram.Bot.Types;
|
|
|
|
namespace Luna.Channels.Telegram;
|
|
|
|
public class TelegramAdapter(
|
|
IChannelManager channelManager,
|
|
IUserIdentityService identityService,
|
|
ITelegramBotClient botClient,
|
|
TelegramOptions options,
|
|
ILogger<TelegramAdapter> adapterLogger,
|
|
ILogger<TelegramChannel> channelLogger) : IHostedService, IUpdateHandler
|
|
{
|
|
private readonly Dictionary<long, TelegramChannel> channels = new();
|
|
private CancellationTokenSource? cts;
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
cts = new CancellationTokenSource();
|
|
botClient.StartReceiving(this, new ReceiverOptions(), cts.Token);
|
|
adapterLogger.LogInformation("Telegram adapter started polling");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
cts?.Cancel();
|
|
cts?.Dispose();
|
|
|
|
foreach (var (_, channel) in channels)
|
|
{
|
|
channelManager.UnregisterChannel(channel.ChannelId);
|
|
channel.Dispose();
|
|
}
|
|
|
|
channels.Clear();
|
|
adapterLogger.LogInformation("Telegram adapter stopped");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task HandleUpdateAsync(ITelegramBotClient client, Update update, CancellationToken cancellationToken)
|
|
{
|
|
if (update.Message is not { } message)
|
|
return;
|
|
|
|
if (message.Text is null or "")
|
|
return;
|
|
|
|
var chat = message.Chat;
|
|
var channel = GetOrCreateChannel(chat.Id);
|
|
|
|
if (message.Text.StartsWith('/'))
|
|
{
|
|
await HandleCommandAsync(channel, message, cancellationToken);
|
|
return;
|
|
}
|
|
|
|
if (await channel.VerifyChannelLinkAsync() == false)
|
|
return;
|
|
|
|
var channelMessage = new ChannelMessage(
|
|
MessageId: message.Id.ToString(),
|
|
ConversationId: chat.Id.ToString(),
|
|
SenderId: message.From?.Id.ToString() ?? "unknown",
|
|
SenderName: message.From?.FirstName ?? "Unknown",
|
|
Content: message.Text,
|
|
Timestamp: new DateTimeOffset(message.Date, TimeSpan.Zero));
|
|
|
|
channel.RaiseMessageReceived(channelMessage);
|
|
}
|
|
|
|
public Task HandleErrorAsync(ITelegramBotClient client, Exception exception, HandleErrorSource source,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
adapterLogger.LogError(exception, "Telegram polling error from {Source}", source);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private TelegramChannel GetOrCreateChannel(long chatId)
|
|
{
|
|
if (channels.TryGetValue(chatId, out var existing))
|
|
return existing;
|
|
|
|
var channel = new TelegramChannel(
|
|
chatId.ToString(),
|
|
botClient,
|
|
identityService,
|
|
channelLogger,
|
|
options);
|
|
|
|
channels[chatId] = channel;
|
|
channelManager.RegisterChannel(channel);
|
|
adapterLogger.LogInformation("Created TelegramChannel for chat {ChatId}", chatId);
|
|
|
|
return channel;
|
|
}
|
|
|
|
private async Task HandleCommandAsync(TelegramChannel channel, Message message, CancellationToken cancellationToken)
|
|
{
|
|
var command = message.Text!.Split(' ')[0].ToLowerInvariant();
|
|
|
|
var responseText = command switch
|
|
{
|
|
"/start" => "Welcome to Luna AI Assistant! Send me a message and I'll do my best to help.",
|
|
"/help" => "Luna AI Assistant\n\nCommands:\n/start - Start the bot\n/help - Show this help message\n\nJust send a message to chat with Luna!",
|
|
_ => null
|
|
};
|
|
|
|
if (responseText is null)
|
|
return;
|
|
|
|
var responseMessage = new ChannelMessage(
|
|
MessageId: Guid.NewGuid().ToString(),
|
|
ConversationId: message.Chat.Id.ToString(),
|
|
SenderId: "luna",
|
|
SenderName: "Luna",
|
|
Content: responseText,
|
|
Timestamp: DateTimeOffset.UtcNow);
|
|
|
|
await channel.SendMessageAsync(responseMessage, cancellationToken);
|
|
}
|
|
}
|