Compare commits

..
14 Commits
Author SHA1 Message Date
darmanandClaude Opus 5 9f1e62fdbf Remove hardcoded secrets; source credentials from configuration
The Telegram bot token was a const in AddChannels(), and the Mistral API
key was baked into the embedded Provider.Mistral.toml resource.

- Wire AddChannels() through the existing (previously unused)
  AddTelegramOptions(), which binds "Channels:Telegram" and validates on
  start. TelegramOptions is resolved from IOptions<> so TelegramAdapter
  and TelegramChannel are unchanged.
- Ship the embedded provider defaults with an empty ApiKey. Ollama's
  placeholder is now a non-secret literal.
- Re-apply AddEnvironmentVariables() last in AddLunaConfiguration() so
  env vars override the TOML sources; without this the embedded defaults
  would shadow injected credentials.

Credentials are now supplied via Providers__Mistral__ApiKey and
Channels__Telegram__BotToken, or via ~/.luna/providers/*.toml overrides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:59:05 +02:00
darman 2a07e07180 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.
2026-04-04 16:09:10 +02:00
darman 3138436965 Fixed some warnings 2026-04-04 05:01:51 +02:00
darman 6d6b2c1166 Added .env to .gitignore & removed redundant Luna.sln 2026-04-04 04:59:59 +02:00
darman 9929941748 Add project documentation and reference materials
Include Luna AI Assistant design docs covering channels, configuration,
core architecture, memory, scheduler, and skills. Add reference docs
from OpenClaw and ZeroClaw projects, plus Mistral and OpenAI API specs.
2026-04-04 04:14:06 +02:00
darman 969e4d6e37 Add React/TypeScript web interface with Chakra UI and SignalR integration
Implement web frontend using Vite + React + TypeScript with:
- Chakra UI component library for theming and styling
- SignalR bridge (lunaBridge) for real-time server communication
- Chat components (ChatPanel, ChatInput, ChatMessage)
- Layout components (AppLayout, Sidebar, StatusBar, SystemPanel)
- Custom useChat hook for conversation state management
- Dark/light theme support via ColorModeProvider
- Mock conversation data for development
2026-04-04 04:13:51 +02:00
darman 1d7ae158a4 Add CLI client application with SignalR connectivity
Implement standalone CLI client that connects to Luna.Core via SignalR
for interactive chat sessions. Includes ChatClientService for managing
the hub connection and streaming responses, with a hosted service
architecture using Microsoft.Extensions.Hosting.
2026-04-04 04:12:41 +02:00
darman f21d55d87d Add channel system unit and integration tests
Comprehensive NUnit test suite covering ChannelManager, CliChannel,
TelegramChannel, DI registration, and integration scenarios. Uses
Moq for mocking with test stubs for isolated unit testing.
2026-04-04 04:12:24 +02:00
darman 9a05bae4a9 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.
2026-04-04 04:12:11 +02:00
darman dd35e1dea6 Add Luna.Memory with SQLite-backed persistent memory store
Implement IMemoryStore interface and MemoryStore using EF Core with
SQLite for conversation and knowledge persistence. Includes DI
service registration extensions.
2026-04-04 04:11:08 +02:00
darman 2f5841a1b9 Add Luna.Core server with SignalR hub, session management, and tool system
Implement the core application server as an ASP.NET web host with:
- SignalR ChatHub for real-time client communication
- Session management with token estimation for context windowing
- Extensible tool system with attribute-based tool discovery
- Chat stream update builder for streaming AI responses
- App configuration with appsettings for development and production
2026-04-04 04:10:58 +02:00
darman 29eb3851c9 Add agent system with Core and Librarian agent implementations
Define IAgent interface and implement CoreAgent (main conversational
agent) and LibrarianAgent (knowledge retrieval agent). Includes DI
service collection extensions for agent registration.
2026-04-04 04:10:51 +02:00
darman 1c42c8c006 Add provider abstraction layer with Mistral and Ollama implementations
Define IProvider interface and implement concrete providers for Mistral
(with custom DTOs for chat completion API) and Ollama (via OllamaSharp).
Includes DI registration extensions using Microsoft.Extensions.AI.
2026-04-04 04:10:39 +02:00
darman a4ed65e452 Add Luna.Configuration with TOML-based agent and provider config
Implement configuration system with embedded TOML resources for agent
and provider definitions, custom IConfigurationSource/Provider for
agent and provider options, and DI extension methods. Includes config
models for agents, providers, sessions, and Telegram integration.
2026-04-04 04:09:57 +02:00
41 changed files with 879 additions and 730 deletions
+2
View File
@@ -8,3 +8,5 @@ riderModule.iml
.runsettings/
.sisyphus/
.env
+4 -1
View File
@@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
using Luna.Agents.Abstractions;
using Luna.Configuration;
using Luna.Providers.Abstractions;
@@ -38,7 +39,9 @@ public class LibrarianAgent(
.Where(m => m.Role != ChatRole.System);
}
public async IAsyncEnumerable<ChatResponseUpdate> ProcessStreamingAsync(IReadOnlyList<ChatMessage> messages, CancellationToken ct = default)
public async IAsyncEnumerable<ChatResponseUpdate> ProcessStreamingAsync(
IReadOnlyList<ChatMessage> messages,
[EnumeratorCancellation] CancellationToken ct = default)
{
var chatOptions = new ChatOptions
{
+2
View File
@@ -42,6 +42,8 @@ public interface IChannel : IDisposable
/// </summary>
event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
Task<bool> VerifyChannelLinkAsync();
/// <summary>
/// Send a message to the channel
/// </summary>
@@ -1,5 +1,4 @@
using Luna.Channels.Abstractions;
using Luna.Channels.Tests.Stubs;
using Luna.Core.Abstractions;
using Luna.Shared;
using Microsoft.Extensions.Logging;
@@ -95,7 +94,7 @@ public class ChannelManagerInterfaceTests
stub.RegisterChannel(mockChannel.Object);
var message = new ChannelMessage(
var _ = new ChannelMessage(
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
"sender-1",
@@ -103,11 +102,11 @@ public class ChannelManagerInterfaceTests
"Hello",
DateTimeOffset.UtcNow);
var eventArgs = new ChannelMessageReceivedEventArgs
{
Message = message,
Channel = mockChannel.Object
};
// var eventArgs = new ChannelMessageReceivedEventArgs
// {
// Message = message,
// Channel = mockChannel.Object
// };
Assert.That(capturedArgs, Is.Not.Null);
}
+110 -111
View File
@@ -1,111 +1,110 @@
using Luna.Channels.Abstractions;
using Luna.Channels.Cli;
using Luna.Channels.Tests.Stubs;
using Luna.Core.Abstractions;
using Luna.Shared;
using Moq;
namespace Luna.Channels.Tests;
[TestFixture]
[Category("Unit")]
public class CliChannelTests
{
[Test]
public void ChannelType_Returns_Cli()
{
var hubMock = new Mock<IChatHubContext>();
var channel = new CliChannel("test-1", hubMock.Object);
Assert.That(channel.ChannelType, Is.EqualTo("cli"));
}
[Test]
public void ChannelId_IsUnique_PerInstance()
{
var hubMock = new Mock<IChatHubContext>();
var channel1 = new CliChannel("test-1", hubMock.Object);
var channel2 = new CliChannel("test-2", hubMock.Object);
Assert.That(channel1.ChannelId, Is.Not.EqualTo(channel2.ChannelId));
}
[Test]
public void DisplayName_Returns_HumanReadable()
{
var hubMock = new Mock<IChatHubContext>();
var channel = new CliChannel("test-1", hubMock.Object);
Assert.That(channel.DisplayName, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task SendMessageAsync_Sends_ChannelMessage()
{
var hubMock = new Mock<IChatHubContext>();
var channel = new CliChannel("test-1", hubMock.Object);
var message = new ChannelMessage(
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
"cli-user-1",
"CLI User",
"Hello from CLI",
DateTimeOffset.UtcNow);
await channel.SendMessageAsync(message);
}
[Test]
public async Task SendStreamingMessageAsync_Consumes_Stream()
{
var hubMock = new Mock<IChatHubContext>();
var channel = new CliChannel("test-1", hubMock.Object);
async IAsyncEnumerable<ChatStreamUpdate> TestStream()
{
yield return new ChatStreamUpdate(ChatStreamUpdateType.Text, "Hello", "assistant", false);
yield return new ChatStreamUpdate(ChatStreamUpdateType.Text, " world", "assistant", false);
yield return new ChatStreamUpdate(ChatStreamUpdateType.Complete, null, "assistant", true);
await Task.CompletedTask;
}
await channel.SendStreamingMessageAsync(TestStream());
}
[Test]
public void Dispose_CleansUp()
{
var hubMock = new Mock<IChatHubContext>();
var channel = new CliChannel("test-1", hubMock.Object);
Assert.That(() => channel.Dispose(), Throws.Nothing);
}
[Test]
public void MessageReceived_Event_Fires_On_RaiseMessageReceived()
{
var hubMock = new Mock<IChatHubContext>();
var channel = new CliChannel("test-1", hubMock.Object);
ChannelMessageReceivedEventArgs? capturedArgs = null;
channel.MessageReceived += (_, args) =>
{
capturedArgs = args;
return Task.CompletedTask;
};
var message = new ChannelMessage(
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
"cli-user-1",
"CLI User",
"Hello from CLI",
DateTimeOffset.UtcNow);
channel.RaiseMessageReceived(null, message);
Assert.That(capturedArgs, Is.Not.Null,
"MessageReceived event must fire when RaiseMessageReceived is called.");
Assert.That(capturedArgs!.Message.Content, Is.EqualTo("Hello from CLI"));
}
}
// using Luna.Channels.Abstractions;
// using Luna.Channels.Cli;
// using Luna.Core.Abstractions;
// using Luna.Shared;
// using Moq;
//
// namespace Luna.Channels.Tests;
//
// [TestFixture]
// [Category("Unit")]
// public class CliChannelTests
// {
// [Test]
// public void ChannelType_Returns_Cli()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel = new CliChannel("test-1", hubMock.Object);
//
// Assert.That(channel.ChannelType, Is.EqualTo("cli"));
// }
//
// [Test]
// public void ChannelId_IsUnique_PerInstance()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel1 = new CliChannel("test-1", hubMock.Object);
// var channel2 = new CliChannel("test-2", hubMock.Object);
//
// Assert.That(channel1.ChannelId, Is.Not.EqualTo(channel2.ChannelId));
// }
//
// [Test]
// public void DisplayName_Returns_HumanReadable()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel = new CliChannel("test-1", hubMock.Object);
//
// Assert.That(channel.DisplayName, Is.Not.Null.And.Not.Empty);
// }
//
// [Test]
// public async Task SendMessageAsync_Sends_ChannelMessage()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel = new CliChannel("test-1", hubMock.Object);
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(),
// Guid.NewGuid().ToString(),
// "cli-user-1",
// "CLI User",
// "Hello from CLI",
// DateTimeOffset.UtcNow);
//
// await channel.SendMessageAsync(message);
// }
//
// [Test]
// public async Task SendStreamingMessageAsync_Consumes_Stream()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel = new CliChannel("test-1", hubMock.Object);
//
// async IAsyncEnumerable<ChatStreamUpdate> TestStream()
// {
// yield return new ChatStreamUpdate(ChatStreamUpdateType.Text, "Hello", "assistant", false);
// yield return new ChatStreamUpdate(ChatStreamUpdateType.Text, " world", "assistant", false);
// yield return new ChatStreamUpdate(ChatStreamUpdateType.Complete, null, "assistant", true);
// await Task.CompletedTask;
// }
//
// await channel.SendStreamingMessageAsync(TestStream());
// }
//
// [Test]
// public void Dispose_CleansUp()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel = new CliChannel("test-1", hubMock.Object);
//
// Assert.That(() => channel.Dispose(), Throws.Nothing);
// }
//
// [Test]
// public void MessageReceived_Event_Fires_On_RaiseMessageReceived()
// {
// var hubMock = new Mock<IChatHubContext>();
// var channel = new CliChannel("test-1", hubMock.Object);
// ChannelMessageReceivedEventArgs? capturedArgs = null;
//
// channel.MessageReceived += (_, args) =>
// {
// capturedArgs = args;
// return Task.CompletedTask;
// };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(),
// Guid.NewGuid().ToString(),
// "cli-user-1",
// "CLI User",
// "Hello from CLI",
// DateTimeOffset.UtcNow);
//
// channel.RaiseMessageReceived(null, message);
//
// Assert.That(capturedArgs, Is.Not.Null,
// "MessageReceived event must fire when RaiseMessageReceived is called.");
// Assert.That(capturedArgs!.Message.Content, Is.EqualTo("Hello from CLI"));
// }
// }
+20 -20
View File
@@ -10,21 +10,21 @@ namespace Luna.Channels.Tests;
[Category("Unit")]
public class DependencyInjectionTests
{
private ServiceCollection _services = null!;
private ServiceCollection services = null!;
[SetUp]
public void SetUp()
{
_services = new ServiceCollection();
services = [];
}
[Test]
public void AddChannels_RegistersIChannelManager_AsSingleton()
{
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
// var provider = services.BuildServiceProvider();
var descriptor = _services.FirstOrDefault(d => d.ServiceType == typeof(IChannelManager));
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IChannelManager));
Assert.That(descriptor, Is.Not.Null);
Assert.That(descriptor!.Lifetime, Is.EqualTo(ServiceLifetime.Singleton));
@@ -33,8 +33,8 @@ public class DependencyInjectionTests
[Test]
public void AddChannels_IChannelManager_IsResolvableFromContainer()
{
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
var provider = services.BuildServiceProvider();
var manager = provider.GetService<IChannelManager>();
@@ -44,10 +44,10 @@ public class DependencyInjectionTests
[Test]
public void AddChannels_TelegramOptions_IsRegistered()
{
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
// var provider = services.BuildServiceProvider();
var descriptor = _services.FirstOrDefault(d =>
var descriptor = services.FirstOrDefault(d =>
d.ServiceType.IsGenericType &&
d.ServiceType.GetGenericTypeDefinition() == typeof(IOptions<>) &&
d.ServiceType.GetGenericArguments()[0] == typeof(TelegramOptions));
@@ -59,8 +59,8 @@ public class DependencyInjectionTests
[Test]
public void AddChannels_CliChannel_IsRegistered()
{
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
var provider = services.BuildServiceProvider();
var channels = provider.GetServices<IChannel>().ToList();
var channelTypes = channels.Select(c => c.ChannelType).ToList();
@@ -72,8 +72,8 @@ public class DependencyInjectionTests
[Test]
public void AddChannels_TelegramChannel_IsRegistered()
{
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
var provider = services.BuildServiceProvider();
var channels = provider.GetServices<IChannel>().ToList();
var channelTypes = channels.Select(c => c.ChannelType).ToList();
@@ -85,11 +85,11 @@ public class DependencyInjectionTests
[Test]
public void AddChannels_CalledTwice_DoesNotDuplicateSingletonRegistration()
{
_services.AddChannels();
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
services.AddChannels();
//var provider = services.BuildServiceProvider();
var descriptors = _services.Where(d => d.ServiceType == typeof(IChannelManager)).ToList();
var descriptors = services.Where(d => d.ServiceType == typeof(IChannelManager)).ToList();
Assert.That(descriptors, Has.Count.EqualTo(1),
"Calling AddChannels() twice must not register IChannelManager twice");
@@ -98,8 +98,8 @@ public class DependencyInjectionTests
[Test]
public void AddChannels_SameProviderInstance_ReturnsSameIChannelManager()
{
_services.AddChannels();
var provider = _services.BuildServiceProvider();
services.AddChannels();
var provider = services.BuildServiceProvider();
var first = provider.GetRequiredService<IChannelManager>();
var second = provider.GetRequiredService<IChannelManager>();
+41 -41
View File
@@ -10,21 +10,21 @@ namespace Luna.Channels.Tests;
[Category("Unit")]
public class IntegrationTests
{
private Mock<ISessionManager> _sessionManagerMock = null!;
private Mock<IChannel> _channelMock = null!;
private ChannelManagerStub _manager = null!;
private Mock<ISessionManager> sessionManagerMock = null!;
private Mock<IChannel> channelMock = null!;
private ChannelManagerStub manager = null!;
[SetUp]
public void SetUp()
{
_sessionManagerMock = new Mock<ISessionManager>();
_channelMock = new Mock<IChannel>();
_channelMock.Setup(c => c.ChannelId).Returns("test-ch-1");
_channelMock.Setup(c => c.ChannelType).Returns("test");
_channelMock.Setup(c => c.SendStreamingMessageAsync(
sessionManagerMock = new Mock<ISessionManager>();
channelMock = new Mock<IChannel>();
channelMock.Setup(c => c.ChannelId).Returns("test-ch-1");
channelMock.Setup(c => c.ChannelType).Returns("test");
channelMock.Setup(c => c.SendStreamingMessageAsync(
It.IsAny<IAsyncEnumerable<ChatStreamUpdate>>(),
It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
_manager = new ChannelManagerStub(_sessionManagerMock.Object);
manager = new ChannelManagerStub(sessionManagerMock.Object);
}
private static async IAsyncEnumerable<ChatStreamUpdate> BuildStream(params ChatStreamUpdate[] updates)
@@ -42,24 +42,24 @@ public class IntegrationTests
var message = new ChannelMessage(
Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "user-1", "User One", "Hello Luna!", DateTimeOffset.UtcNow);
_sessionManagerMock
sessionManagerMock
.Setup(s => s.RouteMessagesAsync(
message.Content,
message.ConversationId.ToString(),
message.ConversationId,
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Returns(BuildStream(new ChatStreamUpdate(ChatStreamUpdateType.Text, "Hi!", null, false)));
_manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = _channelMock.Object };
_channelMock.Raise(c => c.MessageReceived += null, _channelMock.Object, args);
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = channelMock.Object };
channelMock.Raise(c => c.MessageReceived += null, channelMock.Object, args);
await Task.Delay(50);
_sessionManagerMock.Verify(s => s.RouteMessagesAsync(
sessionManagerMock.Verify(s => s.RouteMessagesAsync(
message.Content,
message.ConversationId.ToString(),
message.ConversationId,
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Once);
}
@@ -75,19 +75,19 @@ public class IntegrationTests
new ChatStreamUpdate(ChatStreamUpdateType.Text, "noon.", null, false),
new ChatStreamUpdate(ChatStreamUpdateType.Complete, null, null, true));
_sessionManagerMock
sessionManagerMock
.Setup(s => s.RouteMessagesAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(responseStream);
_manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = _channelMock.Object };
_channelMock.Raise(c => c.MessageReceived += null, _channelMock.Object, args);
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = channelMock.Object };
channelMock.Raise(c => c.MessageReceived += null, channelMock.Object, args);
await Task.Delay(50);
_channelMock.Verify(c => c.SendStreamingMessageAsync(
channelMock.Verify(c => c.SendStreamingMessageAsync(
It.IsAny<IAsyncEnumerable<ChatStreamUpdate>>(),
It.IsAny<CancellationToken>()), Times.Once);
}
@@ -98,14 +98,14 @@ public class IntegrationTests
var message = new ChannelMessage(
Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "user-3", "User Three", "/start", DateTimeOffset.UtcNow);
_manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = _channelMock.Object, IsHandled = true };
_channelMock.Raise(c => c.MessageReceived += null, _channelMock.Object, args);
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = channelMock.Object, IsHandled = true };
channelMock.Raise(c => c.MessageReceived += null, channelMock.Object, args);
await Task.Delay(50);
_sessionManagerMock.Verify(s => s.RouteMessagesAsync(
sessionManagerMock.Verify(s => s.RouteMessagesAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),
Times.Never);
}
@@ -120,25 +120,25 @@ public class IntegrationTests
It.IsAny<IAsyncEnumerable<ChatStreamUpdate>>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
_sessionManagerMock
sessionManagerMock
.Setup(s => s.RouteMessagesAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(BuildStream(new ChatStreamUpdate(ChatStreamUpdateType.Text, "ok", null, true)));
_manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
_manager.RegisterChannel(channel2Mock.Object);
manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
manager.RegisterChannel(channel2Mock.Object);
var msg1 = new ChannelMessage(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "u1", "U1", "Msg1", DateTimeOffset.UtcNow);
var msg2 = new ChannelMessage(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "u2", "U2", "Msg2", DateTimeOffset.UtcNow);
_channelMock.Raise(c => c.MessageReceived += null, _channelMock.Object,
new ChannelMessageReceivedEventArgs { Message = msg1, Channel = _channelMock.Object });
channelMock.Raise(c => c.MessageReceived += null, channelMock.Object,
new ChannelMessageReceivedEventArgs { Message = msg1, Channel = channelMock.Object });
channel2Mock.Raise(c => c.MessageReceived += null, channel2Mock.Object,
new ChannelMessageReceivedEventArgs { Message = msg2, Channel = channel2Mock.Object });
await Task.Delay(100);
_sessionManagerMock.Verify(s => s.RouteMessagesAsync(
sessionManagerMock.Verify(s => s.RouteMessagesAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),
Times.Exactly(2));
}
@@ -150,21 +150,21 @@ public class IntegrationTests
var message = new ChannelMessage(
Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "user-4", "User Four", "Cancel me", DateTimeOffset.UtcNow);
_sessionManagerMock
sessionManagerMock
.Setup(s => s.RouteMessagesAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), cts.Token))
.Returns(BuildStream(new ChatStreamUpdate(ChatStreamUpdateType.Text, "x", null, true)));
_manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = _channelMock.Object };
await _channelMock.RaiseAsync(c => c.MessageReceived += null, _channelMock.Object, args);
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = channelMock.Object };
await channelMock.RaiseAsync(c => c.MessageReceived += null, channelMock.Object, args);
await Task.Delay(50, cts.Token);
_sessionManagerMock.Verify(s => s.RouteMessagesAsync(
sessionManagerMock.Verify(s => s.RouteMessagesAsync(
message.Content,
message.ConversationId.ToString(),
message.ConversationId,
It.IsAny<string>(),
It.IsAny<CancellationToken>()), Times.Once);
}
@@ -175,17 +175,17 @@ public class IntegrationTests
var message = new ChannelMessage(
Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "user-5", "User Five", "Boom", DateTimeOffset.UtcNow);
_sessionManagerMock
sessionManagerMock
.Setup(s => s.RouteMessagesAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Throws(new InvalidOperationException("Simulated routing failure"));
_manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
manager.RegisterChannel(new CliChannelStub { ChannelId = "test-1" });
Assert.DoesNotThrowAsync(async () =>
{
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = _channelMock.Object };
await _channelMock.RaiseAsync(c => c.MessageReceived += null, _channelMock.Object, args);
var args = new ChannelMessageReceivedEventArgs { Message = message, Channel = channelMock.Object };
await channelMock.RaiseAsync(c => c.MessageReceived += null, channelMock.Object, args);
await Task.Delay(50);
});
}
@@ -14,7 +14,7 @@ public class ChannelManagerStub : IChannelManager
public void UnregisterChannel(string channelId) => throw new NotImplementedException();
public IChannel? GetChannel(string channelId) => throw new NotImplementedException();
public IChannel GetChannel(string channelId) => throw new NotImplementedException();
public IReadOnlyList<IChannel> GetAllChannels() => throw new NotImplementedException();
}
@@ -11,6 +11,8 @@ public class CliChannelStub : IChannel
public bool IsConnected => throw new NotImplementedException();
public event ChannelMessageReceivedEventHandler? MessageReceived;
public event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
public Task<bool> VerifyChannelLinkAsync() => throw new NotImplementedException();
public Task SendMessageAsync(ChannelMessage message, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public Task SendStreamingMessageAsync(IAsyncEnumerable<ChatStreamUpdate> messageStream, CancellationToken cancellationToken = default) => throw new NotImplementedException();
public void RaiseMessageReceived(object? sender, ChannelMessage message) => throw new NotImplementedException();
@@ -13,6 +13,11 @@ public class TelegramChannelStub : IChannel
public event ChannelMessageReceivedEventHandler? MessageReceived;
public event ChannelConnectionChangedEventHandler? ConnectionStateChanged;
public Task<bool> VerifyChannelLinkAsync()
{
throw new NotImplementedException();
}
public Task SendMessageAsync(ChannelMessage message, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
+345 -345
View File
@@ -1,345 +1,345 @@
using Luna.Channels.Abstractions;
using Luna.Channels.Telegram;
using Luna.Configuration;
using Luna.Shared;
using Microsoft.Extensions.Logging;
using Moq;
using Telegram.Bot;
using Telegram.Bot.Requests;
using Telegram.Bot.Types;
namespace Luna.Channels.Tests;
[TestFixture]
[Category("Unit")]
public class TelegramChannelTests
{
private Mock<ITelegramBotClient> _mockBotClient = null!;
private Mock<ILogger<TelegramChannel>> _mockLogger = null!;
private TelegramOptions _options = null!;
private TelegramChannel _channel = null!;
[SetUp]
public void SetUp()
{
_mockBotClient = new Mock<ITelegramBotClient>();
_mockLogger = new Mock<ILogger<TelegramChannel>>();
_options = new TelegramOptions { BotToken = "test-bot-token:ABCDEF" };
_channel = new TelegramChannel("100", _mockBotClient.Object, _mockLogger.Object, _options);
}
[TearDown]
public void TearDown()
{
_channel.Dispose();
}
[Test]
public void ChannelType_Returns_Telegram()
{
Assert.That(_channel.ChannelType, Is.EqualTo("telegram"));
}
[Test]
public void ChannelId_IsSet_From_Constructor()
{
Assert.That(_channel.ChannelId, Is.EqualTo("100"));
}
[Test]
public void ChannelId_IsUnique_Per_Instance()
{
var channel1 = new TelegramChannel("111", _mockBotClient.Object, _mockLogger.Object, _options);
var channel2 = new TelegramChannel("222", _mockBotClient.Object, _mockLogger.Object, _options);
Assert.That(channel1.ChannelId, Is.Not.EqualTo(channel2.ChannelId));
channel1.Dispose();
channel2.Dispose();
}
[Test]
public void DisplayName_Contains_Telegram()
{
Assert.That(_channel.DisplayName, Does.Contain("Telegram"));
}
[Test]
public void IsConnected_Is_True_Initially()
{
Assert.That(_channel.IsConnected, Is.True);
}
[Test]
public void IsConnected_Is_False_After_Dispose()
{
_channel.Dispose();
Assert.That(_channel.IsConnected, Is.False);
}
[Test]
public async Task SendMessageAsync_Calls_BotClient_SendMessage()
{
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 1, Chat = new Chat { Id = 100 } });
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "100", "42", "Alice", "Hello!", DateTimeOffset.UtcNow);
await _channel.SendMessageAsync(message);
_mockBotClient.Verify(
c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public async Task SendMessageAsync_Handles_Network_Error_Without_Crash()
{
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new HttpRequestException("Network error"));
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "100", "42", "Alice", "Hello!", DateTimeOffset.UtcNow);
Assert.DoesNotThrowAsync(async () => await _channel.SendMessageAsync(message));
}
[Test]
public void RaiseMessageReceived_Fires_Event_With_Correct_Mapping()
{
ChannelMessageReceivedEventArgs? captured = null;
_channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "999", "77", "Bob", "Hello Luna", DateTimeOffset.UtcNow);
_channel.RaiseMessageReceived(message);
Assert.That(captured, Is.Not.Null);
Assert.That(captured!.Message.ConversationId, Is.EqualTo("999"));
Assert.That(captured.Message.SenderId, Is.EqualTo("77"));
Assert.That(captured.Message.SenderName, Is.EqualTo("Bob"));
Assert.That(captured.Message.Content, Is.EqualTo("Hello Luna"));
Assert.That(captured.Channel, Is.SameAs(_channel));
}
[Test]
public void RaiseMessageReceived_Maps_SenderName_And_Timestamp()
{
var sentAt = new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero);
ChannelMessageReceivedEventArgs? captured = null;
_channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "123", "55", "Carol", "Hi", sentAt);
_channel.RaiseMessageReceived(message);
Assert.That(captured, Is.Not.Null);
Assert.That(captured!.Message.SenderName, Is.EqualTo("Carol"));
Assert.That(captured.Message.Timestamp, Is.EqualTo(sentAt));
}
[Test]
public async Task SendStreamingMessageAsync_Sends_Initial_Then_Edits_In_Place()
{
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 10, Chat = new Chat { Id = 100 } });
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 10, Chat = new Chat { Id = 100 } });
var updates = new List<ChatStreamUpdate>
{
new(ChatStreamUpdateType.Text, "Hello ", null, false),
new(ChatStreamUpdateType.Text, "World", null, false),
new(ChatStreamUpdateType.Complete, null, null, true),
};
await _channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
// First chunk sends a message, second chunk edits it
_mockBotClient.Verify(
c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Once);
_mockBotClient.Verify(
c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()),
Times.AtLeastOnce);
}
[Test]
public async Task SendStreamingMessageAsync_Handles_4096_Char_Limit_By_Chunking()
{
var longContent = new string('A', 4097);
var updates = new List<ChatStreamUpdate>
{
new(ChatStreamUpdateType.Text, longContent, null, false),
new(ChatStreamUpdateType.Complete, null, null, true),
};
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 20, Chat = new Chat { Id = 100 } });
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 20, Chat = new Chat { Id = 100 } });
await _channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
// Content exceeds 4096 — needs at least 2 SendMessage calls (initial + overflow)
_mockBotClient.Verify(
c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
Times.AtLeast(2));
}
[Test]
public async Task SendStreamingMessageAsync_Handles_IsComplete_True_With_Final_Edit()
{
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 30, Chat = new Chat { Id = 100 } });
_mockBotClient
.Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Message { Id = 30, Chat = new Chat { Id = 100 } });
var updates = new List<ChatStreamUpdate>
{
new(ChatStreamUpdateType.Text, "Final answer", null, false),
new(ChatStreamUpdateType.Complete, null, null, true),
};
await _channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
// Initial message sent
_mockBotClient.Verify(
c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
Times.Once);
}
[Test]
public void AllowedUserIds_Non_Empty_Blocks_Unauthorized_User()
{
var restrictedOptions = new TelegramOptions
{
BotToken = "test-token",
AllowedUserIds = ["111", "222"]
};
var restrictedChannel = new TelegramChannel(
"500", _mockBotClient.Object, _mockLogger.Object, restrictedOptions);
ChannelMessageReceivedEventArgs? captured = null;
restrictedChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "500", "999", "Hacker", "attack", DateTimeOffset.UtcNow);
restrictedChannel.RaiseMessageReceived(message);
Assert.That(captured, Is.Null, "Message from unauthorized user should be silently ignored");
restrictedChannel.Dispose();
}
[Test]
public void AllowedUserIds_Non_Empty_Allows_Authorized_User()
{
var restrictedOptions = new TelegramOptions
{
BotToken = "test-token",
AllowedUserIds = ["111", "222"]
};
var restrictedChannel = new TelegramChannel(
"500", _mockBotClient.Object, _mockLogger.Object, restrictedOptions);
ChannelMessageReceivedEventArgs? captured = null;
restrictedChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "500", "111", "TrustedUser", "hello", DateTimeOffset.UtcNow);
restrictedChannel.RaiseMessageReceived(message);
Assert.That(captured, Is.Not.Null, "Message from authorized user should be accepted");
restrictedChannel.Dispose();
}
[Test]
public void AllowedUserIds_Empty_Accepts_All_Users()
{
var openOptions = new TelegramOptions
{
BotToken = "test-token",
AllowedUserIds = []
};
var openChannel = new TelegramChannel(
"600", _mockBotClient.Object, _mockLogger.Object, openOptions);
ChannelMessageReceivedEventArgs? captured = null;
openChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "600", "12345", "Anyone", "hello", DateTimeOffset.UtcNow);
openChannel.RaiseMessageReceived(message);
Assert.That(captured, Is.Not.Null, "Message should be accepted when AllowedUserIds is empty");
openChannel.Dispose();
}
[Test]
public void Malformed_Message_With_Null_Text_Is_Skipped()
{
ChannelMessageReceivedEventArgs? captured = null;
_channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "700", "88", "Ghost", null!, DateTimeOffset.UtcNow);
_channel.RaiseMessageReceived(message);
Assert.That(captured, Is.Null, "Message with null text should be silently skipped");
}
[Test]
public void Malformed_Message_With_Empty_Text_Is_Skipped()
{
ChannelMessageReceivedEventArgs? captured = null;
_channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
var message = new ChannelMessage(
Guid.NewGuid().ToString(), "700", "88", "Ghost", "", DateTimeOffset.UtcNow);
_channel.RaiseMessageReceived(message);
Assert.That(captured, Is.Null, "Message with empty text should be silently skipped");
}
[Test]
public void Dispose_Fires_ConnectionStateChanged()
{
ChannelConnectionEventArgs? captured = null;
_channel.ConnectionStateChanged += (_, args) =>
{
captured = args;
return Task.CompletedTask;
};
_channel.Dispose();
Assert.That(captured, Is.Not.Null);
Assert.That(captured!.IsConnected, Is.False);
Assert.That(captured.Channel, Is.SameAs(_channel));
}
}
// using Luna.Channels.Abstractions;
// using Luna.Channels.Telegram;
// using Luna.Configuration;
// using Luna.Shared;
// using Microsoft.Extensions.Logging;
// using Moq;
// using Telegram.Bot;
// using Telegram.Bot.Requests;
// using Telegram.Bot.Types;
//
// namespace Luna.Channels.Tests;
//
// [TestFixture]
// [Category("Unit")]
// public class TelegramChannelTests
// {
// private Mock<ITelegramBotClient> mockBotClient = null!;
// private Mock<ILogger<TelegramChannel>> mockLogger = null!;
// private TelegramOptions options = null!;
// private TelegramChannel channel = null!;
//
// [SetUp]
// public void SetUp()
// {
// mockBotClient = new Mock<ITelegramBotClient>();
// mockLogger = new Mock<ILogger<TelegramChannel>>();
// options = new TelegramOptions { BotToken = "test-bot-token:ABCDEF" };
// channel = new TelegramChannel("100", mockBotClient.Object, mockLogger.Object, options);
// }
//
// [TearDown]
// public void TearDown()
// {
// channel.Dispose();
// }
//
// [Test]
// public void ChannelType_Returns_Telegram()
// {
// Assert.That(channel.ChannelType, Is.EqualTo("telegram"));
// }
//
// [Test]
// public void ChannelId_IsSet_From_Constructor()
// {
// Assert.That(channel.ChannelId, Is.EqualTo("100"));
// }
//
// [Test]
// public void ChannelId_IsUnique_Per_Instance()
// {
// var channel1 = new TelegramChannel("111", mockBotClient.Object, mockLogger.Object, options);
// var channel2 = new TelegramChannel("222", mockBotClient.Object, mockLogger.Object, options);
//
// Assert.That(channel1.ChannelId, Is.Not.EqualTo(channel2.ChannelId));
//
// channel1.Dispose();
// channel2.Dispose();
// }
//
// [Test]
// public void DisplayName_Contains_Telegram()
// {
// Assert.That(channel.DisplayName, Does.Contain("Telegram"));
// }
//
// [Test]
// public void IsConnected_Is_True_Initially()
// {
// Assert.That(channel.IsConnected, Is.True);
// }
//
// [Test]
// public void IsConnected_Is_False_After_Dispose()
// {
// channel.Dispose();
//
// Assert.That(channel.IsConnected, Is.False);
// }
//
// [Test]
// public async Task SendMessageAsync_Calls_BotClient_SendMessage()
// {
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 1, Chat = new Chat { Id = 100 } });
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "100", "42", "Alice", "Hello!", DateTimeOffset.UtcNow);
//
// await channel.SendMessageAsync(message);
//
// mockBotClient.Verify(
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
// Times.Once);
// }
//
// [Test]
// public async Task SendMessageAsync_Handles_Network_Error_Without_Crash()
// {
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
// .ThrowsAsync(new HttpRequestException("Network error"));
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "100", "42", "Alice", "Hello!", DateTimeOffset.UtcNow);
//
// Assert.DoesNotThrowAsync(async () => await channel.SendMessageAsync(message));
// }
//
// [Test]
// public void RaiseMessageReceived_Fires_Event_With_Correct_Mapping()
// {
// ChannelMessageReceivedEventArgs? captured = null;
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "999", "77", "Bob", "Hello Luna", DateTimeOffset.UtcNow);
//
// channel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Not.Null);
// Assert.That(captured!.Message.ConversationId, Is.EqualTo("999"));
// Assert.That(captured.Message.SenderId, Is.EqualTo("77"));
// Assert.That(captured.Message.SenderName, Is.EqualTo("Bob"));
// Assert.That(captured.Message.Content, Is.EqualTo("Hello Luna"));
// Assert.That(captured.Channel, Is.SameAs(channel));
// }
//
// [Test]
// public void RaiseMessageReceived_Maps_SenderName_And_Timestamp()
// {
// var sentAt = new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero);
//
// ChannelMessageReceivedEventArgs? captured = null;
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "123", "55", "Carol", "Hi", sentAt);
//
// channel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Not.Null);
// Assert.That(captured!.Message.SenderName, Is.EqualTo("Carol"));
// Assert.That(captured.Message.Timestamp, Is.EqualTo(sentAt));
// }
//
// [Test]
// public async Task SendStreamingMessageAsync_Sends_Initial_Then_Edits_In_Place()
// {
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 10, Chat = new Chat { Id = 100 } });
//
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 10, Chat = new Chat { Id = 100 } });
//
// var updates = new List<ChatStreamUpdate>
// {
// new(ChatStreamUpdateType.Text, "Hello ", null, false),
// new(ChatStreamUpdateType.Text, "World", null, false),
// new(ChatStreamUpdateType.Complete, null, null, true),
// };
//
// await channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
//
// // First chunk sends a message, second chunk edits it
// mockBotClient.Verify(
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
// Times.Once);
//
// mockBotClient.Verify(
// c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()),
// Times.AtLeastOnce);
// }
//
// [Test]
// public async Task SendStreamingMessageAsync_Handles_4096_Char_Limit_By_Chunking()
// {
// var longContent = new string('A', 4097);
// var updates = new List<ChatStreamUpdate>
// {
// new(ChatStreamUpdateType.Text, longContent, null, false),
// new(ChatStreamUpdateType.Complete, null, null, true),
// };
//
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 20, Chat = new Chat { Id = 100 } });
//
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 20, Chat = new Chat { Id = 100 } });
//
// await channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
//
// // Content exceeds 4096 — needs at least 2 SendMessage calls (initial + overflow)
// mockBotClient.Verify(
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
// Times.AtLeast(2));
// }
//
// [Test]
// public async Task SendStreamingMessageAsync_Handles_IsComplete_True_With_Final_Edit()
// {
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 30, Chat = new Chat { Id = 100 } });
//
// mockBotClient
// .Setup(c => c.SendRequest(It.IsAny<EditMessageTextRequest>(), It.IsAny<CancellationToken>()))
// .ReturnsAsync(new Message { Id = 30, Chat = new Chat { Id = 100 } });
//
// var updates = new List<ChatStreamUpdate>
// {
// new(ChatStreamUpdateType.Text, "Final answer", null, false),
// new(ChatStreamUpdateType.Complete, null, null, true),
// };
//
// await channel.SendStreamingMessageAsync(updates.ToAsyncEnumerable());
//
// // Initial message sent
// mockBotClient.Verify(
// c => c.SendRequest(It.IsAny<SendMessageRequest>(), It.IsAny<CancellationToken>()),
// Times.Once);
// }
//
// [Test]
// public void AllowedUserIds_Non_Empty_Blocks_Unauthorized_User()
// {
// var restrictedOptions = new TelegramOptions
// {
// BotToken = "test-token",
// AllowedUserIds = ["111", "222"]
// };
// var restrictedChannel = new TelegramChannel(
// "500", mockBotClient.Object, mockLogger.Object, restrictedOptions);
//
// ChannelMessageReceivedEventArgs? captured = null;
// restrictedChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "500", "999", "Hacker", "attack", DateTimeOffset.UtcNow);
//
// restrictedChannel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Null, "Message from unauthorized user should be silently ignored");
//
// restrictedChannel.Dispose();
// }
//
// [Test]
// public void AllowedUserIds_Non_Empty_Allows_Authorized_User()
// {
// var restrictedOptions = new TelegramOptions
// {
// BotToken = "test-token",
// AllowedUserIds = ["111", "222"]
// };
// var restrictedChannel = new TelegramChannel(
// "500", mockBotClient.Object, mockLogger.Object, restrictedOptions);
//
// ChannelMessageReceivedEventArgs? captured = null;
// restrictedChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "500", "111", "TrustedUser", "hello", DateTimeOffset.UtcNow);
//
// restrictedChannel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Not.Null, "Message from authorized user should be accepted");
//
// restrictedChannel.Dispose();
// }
//
// [Test]
// public void AllowedUserIds_Empty_Accepts_All_Users()
// {
// var openOptions = new TelegramOptions
// {
// BotToken = "test-token",
// AllowedUserIds = []
// };
// var openChannel = new TelegramChannel(
// "600", mockBotClient.Object, mockLogger.Object, openOptions);
//
// ChannelMessageReceivedEventArgs? captured = null;
// openChannel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "600", "12345", "Anyone", "hello", DateTimeOffset.UtcNow);
//
// openChannel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Not.Null, "Message should be accepted when AllowedUserIds is empty");
//
// openChannel.Dispose();
// }
//
// [Test]
// public void Malformed_Message_With_Null_Text_Is_Skipped()
// {
// ChannelMessageReceivedEventArgs? captured = null;
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "700", "88", "Ghost", null!, DateTimeOffset.UtcNow);
//
// channel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Null, "Message with null text should be silently skipped");
// }
//
// [Test]
// public void Malformed_Message_With_Empty_Text_Is_Skipped()
// {
// ChannelMessageReceivedEventArgs? captured = null;
// channel.MessageReceived += (_, args) => { captured = args; return Task.CompletedTask; };
//
// var message = new ChannelMessage(
// Guid.NewGuid().ToString(), "700", "88", "Ghost", "", DateTimeOffset.UtcNow);
//
// channel.RaiseMessageReceived(message);
//
// Assert.That(captured, Is.Null, "Message with empty text should be silently skipped");
// }
//
// [Test]
// public void Dispose_Fires_ConnectionStateChanged()
// {
// ChannelConnectionEventArgs? captured = null;
// channel.ConnectionStateChanged += (_, args) =>
// {
// captured = args;
// return Task.CompletedTask;
// };
//
// channel.Dispose();
//
// Assert.That(captured, Is.Not.Null);
// Assert.That(captured!.IsConnected, Is.False);
// Assert.That(captured.Channel, Is.SameAs(channel));
// }
// }
+26 -1
View File
@@ -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);
@@ -1,25 +1,27 @@
using Luna.Channels.Abstractions;
using Luna.Channels.Telegram;
using Luna.Configuration;
using Luna.Configuration.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Telegram.Bot;
namespace Luna.Channels.Extensions;
public static class ServiceCollectionExtensions
{
private const string TelegramBotToken = "8778257521:AAEt0ML88u2zx5AdZCi7XNKC1MhdngyCHJI";
extension(IServiceCollection services)
{
public IServiceCollection AddChannels()
{
services.AddSingleton<IChannelManager, ChannelManager>();
// Telegram
var telegramOptions = new TelegramOptions { BotToken = TelegramBotToken };
services.AddSingleton(telegramOptions);
services.AddSingleton<ITelegramBotClient>(_ => new TelegramBotClient(TelegramBotToken));
// Telegram — BotToken is bound from configuration ("Channels:Telegram"),
// supplied via the Channels__Telegram__BotToken environment variable.
services.AddTelegramOptions();
services.AddSingleton(provider => provider.GetRequiredService<IOptions<TelegramOptions>>().Value);
services.AddSingleton<ITelegramBotClient>(provider =>
new TelegramBotClient(provider.GetRequiredService<TelegramOptions>().BotToken));
services.AddHostedService<TelegramAdapter>();
return services;
+1
View File
@@ -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);
+23 -2
View File
@@ -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
@@ -42,10 +64,9 @@ public class TelegramChannel(
CancellationToken cancellationToken = default)
{
var targetChatId = long.Parse(ChannelId);
int? currentMessageId = null;
var typingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var typingTask = KeepTypingAsync(targetChatId, typingCts.Token);
var _ = KeepTypingAsync(targetChatId, typingCts.Token);
var messageBuilder = new StringBuilder();
await foreach (var update in messageStream.WithCancellation(cancellationToken))
+55 -2
View File
@@ -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;
@@ -14,6 +15,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)
{
await hubContext.SendStreamingResponseAsync(ChannelId, messageStream, cancellationToken);
@@ -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,
@@ -34,5 +44,48 @@ public class WebChannel(string channelId, IChatHubContext hubContext) : IChannel
});
}
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() { }
}
+1 -3
View File
@@ -1,5 +1,3 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using Luna.Shared;
using Microsoft.AspNetCore.SignalR.Client;
@@ -143,7 +141,7 @@ public class ChatClientService(IHostApplicationLifetime lifetime) : IHostedServi
try
{
await connection.InvokeAsync("OnMessageReceived", message, cancellationToken);
await connection.InvokeAsync("OnMessageReceivedAsync", message, cancellationToken);
await foreach (var update in responseChannel.Reader.ReadAllAsync(cancellationToken))
{
@@ -6,12 +6,12 @@ namespace Luna.Configuration.Extensions;
public static class ConfigurationBuilderExtensions
{
private static string[] AgentConfigSearchPaths =
private static readonly string[] AgentConfigSearchPaths =
[
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".luna", "agents")
];
private static string[] ProviderConfigSearchPaths =
private static readonly string[] ProviderConfigSearchPaths =
[
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".luna", "providers")
];
@@ -34,6 +34,11 @@ public static class ConfigurationBuilderExtensions
builder.Add(new ProviderOptionsConfigurationSource(searchPath));
}
// Re-applied last so environment variables take precedence over the TOML
// sources above. This is how secrets (API keys, bot tokens) are supplied —
// the embedded defaults ship with empty credentials.
builder.AddEnvironmentVariables();
return builder;
}
}
@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
@@ -1,5 +1,4 @@
ApiKey = "1nv151T9Qd2Vi5p3PMrvNDQ3DIDDVmGy"
ApiKey = ""
ApiUrl = "https://api.mistral.ai/v1/"
Models = ["mistral-small-latest"]
@@ -1,5 +1,4 @@
ApiKey = "apfelkuchen"
ApiKey = "ollama"
ApiUrl = "http://localhost:11434/"
Models = ["mistral-nemo:12b"]
+10 -5
View File
@@ -2,12 +2,17 @@ using Luna.Channels.Abstractions;
using Luna.Channels.Cli;
using Luna.Channels.Web;
using Luna.Core.Abstractions;
using Luna.Identity.Abstractions;
using Luna.Shared;
using Microsoft.AspNetCore.SignalR;
namespace Luna.Core.Hubs;
public class ChatHub(IChannelManager channelManager, IChatHubContext hubContext) : Hub
public class ChatHub(
IChannelManager channelManager,
IChatHubContext hubContext,
IUserIdentityService identityService)
: Hub
{
public override Task OnConnectedAsync()
{
@@ -15,8 +20,8 @@ public class ChatHub(IChannelManager channelManager, IChatHubContext hubContext)
IChannel channel = clientType switch
{
"web" => new WebChannel(Context.ConnectionId, hubContext),
_ => new CliChannel(Context.ConnectionId, hubContext)
"web" => new WebChannel(Context.ConnectionId, hubContext, identityService),
_ => new CliChannel(Context.ConnectionId, hubContext, identityService)
};
channelManager.RegisterChannel(channel);
@@ -29,7 +34,7 @@ public class ChatHub(IChannelManager channelManager, IChatHubContext hubContext)
return base.OnDisconnectedAsync(exception);
}
public void OnMessageReceived(ChannelMessage message)
public async Task OnMessageReceivedAsync(ChannelMessage message)
{
var channel = channelManager.GetChannel(Context.ConnectionId);
@@ -39,7 +44,7 @@ public class ChatHub(IChannelManager channelManager, IChatHubContext hubContext)
cli.RaiseMessageReceived(this, message);
break;
case WebChannel web:
web.RaiseMessageReceived(this, message);
await web.RaiseMessageReceivedAsync(this, message);
break;
default:
throw new InvalidOperationException(
+1
View File
@@ -19,6 +19,7 @@
<ProjectReference Include="..\Luna.Channels\Luna.Channels.csproj" />
<ProjectReference Include="..\Luna.Configuration\Luna.Configuration.csproj" />
<ProjectReference Include="..\Luna.Core.Abstractions\Luna.Core.Abstractions.csproj" />
<ProjectReference Include="..\Luna.Identity\Luna.Identity.csproj" />
<ProjectReference Include="..\Luna.Memory\Luna.Memory.csproj" />
<ProjectReference Include="..\Luna.Providers.Abstractions\Luna.Providers.Abstractions.csproj" />
<ProjectReference Include="..\Luna.Providers\Luna.Providers.csproj" />
+2
View File
@@ -7,6 +7,7 @@ using Luna.Core.Abstractions;
using Luna.Core.Hubs;
using Luna.Core.Services.Extensions;
using Luna.Core.Tools.Extensions;
using Luna.Identity.Extensions;
using Luna.Memory.Extensions;
using Luna.Providers.Extensions;
@@ -21,6 +22,7 @@ builder.Services
.AddAgents()
.AddTools()
.AddMemoryStore()
.AddUserIdentity()
.AddSessionManager()
.AddChannels();
+2 -1
View File
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using Luna.Agents.Abstractions;
using Luna.Core.Abstractions;
@@ -31,7 +32,7 @@ public class SessionManager(
string content,
string conversationId,
string connectionId,
CancellationToken ct = default)
[EnumeratorCancellation] CancellationToken ct = default)
{
if (!connectedClients.ContainsKey(connectionId))
connectedClients[connectionId] = conversationId;
+11
View File
@@ -0,0 +1,11 @@
namespace Luna.Identity.Abstractions;
/// <summary>
/// Associates a channel-specific user identity with a Luna user.
/// E.g., Telegram user ID "12345" → Luna user "guid-xxx"
/// </summary>
public record ChannelLink(
string ChannelType,
string ChannelUserId,
string UserId,
DateTimeOffset LinkedAt);
@@ -0,0 +1,6 @@
namespace Luna.Identity.Abstractions;
public interface IPairingCodeGenerator
{
PairingCode Generate(string channelType, string userId);
}
@@ -0,0 +1,9 @@
namespace Luna.Identity.Abstractions;
public interface IUserIdentityService
{
UserIdentity GetCurrentUser();
string StartChannelLink(string userId, string channelType);
void CompleteChannelLink(string userId, string channelType, string code);
bool IsChannelLinked(string userId, string channelType);
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+12
View File
@@ -0,0 +1,12 @@
namespace Luna.Identity.Abstractions;
/// <summary>
/// A short-lived code used to link a new channel to an existing user.
/// Generated on a trusted channel (e.g., web), confirmed from the channel being linked.
/// </summary>
public record PairingCode(
string Code,
string ChannelType,
string UserId,
DateTimeOffset CreatedAt,
TimeSpan TimeToLive);
@@ -0,0 +1,5 @@
namespace Luna.Identity.Abstractions;
public record UserIdentity(
string UserId,
string UserName);
@@ -0,0 +1,17 @@
using Luna.Identity.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace Luna.Identity.Extensions;
public static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddUserIdentity()
{
services.AddSingleton<IUserIdentityService, UserIdentityService>();
services.AddSingleton<IPairingCodeGenerator, PairingCodeGenerator>();
return services;
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Luna.Channels.Abstractions\Luna.Channels.Abstractions.csproj" />
<ProjectReference Include="..\Luna.Identity.Abstractions\Luna.Identity.Abstractions.csproj" />
<ProjectReference Include="..\Luna.Shared\Luna.Shared.csproj" />
</ItemGroup>
</Project>
+22
View File
@@ -0,0 +1,22 @@
using Luna.Identity.Abstractions;
namespace Luna.Identity;
public class PairingCodeGenerator : IPairingCodeGenerator
{
private const int TimeToLiveMinutes = 5;
private static readonly char[] CodeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".ToCharArray();
public PairingCode Generate(string channelType, string userId)
=> new(
Code: GenerateCode(),
ChannelType: channelType,
UserId: userId,
CreatedAt: DateTimeOffset.UtcNow,
TimeToLive: TimeSpan.FromMinutes(TimeToLiveMinutes));
private string GenerateCode()
=> new(Enumerable.Range(0, 6)
.Select(_ => CodeAlphabet[Random.Shared.Next(CodeAlphabet.Length)])
.ToArray());
}
+78
View File
@@ -0,0 +1,78 @@
using System.Collections.Concurrent;
using Luna.Identity.Abstractions;
namespace Luna.Identity;
public class UserIdentityService(IPairingCodeGenerator pairingCodeGenerator) : IUserIdentityService
{
// Todo: User creation
private readonly UserIdentity currentUser = new(
UserId: Guid.NewGuid().ToString(),
UserName: "Erik");
private readonly ConcurrentDictionary<string, IList<PairingCode>> pairingCodes = new();
private readonly ConcurrentDictionary<string, IList<ChannelLink>> linkedChannels = new();
public UserIdentity GetCurrentUser() => currentUser;
public string StartChannelLink(string userId, string channelType)
{
if (!pairingCodes.ContainsKey(userId))
pairingCodes[userId] = new List<PairingCode>();
var existingCode = pairingCodes[userId]
.SingleOrDefault(c => c.ChannelType == channelType);
switch (existingCode)
{
case {} when !IsPairingCodeExpired(existingCode):
return existingCode.Code;
case {} when IsPairingCodeExpired(existingCode):
pairingCodes[userId].Remove(existingCode);
return CreateNewPairingCode(userId, channelType);
default:
return CreateNewPairingCode(userId, channelType);
}
}
public void CompleteChannelLink(string userId, string channelType, string code)
{
if (!linkedChannels.ContainsKey(userId))
linkedChannels[userId] = new List<ChannelLink>();
if (linkedChannels[userId].Any(channel => channel.ChannelType == channelType && channel.UserId == userId))
throw new InvalidOperationException($"Channel {channelType} already linked to user with id {userId}");
if (!pairingCodes.TryGetValue(userId, out var userCodes))
throw new InvalidOperationException($"No active pairing codes for user with id {userId}");
var userCode = userCodes.SingleOrDefault(c => c.ChannelType == channelType && c.Code == code);
if (userCode is null)
throw new InvalidOperationException($"Pairing code {code} did not match any active codes for channel {channelType}");
if (IsPairingCodeExpired(userCode))
throw new InvalidOperationException($"Pairing code {code} is already expired");
linkedChannels[userId].Add(new ChannelLink(
ChannelType: channelType,
ChannelUserId: userId,
UserId: userId,
LinkedAt: DateTimeOffset.UtcNow));
}
public bool IsChannelLinked(string userId, string channelType)
=> linkedChannels.TryGetValue(userId, out var channelLinks)
&& channelLinks.Any(link => link.ChannelType == channelType && link.UserId == userId);
private bool IsPairingCodeExpired(PairingCode code)
=> code.CreatedAt + code.TimeToLive < DateTimeOffset.UtcNow;
private string CreateNewPairingCode(string userId, string channelType)
{
var code = pairingCodeGenerator.Generate(channelType, userId);
pairingCodes[userId].Add(code);
return code.Code;
}
}
@@ -19,9 +19,8 @@ public static class ServiceCollectionExtensions
}
/// <summary>
/// Registers the <see cref="MistralProvider"/> and maps <see cref="Provider"/> to it as a singleton.
/// Registers the <see cref="MistralProvider"/> and maps <see cref="IProvider"/> to it as a singleton.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The updated service collection.</returns>
private IServiceCollection AddMistralProvider()
{
@@ -42,7 +41,7 @@ public static class ServiceCollectionExtensions
}
/// <summary>
/// Registers the <see cref="OllamaProvider"/> and maps <see cref="Provider"/> to it as a singleton.
/// Registers the <see cref="OllamaProvider"/> and maps <see cref="IProvider"/> to it as a singleton.
/// </summary>
/// <returns></returns>
private IServiceCollection AddOllamaProvider()
-174
View File
@@ -1,174 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Clients.Cli", "Luna.Clients.Cli\Luna.Clients.Cli.csproj", "{233F9016-8B53-4598-B37D-8D1841354237}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Shared", "Luna.Shared\Luna.Shared.csproj", "{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Agents.Abstractions", "Luna.Agents.Abstractions\Luna.Agents.Abstractions.csproj", "{7843C43A-EA5C-4C73-8313-ED541FABB979}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Configuration", "Luna.Configuration\Luna.Configuration.csproj", "{2E23010C-732B-40E8-BA69-21306F3153EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Channels.Abstractions", "Luna.Channels.Abstractions\Luna.Channels.Abstractions.csproj", "{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Memory", "Luna.Memory\Luna.Memory.csproj", "{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Providers.Abstractions", "Luna.Providers.Abstractions\Luna.Providers.Abstractions.csproj", "{D09EF397-8671-4209-9B7C-2B3359D92AEB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Agents", "Luna.Agents\Luna.Agents.csproj", "{8373CF88-0AF3-4A59-B968-C26C2785F6E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Providers", "Luna.Providers\Luna.Providers.csproj", "{B969F122-E4F5-40F3-9554-51273C4CA163}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Core", "Luna.Core\Luna.Core.csproj", "{C58208B6-AF00-403A-A412-D4DD4B409AB5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Luna.Channels.Tests", "Luna.Channels.Tests\Luna.Channels.Tests.csproj", "{7FBBD74D-61AC-42C8-B745-73AC019871ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{233F9016-8B53-4598-B37D-8D1841354237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Debug|Any CPU.Build.0 = Debug|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Debug|x64.ActiveCfg = Debug|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Debug|x64.Build.0 = Debug|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Debug|x86.ActiveCfg = Debug|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Debug|x86.Build.0 = Debug|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Release|Any CPU.ActiveCfg = Release|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Release|Any CPU.Build.0 = Release|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Release|x64.ActiveCfg = Release|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Release|x64.Build.0 = Release|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Release|x86.ActiveCfg = Release|Any CPU
{233F9016-8B53-4598-B37D-8D1841354237}.Release|x86.Build.0 = Release|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Debug|x64.ActiveCfg = Debug|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Debug|x64.Build.0 = Debug|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Debug|x86.ActiveCfg = Debug|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Debug|x86.Build.0 = Debug|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Release|Any CPU.Build.0 = Release|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Release|x64.ActiveCfg = Release|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Release|x64.Build.0 = Release|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Release|x86.ActiveCfg = Release|Any CPU
{2F1BC65E-DF7D-47A7-8ED2-3FD152B009B6}.Release|x86.Build.0 = Release|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Debug|x64.ActiveCfg = Debug|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Debug|x64.Build.0 = Debug|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Debug|x86.ActiveCfg = Debug|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Debug|x86.Build.0 = Debug|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Release|Any CPU.Build.0 = Release|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Release|x64.ActiveCfg = Release|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Release|x64.Build.0 = Release|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Release|x86.ActiveCfg = Release|Any CPU
{7843C43A-EA5C-4C73-8313-ED541FABB979}.Release|x86.Build.0 = Release|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Debug|x64.ActiveCfg = Debug|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Debug|x64.Build.0 = Debug|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Debug|x86.ActiveCfg = Debug|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Debug|x86.Build.0 = Debug|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Release|Any CPU.Build.0 = Release|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Release|x64.ActiveCfg = Release|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Release|x64.Build.0 = Release|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Release|x86.ActiveCfg = Release|Any CPU
{2E23010C-732B-40E8-BA69-21306F3153EB}.Release|x86.Build.0 = Release|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Debug|x64.ActiveCfg = Debug|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Debug|x64.Build.0 = Debug|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Debug|x86.ActiveCfg = Debug|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Debug|x86.Build.0 = Debug|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Release|Any CPU.Build.0 = Release|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Release|x64.ActiveCfg = Release|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Release|x64.Build.0 = Release|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Release|x86.ActiveCfg = Release|Any CPU
{B99B44FC-2C24-4E44-9D47-A0B9BB0D28D7}.Release|x86.Build.0 = Release|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Debug|x64.ActiveCfg = Debug|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Debug|x64.Build.0 = Debug|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Debug|x86.ActiveCfg = Debug|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Debug|x86.Build.0 = Debug|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Release|Any CPU.Build.0 = Release|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Release|x64.ActiveCfg = Release|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Release|x64.Build.0 = Release|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Release|x86.ActiveCfg = Release|Any CPU
{CE5CCA51-AC5F-4F56-82D9-9CA698AD1CD7}.Release|x86.Build.0 = Release|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Debug|x64.ActiveCfg = Debug|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Debug|x64.Build.0 = Debug|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Debug|x86.ActiveCfg = Debug|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Debug|x86.Build.0 = Debug|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Release|Any CPU.Build.0 = Release|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Release|x64.ActiveCfg = Release|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Release|x64.Build.0 = Release|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Release|x86.ActiveCfg = Release|Any CPU
{D09EF397-8671-4209-9B7C-2B3359D92AEB}.Release|x86.Build.0 = Release|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Debug|x64.ActiveCfg = Debug|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Debug|x64.Build.0 = Debug|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Debug|x86.ActiveCfg = Debug|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Debug|x86.Build.0 = Debug|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Release|Any CPU.Build.0 = Release|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Release|x64.ActiveCfg = Release|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Release|x64.Build.0 = Release|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Release|x86.ActiveCfg = Release|Any CPU
{8373CF88-0AF3-4A59-B968-C26C2785F6E5}.Release|x86.Build.0 = Release|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Debug|x64.ActiveCfg = Debug|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Debug|x64.Build.0 = Debug|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Debug|x86.ActiveCfg = Debug|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Debug|x86.Build.0 = Debug|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Release|Any CPU.Build.0 = Release|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Release|x64.ActiveCfg = Release|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Release|x64.Build.0 = Release|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Release|x86.ActiveCfg = Release|Any CPU
{B969F122-E4F5-40F3-9554-51273C4CA163}.Release|x86.Build.0 = Release|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Debug|x64.ActiveCfg = Debug|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Debug|x64.Build.0 = Debug|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Debug|x86.ActiveCfg = Debug|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Debug|x86.Build.0 = Debug|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Release|Any CPU.Build.0 = Release|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Release|x64.ActiveCfg = Release|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Release|x64.Build.0 = Release|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Release|x86.ActiveCfg = Release|Any CPU
{C58208B6-AF00-403A-A412-D4DD4B409AB5}.Release|x86.Build.0 = Release|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Debug|x64.ActiveCfg = Debug|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Debug|x64.Build.0 = Debug|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Debug|x86.ActiveCfg = Debug|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Debug|x86.Build.0 = Debug|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Release|Any CPU.Build.0 = Release|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Release|x64.ActiveCfg = Release|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Release|x64.Build.0 = Release|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Release|x86.ActiveCfg = Release|Any CPU
{7FBBD74D-61AC-42C8-B745-73AC019871ED}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+3
View File
@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String>
<s:String x:Key="/Default/CustomTools/CustomToolsData/@EntryValue"></s:String></wpf:ResourceDictionary>
+2
View File
@@ -10,6 +10,8 @@
<Project Path="Luna.Configuration/Luna.Configuration.csproj" />
<Project Path="Luna.Core.Abstractions/Luna.Core.Abstractions.csproj" />
<Project Path="Luna.Core/Luna.Core.csproj" />
<Project Path="Luna.Identity.Abstractions/Luna.Identity.Abstractions.csproj" />
<Project Path="Luna.Identity/Luna.Identity.csproj" />
<Project Path="Luna.Memory/Luna.Memory.csproj" />
<Project Path="Luna.Providers.Abstractions/Luna.Providers.Abstractions.csproj" />
<Project Path="Luna.Providers/Luna.Providers.csproj" />
+1 -1
View File
@@ -129,7 +129,7 @@ export const LunaBridgeProvider: React.FC<PropsWithChildren> = ({ children }) =>
timestamp: message.timestamp
}]);
await connection.invoke("OnMessageReceived", message);
await connection.invoke("OnMessageReceivedAsync", message);
}, []);
return (