diff --git a/.gitignore b/.gitignore index 8e8f5b54..d8289836 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ yarn.lock .vscode/ .DS_Store go-peer/go-peer -**/.idea +.cursor/ +.qodo diff --git a/dotnet-peer/.gitignore b/dotnet-peer/.gitignore new file mode 100644 index 00000000..c008421e --- /dev/null +++ b/dotnet-peer/.gitignore @@ -0,0 +1,40 @@ +# .NET Core +bin/ +obj/ +.vs/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio files +.vs/ +.vscode/ +*.nupkg +*.pfx +*.snk +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +*.snupkg +**/packages/* +!**/packages/build/ diff --git a/dotnet-peer/Libp2pChat/.gitignore b/dotnet-peer/Libp2pChat/.gitignore new file mode 100644 index 00000000..6ac8832a --- /dev/null +++ b/dotnet-peer/Libp2pChat/.gitignore @@ -0,0 +1,41 @@ +# .NET Core +bin/ +obj/ +.vs/ +*.user +*.suo +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio files +.vs/ +.vscode/ +*.nupkg +*.pfx +*.snk +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +*.snupkg +**/packages/* +!**/packages/build/ +.qodo diff --git a/dotnet-peer/Libp2pChat/Application/Interfaces/IAppLogger.cs b/dotnet-peer/Libp2pChat/Application/Interfaces/IAppLogger.cs new file mode 100644 index 00000000..73561142 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Application/Interfaces/IAppLogger.cs @@ -0,0 +1,22 @@ +namespace Libp2pChat.Application.Interfaces; + +/// +/// Legacy interface for application logging. Use Microsoft.Extensions.Logging.ILogger instead for new code. +/// +public interface IAppLogger +{ + /// Logs an informational message. + void LogInformation(string message); + + /// Logs a warning message. + void LogWarning(string message); + + /// Logs an error message. + void LogError(string message); + + /// Logs an error message with an exception. + void LogError(string message, Exception exception); + + /// Logs a debug message. + void LogDebug(string message); +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Application/Interfaces/IChatService.cs b/dotnet-peer/Libp2pChat/Application/Interfaces/IChatService.cs new file mode 100644 index 00000000..29cd8aa2 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Application/Interfaces/IChatService.cs @@ -0,0 +1,54 @@ +using Libp2pChat.Domain.Models; + +namespace Libp2pChat.Application.Interfaces; + +/// +/// Defines the operations for a chat service. +/// +public interface IChatService +{ + /// + /// Gets the current peer ID. + /// + string PeerId { get; } + + /// + /// Gets the multiaddress for this peer. + /// + string GetMultiaddress(); + + /// + /// Gets the list of currently known peers. + /// + IReadOnlyCollection GetKnownPeers(); + + /// + /// Sends a message to all peers. + /// + /// The message to send. + /// A task representing the asynchronous operation. + Task SendMessageAsync(string message); + + /// + /// Event raised when a new message is received. + /// + event EventHandler MessageReceived; + + /// + /// Event raised when a peer is detected. + /// + event EventHandler PeerDiscovered; + + /// + /// Starts the chat service. + /// + /// The cancellation token. + /// A task representing the asynchronous operation. + Task StartAsync(CancellationToken cancellationToken); + + /// + /// Stops the chat service. + /// + /// A task representing the asynchronous operation. + Task StopAsync(); +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Application/Services/ChatApplicationService.cs b/dotnet-peer/Libp2pChat/Application/Services/ChatApplicationService.cs new file mode 100644 index 00000000..b5b4d8db --- /dev/null +++ b/dotnet-peer/Libp2pChat/Application/Services/ChatApplicationService.cs @@ -0,0 +1,203 @@ +using Libp2pChat.Application.Interfaces; +using Libp2pChat.Domain.Models; +using Libp2pChat.Presentation.UI; +using Microsoft.Extensions.Logging; + +namespace Libp2pChat.Application.Services; + +/// +/// Application service that coordinates the chat components. +/// +public class ChatApplicationService : IDisposable +{ + private readonly IChatService _chatService; + private readonly IChatUI _chatUI; + private readonly ILogger _logger; + private readonly CancellationTokenSource _cancellationTokenSource; + private bool _isDisposed; + + /// + /// Creates a new instance of the chat application service. + /// + public ChatApplicationService(IChatService chatService, IChatUI chatUI, ILogger logger) + { + _chatService = chatService; + _chatUI = chatUI; + _logger = logger; + _cancellationTokenSource = new CancellationTokenSource(); + } + + /// + /// Starts the chat application. + /// + public async Task RunAsync() + { + try + { + // Initialize the UI + _chatUI.Initialize(); + + // Wire up events + _chatService.MessageReceived += OnMessageReceived; + _chatService.PeerDiscovered += OnPeerDiscovered; + _chatUI.MessageSent += OnMessageSent; + _chatUI.ExitRequested += OnExitRequested; + + // Start the chat service in a background task + _ = Task.Run(async () => + { + try + { + // Start the chat service + _logger.LogInformation("Starting chat application..."); + await _chatService.StartAsync(_cancellationTokenSource.Token); + + // Update the UI with peer info + string peerId = _chatService.PeerId; + string multiaddress = _chatService.GetMultiaddress(); + _chatUI.UpdatePeerInfo(peerId, multiaddress); + + // Log connection info + _logger.LogInformation("Peer ID: {PeerId}", peerId); + _logger.LogInformation("Multiaddress: {Multiaddress}", multiaddress); + + // Add welcome messages directly to the chat area + _chatUI.AddChatMessage("Welcome to Libp2p Chat!"); + _chatUI.AddChatMessage("You can exchange messages with other libp2p peers."); + _chatUI.AddChatMessage("Use the 'Send' button or press Enter to send messages."); + _chatUI.AddChatMessage("Press Ctrl+Q to exit."); + + // Add help information + _logger.LogInformation("\n[Help] CONNECTION INSTRUCTIONS:"); + _logger.LogInformation("[Help] Connect to this peer using your libp2p client:"); + _logger.LogInformation("[Help] Multiaddress: {Multiaddress}", multiaddress); + _logger.LogInformation("[Help] For localhost connections: /ip4/127.0.0.1/tcp/{Port}/p2p/{PeerId}", multiaddress.Split('/')[4], peerId); + _logger.LogInformation("[Help] Use the above multiaddress with your libp2p client's connect command."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in background task"); + } + }); + + // Run the UI on the main thread + _chatUI.Run(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting chat application"); + throw; + } + } + + /// + /// Stops the chat application. + /// + public async Task StopAsync() + { + try + { + _logger.LogInformation("Stopping chat application..."); + + if (!_cancellationTokenSource.IsCancellationRequested) + _cancellationTokenSource.Cancel(); + + await _chatService.StopAsync(); + + _logger.LogInformation("Chat application stopped"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error stopping chat application"); + throw; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes resources. + /// + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + { + // Unwire events + _chatService.MessageReceived -= OnMessageReceived; + _chatService.PeerDiscovered -= OnPeerDiscovered; + _chatUI.MessageSent -= OnMessageSent; + _chatUI.ExitRequested -= OnExitRequested; + + // Dispose cancellation token source + _cancellationTokenSource.Dispose(); + + // Dispose other disposables + if (_chatService is IDisposable disposableService) + disposableService.Dispose(); + + if (_chatUI is IDisposable disposableUI) + disposableUI.Dispose(); + } + + _isDisposed = true; + } + + private void OnMessageReceived(object? sender, ChatMessage message) + { + try + { + _chatUI.AddChatMessage(message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling received message"); + } + } + + private void OnPeerDiscovered(object? sender, Peer peer) + { + try + { + _chatUI.AddPeer(peer); + _logger.LogInformation("Peer discovered: {PeerId} ({DisplayName})", peer.Id, peer.DisplayName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling peer discovery"); + } + } + + private async void OnMessageSent(object? sender, string message) + { + try + { + await _chatService.SendMessageAsync(message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending message"); + } + } + + private async void OnExitRequested(object? sender, EventArgs e) + { + try + { + await StopAsync(); + + Terminal.Gui.Application.RequestStop(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling exit request"); + } + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Dockerfile b/dotnet-peer/Libp2pChat/Dockerfile new file mode 100644 index 00000000..0d675fbb --- /dev/null +++ b/dotnet-peer/Libp2pChat/Dockerfile @@ -0,0 +1,46 @@ +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS builder + +WORKDIR /usr/src/app + +# Copy csproj and restore dependencies +COPY Libp2pChat.csproj ./ +RUN dotnet restore + +COPY . . + + +RUN dotnet publish -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine + +# Install required packages for Terminal.Gui +RUN apk add --no-cache \ + bash \ + ncurses \ + ca-certificates \ + && adduser -D appuser + +# Create dir & add permissions +RUN mkdir -p /app/data && \ + chown -R appuser:appuser /app + +# Copy the published application from builder +COPY --from=builder /app/publish /app/ + +# Set working directory +WORKDIR /app/data + +# Use non-root user +USER appuser + +# Set env vars +ENV TERM=xterm-256color +ENV LANG=C.UTF-8 +ENV DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION=1 + +# Expose port +EXPOSE 9050 + +# Run the application +ENTRYPOINT ["dotnet", "/app/Libp2pChat.dll"] \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Domain/Models/ChatMessage.cs b/dotnet-peer/Libp2pChat/Domain/Models/ChatMessage.cs new file mode 100644 index 00000000..83904fca --- /dev/null +++ b/dotnet-peer/Libp2pChat/Domain/Models/ChatMessage.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; + +namespace Libp2pChat.Domain.Models; + +/// +/// Represents a chat message exchanged between peers. +/// +public class ChatMessage +{ + /// + /// The content of the message. + /// + [JsonPropertyName("Message")] + public string Message { get; set; } = string.Empty; + + /// + /// The unique identifier of the sender. + /// + [JsonPropertyName("SenderID")] + public string SenderID { get; set; } = string.Empty; + + /// + /// The nickname of the sender. + /// + [JsonPropertyName("SenderNick")] + public string SenderNick { get; set; } = string.Empty; + + /// + /// The timestamp when the message was created. + /// + [JsonIgnore] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + /// + /// Creates a new instance of the class. + /// + public ChatMessage() { } + + /// + /// Creates a new instance of the class with specified properties. + /// + public ChatMessage(string message, string senderId, string senderNick) + { + Message = message; + SenderID = senderId; + SenderNick = senderNick; + } + + /// + /// Creates a system message. + /// + public static ChatMessage CreateSystemMessage(string message) + { + return new ChatMessage(message, "system", "system"); + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Domain/Models/Peer.cs b/dotnet-peer/Libp2pChat/Domain/Models/Peer.cs new file mode 100644 index 00000000..02d881e5 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Domain/Models/Peer.cs @@ -0,0 +1,68 @@ +namespace Libp2pChat.Domain.Models; + +/// +/// Represents a peer in the libp2p network. +/// +public class Peer +{ + /// + /// Gets the unique identifier of the peer. + /// + public string Id { get; } + + /// + /// Gets or sets the display name of the peer. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets the last time this peer was seen active. + /// + public DateTime LastSeen { get; set; } + + /// + /// Gets the short version of the peer ID suitable for display. + /// + public string ShortId => Id.Length > 10 ? Id.Substring(0, 10) + "..." : Id; + + /// + /// Creates a new instance of the class. + /// + /// The peer identifier. + /// The display name for the peer. + public Peer(string id, string displayName) + { + Id = id; + DisplayName = displayName; + LastSeen = DateTime.UtcNow; + } + + /// + /// Updates the last seen timestamp to the current time. + /// + public void UpdateLastSeen() + { + LastSeen = DateTime.UtcNow; + } + + /// + /// Gets a formatted string indicating how long ago this peer was last seen. + /// + public string GetLastSeenFormatted() + { + TimeSpan sinceLastSeen = DateTime.UtcNow - LastSeen; + return sinceLastSeen.TotalMinutes < 1 + ? $"{sinceLastSeen.TotalSeconds:0}s ago" + : $"{sinceLastSeen.TotalMinutes:0.0}m ago"; + } + + /// + /// Creates a generic libp2p peer. + /// + /// Optional implementation name (e.g., 'go', 'js', 'rust') + /// A new peer instance representing a libp2p peer + public static Peer CreateLibp2pPeer(string implementation = "unknown") + { + return new Peer($"libp2p-{implementation}-{DateTime.UtcNow.Ticks % 10000}", $"libp2p-{implementation}"); + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Infrastructure/Libp2p/Libp2pChatService.cs b/dotnet-peer/Libp2pChat/Infrastructure/Libp2p/Libp2pChatService.cs new file mode 100644 index 00000000..bf369c46 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Infrastructure/Libp2p/Libp2pChatService.cs @@ -0,0 +1,353 @@ +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; +using Libp2pChat.Application.Interfaces; +using Libp2pChat.Domain.Models; +using Multiformats.Address; +using Nethermind.Libp2p; +using Nethermind.Libp2p.Core; +using Nethermind.Libp2p.Protocols.Pubsub; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; + +namespace Libp2pChat.Infrastructure.Libp2p; + +/// +/// Implementation of the chat service using libp2p. +/// +public class Libp2pChatService : IChatService, IDisposable +{ + private readonly ILogger _logger; + private readonly IPeerFactory _peerFactory; + private readonly PubsubRouter _router; + private readonly Identity _identity; + private readonly string _topicName; + private readonly ConcurrentDictionary _knownPeers = new(); + private ILocalPeer? _localPeer; + private ITopic? _topic; + private CancellationTokenSource? _cancellationTokenSource; + private bool _isStarted; + private bool _isDisposed; + private readonly string _listenAddress; + private readonly int _port; + private int _actualPort; + + /// + public string PeerId => _identity.PeerId.ToString(); + + /// + public event EventHandler? MessageReceived; + + /// + public event EventHandler? PeerDiscovered; + + /// + /// Creates a new instance of the chat service. + /// + public Libp2pChatService( + ILogger logger, + IPeerFactory peerFactory, + PubsubRouter router, + string topicName = "universal-connectivity", + Identity? identity = null, + int port = 0 + ) + { + _logger = logger; + _peerFactory = peerFactory; + _router = router; + _topicName = topicName; + _identity = identity ?? new Identity(); + _listenAddress = "0.0.0.0"; + _port = port; + _actualPort = port; + } + + /// + public string GetMultiaddress() + { + return $"/ip4/{_listenAddress}/tcp/{_actualPort}/p2p/{PeerId}"; + } + + /// + public IReadOnlyCollection GetKnownPeers() + { + return _knownPeers.Values.ToList().AsReadOnly(); + } + + /// + public async Task SendMessageAsync(string message) + { + if (!_isStarted) + throw new InvalidOperationException("The chat service has not been started."); + + if (_topic == null) + throw new InvalidOperationException("The chat topic is not available."); + + try + { + // Send as plain text for compatibility with other implementations + byte[] data = Encoding.UTF8.GetBytes(message); + _topic.Publish(data); + + _logger.LogDebug("Message sent: {Message}", message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send message"); + throw; + } + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_isStarted) + return; + + _logger.LogInformation("Starting chat service..."); + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + try + { + _localPeer = _peerFactory.Create(_identity); + + if (_port == 0) + { + _actualPort = GetRandomAvailablePort(); + _logger.LogInformation("Using random port: {Port}", _actualPort); + } + else + { + _actualPort = _port; + } + + // Create multiaddresses to listen on + var allIfacesAddress = Multiaddress.Decode($"/ip4/{_listenAddress}/tcp/{_actualPort}/p2p/{PeerId}"); + + // Start listening for peer connections + _logger.LogInformation("Starting peer listener..."); + await _localPeer.StartListenAsync(new[] { allIfacesAddress }, _cancellationTokenSource.Token); + _logger.LogInformation("Peer listener started successfully"); + _logger.LogInformation("Listening on: {Address}", allIfacesAddress); + + // Get topic and subscribe to messages + _topic = _router.GetTopic(_topicName); + _topic.OnMessage += HandleMessageReceived; + + // Start router + _logger.LogInformation("Starting pubsub router..."); + await _router.StartAsync(_localPeer, _cancellationTokenSource.Token); + _logger.LogInformation("Pubsub router started successfully"); + + // Start cleanup task + _ = Task.Run(RunPeerCleanupTask, _cancellationTokenSource.Token); + + _isStarted = true; + + _logger.LogInformation("Chat service started successfully"); + _logger.LogInformation("Peer ID: {PeerId}", PeerId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to start chat service"); + await StopAsync(); + throw; + } + } + + /// + /// Gets a random available port. + /// + private int GetRandomAvailablePort() + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(new IPEndPoint(IPAddress.Any, 0)); + var endPoint = (IPEndPoint)socket.LocalEndPoint!; + return endPoint.Port; + } + + /// + public async Task StopAsync() + { + if (!_isStarted) + return; + + _logger.LogInformation("Stopping chat service..."); + + try + { + if (_topic != null) + { + _topic.OnMessage -= HandleMessageReceived; + } + + if (_cancellationTokenSource != null) + { + if (!_cancellationTokenSource.IsCancellationRequested) + _cancellationTokenSource.Cancel(); + + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + + _isStarted = false; + _logger.LogInformation("Chat service stopped"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while stopping chat service"); + throw; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes resources. + /// + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + { + _cancellationTokenSource?.Dispose(); + } + + _isDisposed = true; + } + + private void HandleMessageReceived(byte[] data) + { + try + { + if (data == null || data.Length == 0) + { + _logger.LogWarning("Received empty message data"); + return; + } + + string raw = Encoding.UTF8.GetString(data).Trim(); + _logger.LogDebug("Raw message received: {Message}", raw); + + // Check if the message is JSON or plain text + bool isJsonMessage = raw.StartsWith("{"); + if (isJsonMessage) + { + try + { + // Try to deserialize as a ChatMessage + var chatMessage = JsonSerializer.Deserialize(raw); + if (chatMessage != null && !string.IsNullOrEmpty(chatMessage.Message)) + { + // Don't process messages from ourselves + if (chatMessage.SenderID == PeerId) + return; + + // Track peer + UpdateOrAddPeer(chatMessage.SenderID, chatMessage.SenderNick); + + // Raise event + MessageReceived?.Invoke(this, chatMessage); + } + else + { + _logger.LogWarning("Received invalid JSON message structure: {Message}", raw); + } + } + catch (JsonException ex) + { + _logger.LogError(ex, "JSON parsing error: {Message}", ex.Message); + + var plainTextMessage = new ChatMessage(raw, "unknown", "peer"); + MessageReceived?.Invoke(this, plainTextMessage); + } + } + else + { + var plainTextMessage = new ChatMessage(raw, "unknown", "peer"); + MessageReceived?.Invoke(this, plainTextMessage); + _logger.LogInformation("Received plain text message from peer"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling message"); + } + } + + private void UpdateOrAddPeer(string peerId, string nickname) + { + if (string.IsNullOrEmpty(peerId)) + return; + + if (_knownPeers.TryGetValue(peerId, out var existingPeer)) + { + existingPeer.UpdateLastSeen(); + + if (!string.IsNullOrEmpty(nickname) && existingPeer.DisplayName != nickname) + { + existingPeer.DisplayName = nickname; + } + } + else + { + var newPeer = new Peer(peerId, string.IsNullOrEmpty(nickname) ? "unknown" : nickname); + if (_knownPeers.TryAdd(peerId, newPeer)) + { + PeerDiscovered?.Invoke(this, newPeer); + _logger.LogInformation("New peer discovered: {PeerId} ({DisplayName})", peerId, newPeer.DisplayName); + } + } + } + + private async Task RunPeerCleanupTask() + { + try + { + while (!_cancellationTokenSource!.Token.IsCancellationRequested) + { + try + { + _logger.LogDebug("Known peers: {Count}", _knownPeers.Count); + + // Clean up inactive peers (not seen in last 5 minutes) + DateTime cutoff = DateTime.UtcNow.AddMinutes(-5); + foreach (var (peerId, peer) in _knownPeers) + { + if (peer.LastSeen < cutoff) + { + if (_knownPeers.TryRemove(peerId, out _)) + { + _logger.LogInformation("Removed inactive peer: {PeerId}", peerId); + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in peer cleanup task"); + } + + // Check every 30 seconds + await Task.Delay(TimeSpan.FromSeconds(30), _cancellationTokenSource.Token); + } + } + catch (OperationCanceledException) + { + // Expected when token is canceled + } + catch (Exception ex) + { + _logger.LogError(ex, "Peer cleanup task failed"); + } + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Infrastructure/Logging/LoggerAdapter.cs b/dotnet-peer/Libp2pChat/Infrastructure/Logging/LoggerAdapter.cs new file mode 100644 index 00000000..c95ed0c8 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Infrastructure/Logging/LoggerAdapter.cs @@ -0,0 +1,36 @@ +using Libp2pChat.Application.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Libp2pChat.Infrastructure.Logging; + +/// +/// Adapter that wraps ILogger to implement IAppLogger for backward compatibility. +/// +public class LoggerAdapter : IAppLogger +{ + private readonly ILogger _logger; + + /// + /// Creates a new instance of the class. + /// + /// The standard logger implementation. + public LoggerAdapter(ILogger logger) + { + _logger = logger; + } + + /// + public void LogInformation(string message) => _logger.LogInformation(message); + + /// + public void LogWarning(string message) => _logger.LogWarning(message); + + /// + public void LogError(string message) => _logger.LogError(message); + + /// + public void LogError(string message, Exception exception) => _logger.LogError(exception, message); + + /// + public void LogDebug(string message) => _logger.LogDebug(message); +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Infrastructure/Logging/TerminalLogger.cs b/dotnet-peer/Libp2pChat/Infrastructure/Logging/TerminalLogger.cs new file mode 100644 index 00000000..21e5f837 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Infrastructure/Logging/TerminalLogger.cs @@ -0,0 +1,92 @@ +using Libp2pChat.Application.Interfaces; +using Libp2pChat.Presentation.UI; +using Microsoft.Extensions.Logging; + +namespace Libp2pChat.Infrastructure.Logging; + +/// +/// A logger implementation that logs to the terminal UI. +/// +public class TerminalLogger : IAppLogger +{ + private readonly IChatUI _chatUI; + private readonly string _category; + + /// + /// Creates a new instance of the class. + /// + /// The chat UI. + /// The category name for the logger. + public TerminalLogger(IChatUI chatUI, string category) + { + _chatUI = chatUI; + _category = category; + } + + private void LogWithLevel(string level, string message) + { + string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] [{level}] {message}"; + _chatUI.AddLog(formattedMessage); + } + + /// + public void LogInformation(string message) + { + LogWithLevel("Info", message); + } + + /// + public void LogWarning(string message) + { + LogWithLevel("Warning", message); + } + + /// + public void LogError(string message) + { + LogWithLevel("Error", message); + } + + /// + public void LogError(string message, Exception exception) + { + LogWithLevel("Error", message); + LogWithLevel("Error", $"Exception: {exception.Message}\nStack trace: {exception.StackTrace}"); + } + + /// + public void LogDebug(string message) + { + LogWithLevel("Debug", message); + } +} + +/// +/// Factory for creating terminal loggers. +/// +public class TerminalLoggerFactory +{ + private readonly IChatUI _chatUI; + private readonly ILoggerFactory _loggerFactory; + + /// + /// Creates a new instance of the class. + /// + /// The chat UI. + /// The logger factory. + public TerminalLoggerFactory(IChatUI chatUI, ILoggerFactory loggerFactory) + { + _chatUI = chatUI; + _loggerFactory = loggerFactory; + } + + /// + /// Creates a new terminal logger for the specified category. + /// + /// The category name. + /// A new terminal logger. + public IAppLogger CreateLogger(string category) + { + return new TerminalLogger(_chatUI, category); + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Infrastructure/Logging/UiLoggerProvider.cs b/dotnet-peer/Libp2pChat/Infrastructure/Logging/UiLoggerProvider.cs new file mode 100644 index 00000000..01dc5a19 --- /dev/null +++ b/dotnet-peer/Libp2pChat/Infrastructure/Logging/UiLoggerProvider.cs @@ -0,0 +1,144 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Libp2pChat.Presentation.UI; + +namespace Libp2pChat.Infrastructure.Logging; + +/// +/// Logger implementation that sends log messages to the chat UI. +/// +public class UiLogger : ILogger +{ + private readonly string _categoryName; + private readonly IChatUI _chatUI; + + /// + /// Creates a new instance of . + /// + /// The category name for the logger. + /// The chat UI. + public UiLogger(string categoryName, IChatUI chatUI) + { + _categoryName = categoryName; + _chatUI = chatUI; + } + + /// + public IDisposable BeginScope(TState state) + { + return NullScope.Instance; + } + + /// + public bool IsEnabled(LogLevel logLevel) + { + return logLevel != LogLevel.None; + } + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + string message = formatter(state, exception); + string shortCategory = GetShortCategoryName(_categoryName); + string levelString = GetLogLevelString(logLevel); + string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] [{levelString}] [{shortCategory}] {message}"; + + _chatUI.AddLog(formattedMessage); + + if (exception != null) + { + _chatUI.AddLog($"[{DateTime.Now:HH:mm:ss.fff}] [{levelString}] Exception: {exception.Message}"); + } + } + + private string GetShortCategoryName(string categoryName) + { + // Get the last part of the category name (e.g., "Sample.App.Controllers.HomeController" -> "HomeController") + int lastDotIndex = categoryName.LastIndexOf('.'); + return lastDotIndex != -1 ? categoryName.Substring(lastDotIndex + 1) : categoryName; + } + + private string GetLogLevelString(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => "TRACE", + LogLevel.Debug => "DEBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERROR", + LogLevel.Critical => "CRIT", + _ => logLevel.ToString().ToUpper(), + }; + } + + private class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + private NullScope() + { + } + + public void Dispose() + { + } + } +} + +/// +/// Creates UiLogger instances that send logs to the chat UI. +/// +public class UiLoggerProvider : ILoggerProvider +{ + private readonly IChatUI _chatUI; + + /// + /// Creates a new instance of . + /// + /// The chat UI to log to. + public UiLoggerProvider(IChatUI chatUI) + { + _chatUI = chatUI; + } + + /// + public ILogger CreateLogger(string categoryName) + { + return new UiLogger(categoryName, _chatUI); + } + + /// + public void Dispose() + { + // No resources to dispose + } +} + +/// +/// Extension methods for adding UI logger to the logging builder. +/// +public static class UiLoggerExtensions +{ + /// + /// Adds a UI logger that logs to the chat UI. + /// + /// The logging builder. + /// The logging builder for chaining. + public static ILoggingBuilder AddUiLogger(this ILoggingBuilder builder) + { + builder.Services.AddSingleton(sp => + { + var chatUI = sp.GetRequiredService(); + return new UiLoggerProvider(chatUI); + }); + + return builder; + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Libp2pChat.csproj b/dotnet-peer/Libp2pChat/Libp2pChat.csproj new file mode 100644 index 00000000..862b55ff --- /dev/null +++ b/dotnet-peer/Libp2pChat/Libp2pChat.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/dotnet-peer/Libp2pChat/Presentation/UI/IChatUI.cs b/dotnet-peer/Libp2pChat/Presentation/UI/IChatUI.cs new file mode 100644 index 00000000..09f4412b --- /dev/null +++ b/dotnet-peer/Libp2pChat/Presentation/UI/IChatUI.cs @@ -0,0 +1,60 @@ +using Libp2pChat.Domain.Models; + +namespace Libp2pChat.Presentation.UI; + +/// +/// Defines the operations for a chat user interface. +/// +public interface IChatUI +{ + /// + /// Adds a message to the chat. + /// + /// The message to add. + void AddChatMessage(string message); + + /// + /// Adds a message to the chat. + /// + /// The chat message to add. + void AddChatMessage(ChatMessage chatMessage); + + /// + /// Adds a log message. + /// + /// The message to log. + void AddLog(string message); + + /// + /// Adds a peer to the peers list. + /// + /// The peer to add. + void AddPeer(Peer peer); + + /// + /// Updates the information displayed about the local peer. + /// + /// The peer ID. + /// The multiaddress. + void UpdatePeerInfo(string peerId, string multiaddress); + + /// + /// Initializes the UI. + /// + void Initialize(); + + /// + /// Runs the UI. + /// + void Run(); + + /// + /// Triggered when a message is sent from the UI. + /// + event EventHandler MessageSent; + + /// + /// Triggered when the UI is exited. + /// + event EventHandler ExitRequested; +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Presentation/UI/TerminalUI.cs b/dotnet-peer/Libp2pChat/Presentation/UI/TerminalUI.cs new file mode 100644 index 00000000..94a8d92d --- /dev/null +++ b/dotnet-peer/Libp2pChat/Presentation/UI/TerminalUI.cs @@ -0,0 +1,405 @@ +using Libp2pChat.Domain.Models; +using Terminal.Gui; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Libp2pChat.Presentation.UI; + +/// +/// Implementation of the terminal user interface using Terminal.Gui. +/// +public class TerminalUI : IChatUI, IDisposable +{ + private readonly object _uiLock = new(); + private readonly List _chatHistory = new(); + private readonly List _logHistory = new(); + private readonly List _peersHistory = new(); + private bool _isInitialized; + private bool _isDisposed; + + // UI elements + private Window? _mainWindow; + private FrameView? _infoFrame; + private Label? _peerIdLabel; + private Label? _multiAddrLabel; + private TabView? _tabView; + private TextView? _chatTextView; + private TextView? _logsTextView; + private ListView? _peersListView; + private TextField? _inputField; + private Button? _sendButton; + private Button? _exitButton; + + /// + public event EventHandler? MessageSent; + + /// + public event EventHandler? ExitRequested; + + /// + /// Creates a new instance of the class. + /// + public TerminalUI() + { + } + + /// + public void Initialize() + { + if (_isInitialized) + return; + + try + { + // Check for Windows console issues + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Console.WriteLine("Running on Windows. Checking console environment..."); + + try + { + int width = Console.WindowWidth; + int height = Console.WindowHeight; + Console.WriteLine($"Console dimensions: {width}x{height}"); + + Console.CursorVisible = true; + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Console environment issue detected: {ex.Message}"); + Console.WriteLine("You may need to run this application in a proper console window."); + } + } + + Terminal.Gui.Application.Init(); + Console.WriteLine("Terminal.Gui initialized successfully"); + + var top = Terminal.Gui.Application.Top; + + _mainWindow = new Window("Libp2p Chat") + { + X = 0, + Y = 1, + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + _infoFrame = new FrameView("Peer Info") + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = 3 + }; + + _peerIdLabel = new Label("Peer ID: [Not Connected]") + { + X = 1, + Y = 0 + }; + + _multiAddrLabel = new Label("Multiaddr: [Not Connected]") + { + X = 1, + Y = 1 + }; + + _infoFrame.Add(_peerIdLabel, _multiAddrLabel); + + _tabView = new TabView() + { + X = 0, + Y = Pos.Bottom(_infoFrame), + Width = Dim.Fill(), + Height = Dim.Fill(3) + }; + + _chatTextView = new TextView() + { + ReadOnly = true, + WordWrap = true, + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + // Create peers list view + _peersListView = new ListView(new List()) + { + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + // Create logs text view + _logsTextView = new TextView() + { + ReadOnly = true, + WordWrap = true, + X = 0, + Y = 0, + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + // Add tabs to tab view + var chatTab = new TabView.Tab("Chat", _chatTextView); + var peersTab = new TabView.Tab("Peers", _peersListView); + var logsTab = new TabView.Tab("Logs", _logsTextView); + + _tabView.AddTab(chatTab, true); + _tabView.AddTab(peersTab, false); + _tabView.AddTab(logsTab, false); + + // Create input field and buttons + _inputField = new TextField("") + { + X = 0, + Y = Pos.Bottom(_tabView), + Width = Dim.Fill(12) + }; + + // Add support for Enter key to send message + _inputField.KeyPress += OnInputFieldKeyPress; + + _sendButton = new Button("Send") + { + X = Pos.Right(_inputField) + 1, + Y = Pos.Top(_inputField) + }; + + _exitButton = new Button("Exit") + { + X = Pos.Right(_sendButton) + 1, + Y = Pos.Top(_inputField) + }; + + // Wire up events + _sendButton.Clicked += OnSendButtonClicked; + _exitButton.Clicked += OnExitButtonClicked; + + // Add input field and buttons to main window + _mainWindow.Add(_infoFrame, _tabView, _inputField, _sendButton, _exitButton); + top.Add(_mainWindow); + + // Add global key binding: Ctrl+Q to exit + top.KeyPress += OnKeyPress; + + _isInitialized = true; + Console.WriteLine("Terminal UI initialized successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"Error initializing Terminal UI: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + throw; + } + } + + /// + public void Run() + { + if (!_isInitialized) + throw new InvalidOperationException("UI must be initialized before running"); + + try + { + Console.WriteLine("Starting Terminal.Gui application..."); + Terminal.Gui.Application.Run(); + Console.WriteLine("Terminal.Gui application ended"); + } + catch (Exception ex) + { + Console.WriteLine($"Error running Terminal UI: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + throw; + } + } + + /// + public void AddChatMessage(string message) + { + lock (_uiLock) + { + _chatHistory.Add(message); + + Terminal.Gui.Application.MainLoop?.Invoke(() => + { + if (_chatTextView != null) + { + _chatTextView.Text = string.Join("\n", _chatHistory); + ScrollToEnd(_chatTextView); + } + }); + } + } + + /// + public void AddChatMessage(ChatMessage chatMessage) + { + string displayName = string.IsNullOrEmpty(chatMessage.SenderNick) ? + (string.IsNullOrEmpty(chatMessage.SenderID) ? "Unknown" : chatMessage.SenderID) : + chatMessage.SenderNick; + + AddChatMessage($"{displayName}: {chatMessage.Message}"); + } + + /// + public void AddLog(string message) + { + lock (_uiLock) + { + _logHistory.Add(message); + + Terminal.Gui.Application.MainLoop?.Invoke(() => + { + if (_logsTextView != null) + { + _logsTextView.Text = string.Join("\n", _logHistory); + ScrollToEnd(_logsTextView); + } + }); + } + } + + /// + public void AddPeer(Peer peer) + { + lock (_uiLock) + { + string displayName = string.IsNullOrEmpty(peer.DisplayName) ? peer.ShortId : peer.DisplayName; + + if (!_peersHistory.Contains(displayName)) + { + _peersHistory.Add(displayName); + + Terminal.Gui.Application.MainLoop?.Invoke(() => + { + if (_peersListView != null) + { + _peersListView.SetSource(new List(_peersHistory)); + } + }); + } + } + } + + /// + public void UpdatePeerInfo(string peerId, string multiaddress) + { + Terminal.Gui.Application.MainLoop?.Invoke(() => + { + if (_peerIdLabel != null) + { + _peerIdLabel.Text = $"Peer ID: {peerId}"; + } + + if (_multiAddrLabel != null) + { + _multiAddrLabel.Text = $"Multiaddr: {multiaddress}"; + } + }); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes resources. + /// + /// Whether to dispose managed resources. + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + { + // Clean up managed resources + if (_inputField != null) + _inputField.KeyPress -= OnInputFieldKeyPress; + + if (_sendButton != null) + _sendButton.Clicked -= OnSendButtonClicked; + + if (_exitButton != null) + _exitButton.Clicked -= OnExitButtonClicked; + + var top = Terminal.Gui.Application.Top; + top.KeyPress -= OnKeyPress; + + Terminal.Gui.Application.Shutdown(); + } + + _isDisposed = true; + } + + private void OnSendButtonClicked() + { + try + { + string message = _inputField?.Text?.ToString() ?? string.Empty; + if (string.IsNullOrWhiteSpace(message)) + return; + + // Clear input field + _inputField!.Text = string.Empty; + + // Show message in chat + AddChatMessage($"[You]: {message}"); + + // Notify subscribers + MessageSent?.Invoke(this, message); + } + catch (Exception ex) + { + AddLog($"[Error] Send button error: {ex.Message}"); + } + } + + private void OnExitButtonClicked() + { + AddLog("[Info] Exit requested via button"); + ExitRequested?.Invoke(this, EventArgs.Empty); + } + + private void OnKeyPress(View.KeyEventEventArgs args) + { + if (args.KeyEvent.Key == (Key.Q | Key.CtrlMask)) + { + AddLog("[Info] Exit requested via Ctrl+Q"); + ExitRequested?.Invoke(this, EventArgs.Empty); + args.Handled = true; + } + } + + private void OnInputFieldKeyPress(View.KeyEventEventArgs args) + { + if (args.KeyEvent.Key == Key.Enter) + { + OnSendButtonClicked(); + args.Handled = true; + } + } + + private void ScrollToEnd(TextView textView) + { + if (textView == null || textView.Text == null) + return; + + // Count the number of lines by counting newline characters + string text = textView.Text.ToString() ?? string.Empty; + if (string.IsNullOrEmpty(text)) + return; + + int lineCount = text.Count(c => c == '\n') + 1; + + textView.CursorPosition = new Point(0, Math.Max(0, lineCount - 1)); + } +} \ No newline at end of file diff --git a/dotnet-peer/Libp2pChat/Program.cs b/dotnet-peer/Libp2pChat/Program.cs new file mode 100644 index 00000000..3d96870c --- /dev/null +++ b/dotnet-peer/Libp2pChat/Program.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Nethermind.Libp2p; +using Nethermind.Libp2p.Core; +using Nethermind.Libp2p.Protocols.Pubsub; +using Libp2pChat.Application.Interfaces; +using Libp2pChat.Application.Services; +using Libp2pChat.Infrastructure.Libp2p; +using Libp2pChat.Infrastructure.Logging; +using Libp2pChat.Presentation.UI; + +namespace Libp2pChat; + +/// +/// Entry point for the Libp2p Chat application. +/// +public class Program +{ + /// + /// Main entry point. + /// + /// Command-line arguments. + /// A task representing the asynchronous operation. + public static async Task Main(string[] args) + { + try + { + Console.WriteLine("Starting Libp2p Chat Application..."); + Console.WriteLine($"Console window size: {Console.WindowWidth}x{Console.WindowHeight}"); + Console.WriteLine($"OS: {Environment.OSVersion}"); + Console.WriteLine($".NET Version: {Environment.Version}"); + + // Set up dependency injection + var serviceProvider = ConfigureServices(); + + // Get the application service + var appService = serviceProvider.GetRequiredService(); + + // Handle application domain unhandled exceptions + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + var logger = serviceProvider.GetRequiredService>(); + logger.LogError(e.ExceptionObject as Exception, "Unhandled exception"); + + Console.WriteLine($"Unhandled exception: {e.ExceptionObject}"); + }; + + await appService.RunAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Fatal error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + } + } + + /// + /// Configures the dependency injection container. + /// + /// The configured service provider. + private static IServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + // Register UI first so it can be used by the logger + services.AddSingleton(); + + // Register libp2p services + services.AddLibp2p(builder => builder.WithPubsub()); + + // Register logging + services.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Debug) + .AddFilter("Nethermind.Libp2p", LogLevel.Debug) + .AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "[HH:mm:ss.fff] "; + }) + .AddUiLogger(); + }); + + // Register application services + services.AddSingleton(); + + // For backward compatibility + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + return new LoggerAdapter(logger); + }); + + // Register infrastructure services that require resolved dependencies + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var peerFactory = sp.GetRequiredService(); + var router = sp.GetRequiredService(); + + return new Libp2pChatService(logger, peerFactory, router); + }); + + // Configure logging provider to forward libp2p logs to our UI + services.Configure(options => + { + options.MinLevel = LogLevel.Debug; + options.AddFilter("Nethermind.Libp2p", LogLevel.Debug); + }); + + return services.BuildServiceProvider(); + } +} diff --git a/dotnet-peer/README.md b/dotnet-peer/README.md new file mode 100644 index 00000000..8c1f468a --- /dev/null +++ b/dotnet-peer/README.md @@ -0,0 +1,172 @@ +# .NET libp2p Chat Application + +![.NET libp2p](https://img.shields.io/badge/.NET%208.0-libp2p-blue) +![C# 12](https://img.shields.io/badge/C%23-12.0-brightgreen) + + +

