Add user identity system with channel pairing and web /link command
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.
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
using Luna.Channels.Abstractions;
|
||||
using Luna.Core.Abstractions;
|
||||
using Luna.Identity.Abstractions;
|
||||
using Luna.Shared;
|
||||
|
||||
namespace Luna.Channels.Cli;
|
||||
|
||||
public class CliChannel(string channelId, IChatHubContext hubContext) : IChannel
|
||||
public class CliChannel(
|
||||
string channelId,
|
||||
IChatHubContext hubContext,
|
||||
IUserIdentityService identityService)
|
||||
: IChannel
|
||||
{
|
||||
public string ChannelId { get; } = channelId;
|
||||
public string ChannelType => Abstractions.ChannelType.Cli;
|
||||
@@ -14,6 +19,26 @@ public class CliChannel(string channelId, IChatHubContext hubContext) : IChannel
|
||||
public event ChannelMessageReceivedEventHandler? MessageReceived;
|
||||
public event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
|
||||
|
||||
public async Task<bool> VerifyChannelLinkAsync()
|
||||
{
|
||||
var currentUser = identityService.GetCurrentUser();
|
||||
|
||||
if (identityService.IsChannelLinked(currentUser.UserId, ChannelType))
|
||||
return true;
|
||||
|
||||
var pairingCode = identityService.StartChannelLink(currentUser.UserId, ChannelType);
|
||||
await SendMessageAsync(new ChannelMessage(
|
||||
MessageId: Guid.NewGuid().ToString(),
|
||||
ConversationId: Guid.NewGuid().ToString(),
|
||||
SenderId: Guid.NewGuid().ToString(),
|
||||
SenderName: "luna",
|
||||
Content:
|
||||
$"{ChannelType} channel is not linked to current user.\nGo to web interface and execute command \"/link [pairing code]\".\nYou will have to provide following PairingCode: {pairingCode}",
|
||||
Timestamp: DateTimeOffset.UtcNow));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task SendStreamingMessageAsync(IAsyncEnumerable<ChatStreamUpdate> messageStream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await hubContext.SendStreamingResponseAsync(ChannelId, messageStream, cancellationToken);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<ProjectReference Include="..\Luna.Channels.Abstractions\Luna.Channels.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Luna.Configuration\Luna.Configuration.csproj" />
|
||||
<ProjectReference Include="..\Luna.Core.Abstractions\Luna.Core.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Luna.Identity.Abstractions\Luna.Identity.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\Luna.Shared\Luna.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Luna.Channels.Abstractions;
|
||||
using Luna.Configuration;
|
||||
using Luna.Identity.Abstractions;
|
||||
using Luna.Shared;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -11,6 +12,7 @@ namespace Luna.Channels.Telegram;
|
||||
|
||||
public class TelegramAdapter(
|
||||
IChannelManager channelManager,
|
||||
IUserIdentityService identityService,
|
||||
ITelegramBotClient botClient,
|
||||
TelegramOptions options,
|
||||
ILogger<TelegramAdapter> adapterLogger,
|
||||
@@ -60,6 +62,9 @@ public class TelegramAdapter(
|
||||
return;
|
||||
}
|
||||
|
||||
if (await channel.VerifyChannelLinkAsync() == false)
|
||||
return;
|
||||
|
||||
var channelMessage = new ChannelMessage(
|
||||
MessageId: message.Id.ToString(),
|
||||
ConversationId: chat.Id.ToString(),
|
||||
@@ -86,6 +91,7 @@ public class TelegramAdapter(
|
||||
var channel = new TelegramChannel(
|
||||
chatId.ToString(),
|
||||
botClient,
|
||||
identityService,
|
||||
channelLogger,
|
||||
options);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text;
|
||||
using Luna.Channels.Abstractions;
|
||||
using Luna.Configuration;
|
||||
using Luna.Identity.Abstractions;
|
||||
using Luna.Shared;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Telegram.Bot;
|
||||
@@ -11,6 +12,7 @@ namespace Luna.Channels.Telegram;
|
||||
public class TelegramChannel(
|
||||
string chatId,
|
||||
ITelegramBotClient botClient,
|
||||
IUserIdentityService identityService,
|
||||
ILogger<TelegramChannel> logger,
|
||||
TelegramOptions options) : IChannel
|
||||
{
|
||||
@@ -24,6 +26,26 @@ public class TelegramChannel(
|
||||
public event ChannelMessageReceivedEventHandler? MessageReceived;
|
||||
public event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
|
||||
|
||||
public async Task<bool> VerifyChannelLinkAsync()
|
||||
{
|
||||
var currentUser = identityService.GetCurrentUser();
|
||||
|
||||
if (identityService.IsChannelLinked(currentUser.UserId, ChannelType))
|
||||
return true;
|
||||
|
||||
var pairingCode = identityService.StartChannelLink(currentUser.UserId, ChannelType);
|
||||
await SendMessageAsync(new ChannelMessage(
|
||||
MessageId: Guid.NewGuid().ToString(),
|
||||
ConversationId: Guid.NewGuid().ToString(),
|
||||
SenderId: Guid.NewGuid().ToString(),
|
||||
SenderName: "luna",
|
||||
Content:
|
||||
$"{ChannelType} channel is not linked to current user.\nGo to web interface and execute command \"/link [pairing code]\".\nYou will have to provide following PairingCode: {pairingCode}",
|
||||
Timestamp: DateTimeOffset.UtcNow));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(ChannelMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Luna.Channels.Abstractions;
|
||||
using Luna.Core.Abstractions;
|
||||
using Luna.Identity.Abstractions;
|
||||
using Luna.Shared;
|
||||
|
||||
namespace Luna.Channels.Web;
|
||||
|
||||
public class WebChannel(string channelId, IChatHubContext hubContext) : IChannel
|
||||
public class WebChannel(string channelId, IChatHubContext hubContext, IUserIdentityService identityService) : IChannel
|
||||
{
|
||||
public string ChannelId { get; } = channelId;
|
||||
public string ChannelType => Abstractions.ChannelType.Web;
|
||||
@@ -13,6 +14,9 @@ public class WebChannel(string channelId, IChatHubContext hubContext) : IChannel
|
||||
|
||||
public event ChannelMessageReceivedEventHandler? MessageReceived;
|
||||
public event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
|
||||
|
||||
public Task<bool> VerifyChannelLinkAsync()
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public async Task SendStreamingMessageAsync(IAsyncEnumerable<ChatStreamUpdate> messageStream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -24,8 +28,14 @@ public class WebChannel(string channelId, IChatHubContext hubContext) : IChannel
|
||||
await hubContext.SendResponseAsync(ChannelId, message, cancellationToken);
|
||||
}
|
||||
|
||||
public void RaiseMessageReceived(object? sender, ChannelMessage message)
|
||||
public async Task RaiseMessageReceivedAsync(object? sender, ChannelMessage message)
|
||||
{
|
||||
if (message.Content.StartsWith("/"))
|
||||
{
|
||||
await HandleCommandAsync(message.Content);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageReceived?.Invoke(sender, new ChannelMessageReceivedEventArgs
|
||||
{
|
||||
Channel = this,
|
||||
@@ -33,6 +43,49 @@ public class WebChannel(string channelId, IChatHubContext hubContext) : IChannel
|
||||
IsHandled = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleCommandAsync(string command)
|
||||
{
|
||||
var parts = command.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var commandName = parts[0].ToLowerInvariant();
|
||||
|
||||
var responseText = commandName switch
|
||||
{
|
||||
"/link" => HandleLinkCommand(parts),
|
||||
_ => $"Unknown command: {commandName}"
|
||||
};
|
||||
|
||||
await SendMessageAsync(new ChannelMessage(
|
||||
MessageId: Guid.NewGuid().ToString(),
|
||||
ConversationId: Guid.NewGuid().ToString(),
|
||||
SenderId: "luna",
|
||||
SenderName: "Luna",
|
||||
Content: responseText,
|
||||
Timestamp: DateTimeOffset.UtcNow));
|
||||
}
|
||||
|
||||
private string HandleLinkCommand(string[] parts)
|
||||
{
|
||||
if (parts.Length < 3)
|
||||
return "Usage: /link <channel-type> <pairing-code>";
|
||||
|
||||
var channelType = parts[1].ToLowerInvariant();
|
||||
var code = parts[2].ToUpperInvariant();
|
||||
var currentUser = identityService.GetCurrentUser();
|
||||
|
||||
if (identityService.IsChannelLinked(currentUser.UserId, channelType))
|
||||
return $"Channel {channelType} is already linked to your account.";
|
||||
|
||||
try
|
||||
{
|
||||
identityService.CompleteChannelLink(currentUser.UserId, channelType, code);
|
||||
return $"Successfully linked {channelType} channel to your account.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return $"Failed to link channel: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
Reference in New Issue
Block a user