+ .NET Peer Chat UI +
+ The .NET libp2p chat application Console UI +

+ +A simple chat application built on top of [dotnet-libp2p](https://github.com/NethermindEth/dotnet-libp2p). This project demonstrates a robust, console-based UI (using [Terminal.Gui](https://github.com/migueldeicaza/gui.cs)) to showcase basic peer-to-peer communication using Libp2p protocols. **Note:** This project is in beta and under active development. + +## Overview + +dotnet-libp2p Chat aims to provide a performant, well-tested implementation of a wide range of protocols that works on multiple platforms, with high throughput and a low memory profile. This chat application leverages Libp2p's PubSub to exchange messages between peers while also displaying vital information such as your peer ID and multiaddress so that you can easily share your connection details with others. + +### Key Features +- **Console UI:** A robust, multi-tab interface built with Terminal.Gui. +- **Peer Information:** Displays generated peer ID and multiaddress for connecting with others. +- **Messaging:** Supports JSON-formatted messages as well as plain text messages. +- **Protocol Integration:** Uses multiple Libp2p protocols for transport and discovery. +- **Thread-Safe Updates:** UI updates are marshaled on the main loop ensuring smooth and safe interactions. +## Requirements + +- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or newer +- Windows, macOS, or Linux operating system + +## Installation + +Clone the repository and navigate to the dotnet-peer directory: + +```bash +git clone https://github.com/libp2p/universal-connectivity +cd universal-connectivity/dotnet-peer/Libp2pChat +``` + +Build the application: + +```bash +dotnet build +``` + +## Usage + +Run the application with the default Console UI: + +```bash +dotnet run +``` + +For debug logging: + +```bash +dotnet run -- --trace +``` + +### UI + +#### 1. Terminal.Gui-based UI + +An alternate UI implementation using the Terminal.Gui library (a cross-platform console UI toolkit): +- Offers a more traditional GUI-like experience +- Provides tab-based navigation +- Includes a formal message input field with send button +- Handles window resize events + +### User Interface + +The application features a TUI (Terminal User Interface) with three main panels: + +1. **Chat Room**: The main panel where messages appear +2. **Peers List**: Shows connected peers with their shortened Peer IDs +3. **System Logs**: Displays all system events, connection details, and errors + +### Commands + +- Type a message and press Enter to send it to all connected peers +- Type `exit` to close the application +- Use arrow keys to navigate text input +- Resize the terminal window to see the responsive layout adjust + +## Connecting with Other Peers + +By default, the application listens on TCP port 9096. This setup enables direct connections with peers that support TCP-based transports, such as Go or Rust implementations. However, due to current transport limitations in dotnet-libp2p, connecting with browser-based JavaScript peers is not yet supported. + +### How to Connect: +1. **Note Your Peer ID:** + Your Peer ID is displayed in the **System logs** panel upon startup. +2. **Connect from Other Environments:** + - **Go/Rust Peers:** Update the hardcoded Peer ID in their configuration (e.g., in `main.go` for the Go peer) with your .NET peer's ID. + - **JavaScript Peers:** + - **Node.js Environment:** JavaScript peers running in Node.js can connect using TCP. + - **Browser Environments:** Direct connection is currently not possible because the required browser-friendly transports (e.g., WebSockets or WebRTC) are not implemented. + +### Way Forward: +- **Transport Enhancements:** + Future work is focused on implementing additional transport protocols (such as WebSockets and WebRTC) to enable connectivity with browser-based peers. +- **Community Contributions:** + Contributions to extend transport support are highly welcome. +- **Stay Updated:** + Follow the progress in [libp2p/universal-connectivity](https://github.com/libp2p/universal-connectivity) and our project roadmap for the latest updates on transport and protocol support. + + +## Architecture + +The application is built using: + +- **Nethermind.Libp2p**: The core libp2p implementation for .NET +- **Terminal.Gui**: TUI framework for .NET (optional implementation) +- **Spectre.Console**: Rich console output for colorful text and UI elements +- **Microsoft.Extensions.DependencyInjection**: For dependency injection and service configuration +- **System.Text.Json**: For message serialization/deserialization + +### Key Components + +- **ChatMessage**: Defines the shared message format used for communication between peers, ensuring compatibility with other implementations. +- **Program.cs**: Contains the entire application logic, including: + - **UI Management:** Implements the terminal interface using Terminal.Gui (handling chat, peers, and logs). + - **Libp2p Configuration:** Sets up peer identity, transport (TCP by default), and PubSub messaging. + - **Message Handling:** Manages serialization/deserialization of messages and processes both JSON and plain text messages. + + +### Project Structure + +``` +dotnet-peer/ +├── Libp2pChat/ +│ ├── Program.cs # Main application entry point +│ └── Libp2pChat.csproj # Project file with dependencies +└── README.md # This documentation +``` + +## Integration with Other libp2p Implementations + +This .NET peer is part of a larger universal connectivity demo that includes implementations in: + +- Go (`/go-peer`) +- Rust (`/rust-peer`) +- JavaScript (`/js-peer`) + +All implementations use compatible message formats and the GossipSub protocol for decentralized chat. + +## Troubleshooting + +### Connection Issues + +- Ensure firewalls allow TCP connections on port 9096 +- Verify the Peer ID is correctly shared between applications +- Check System logs panel for detailed error messages + +### UI Display Issues + +- Resize the terminal window if panels appear distorted +- Ensure your terminal supports UTF-8 for proper display of box characters +- For Windows users, consider using Windows Terminal for best results + +## Contributing + +Contributions are welcome! Here are some ways you can contribute: + +1. Report bugs and issues +2. Add new features or enhancements +3. Improve documentation +4. Submit pull requests + +Please follow the existing code style and include appropriate tests. + +## License + +This project is licensed under dual MIT/Apache-2.0 license - see the [LICENSE-MIT](../../LICENSE-MIT) and [LICENSE-APACHE](../../LICENSE-APACHE) files for details. diff --git a/dotnet-peer/logo.txt b/dotnet-peer/logo.txt new file mode 100644 index 00000000..a91f8c5b --- /dev/null +++ b/dotnet-peer/logo.txt @@ -0,0 +1,11 @@ + _____ ____ ________ ________ + | __ \ / __ \|__ __| | | ____| + | | | | | | | | | | | |__ _ __ __ + | | | | | | | | |_ | | __| '_ \ / _ \ + | |__| | |__| | | | |__| | |__| |_) | __/ + |_____/ \____/ |_|\____/|_____| .__/ \___| + ____ _ _ |_| + / ___| |__ __ _| |_ +| | | '_ \ / _` | __| +| |___| | | | (_| | |_ + \____|_| |_|\__,_|\__| \ No newline at end of file diff --git a/dotnet-peer/screenshots/dotnet-UI.png b/dotnet-peer/screenshots/dotnet-UI.png new file mode 100644 index 00000000..47674782 Binary files /dev/null and b/dotnet-peer/screenshots/dotnet-UI.png differ diff --git a/dotnet-peer/screenshots/screenshot.txt b/dotnet-peer/screenshots/screenshot.txt new file mode 100644 index 00000000..3c5e067d --- /dev/null +++ b/dotnet-peer/screenshots/screenshot.txt @@ -0,0 +1,26 @@ +┌────────────────────Room: universal-connectivity────────────────────┐┌──────────────Peers──────────────┐ +│ ││12D3KooW9z... │ +│14:33:29 libp2p-dotnet: Hello from .NET peer! ││QmW1bVU7... │ +│14:34:05 go-peer: Hi there, .NET peer! ││ │ +│14:34:12 libp2p-dotnet: This UI looks great ││ │ +│14:34:30 js-peer: Nice to see .NET in the libp2p network ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────────────┘└──────────────────────────────────┘ +┌───────────────────────────────────────System────────────────────────────────────────────────────────┐ +│14:30:05 Initializing libp2p chat application... │ +│14:30:06 Creating peer identity... │ +│14:30:06 Peer ID created: 12D3KooW9zAGanopseR2SpnY2WjJBKRxtkfHpLw2p9F3PYKuCFVSieF │ +│14:30:07 Binding to address: /ip4/0.0.0.0/tcp/9096/p2p/12D3KooW9zAGanopseR2SpnY2WjJBKRxtkfHpLw2p... │ +│14:30:08 Starting peer and PubSub... │ +│14:30:09 Peer started successfully │ +│14:30:09 Listening Addresses: │ +│14:30:09 /ip4/127.0.0.1/tcp/9096/p2p/12D3KooW9zAGanopseR2SpnY2WjJBKRxtkfHpLw2p9F3PYKuCFVSieF │ +│14:30:09 /ip4/192.168.1.5/tcp/9096/p2p/12D3KooW9zAGanopseR2SpnY2WjJBKRxtkfHpLw2p9F3PYKuCFVSieF │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────┘ +libp2p-dotnet > _ \ No newline at end of file