using EchoHub.Core.Contracts; using EchoHub.Core.DTOs; namespace EchoHub.Server.Irc; public class IrcBroadcaster : IChatBroadcaster { private readonly IrcGatewayService _gateway; private readonly IMessageEncryptionService _encryption; public IrcBroadcaster(IrcGatewayService gateway, IMessageEncryptionService encryption) { _gateway = gateway; _encryption = encryption; } public async Task SendMessageToChannelAsync(string channelName, MessageDto message, string? excludeConnectionId = null) { // Decrypt transport-encrypted content for IRC clients (they can't handle // app-layer encryption). E2E room ciphertext ($RC1$) passes through untouched. var decryptedMessage = message with { Content = _encryption.Decrypt(message.Content) }; var lines = IrcMessageFormatter.FormatMessage(decryptedMessage, _gateway.Options.PublicBaseUrl); foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { // IRC convention: don't echo a message back to the connection that sent it // (its client already displayed it locally). Match by connection id, not // nickname — the same account may also be online via the TUI or a second // IRC client, and those sessions must still receive the message. if (conn.ConnectionId == excludeConnectionId) continue; foreach (var line in lines) await conn.SendAsync(line); } } public async Task SendUserJoinedAsync(string channelName, string username, UserPresenceDto? presence, string? excludeConnectionId = null) { foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { if (conn.ConnectionId == excludeConnectionId) continue; await conn.SendAsync($":{username}!{username}@echohub JOIN #{channelName}"); } } public async Task SendUserLeftAsync(string channelName, string username) { foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { if (conn.Nickname == username) continue; await conn.SendAsync($":{username}!{username}@echohub PART #{channelName}"); } } public async Task SendChannelUpdatedAsync(ChannelDto channel, string? channelName = null) { var target = channelName ?? channel.Name; if (channel.Topic is null) return; foreach (var conn in _gateway.GetConnectionsInChannel(target)) { await conn.SendAsync($":{_gateway.Options.ServerName} TOPIC #{channel.Name} :{channel.Topic}"); } } public Task SendUserStatusChangedAsync(List channelNames, UserPresenceDto presence) { // IRC has no active status broadcast. Clients discover away via WHOIS/WHO. return Task.CompletedTask; } public async Task SendUserKickedAsync(string channelName, string username, string? reason) { var reasonText = reason is not null ? $" :{reason}" : ""; foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { await conn.SendAsync($":{_gateway.Options.ServerName} KICK #{channelName} {username}{reasonText}"); } } public async Task SendUserBannedAsync(string username, string? reason) { var reasonText = reason ?? "You have been banned."; foreach (var conn in _gateway.GetAllConnections()) { if (conn.Nickname == username) await conn.SendAsync($":{_gateway.Options.ServerName} NOTICE {username} :You have been banned: {reasonText}"); } } public async Task SendMessageDeletedAsync(string channelName, Guid messageId) { foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { await conn.SendAsync($":{_gateway.Options.ServerName} NOTICE {conn.Nickname ?? "*"} :Message {messageId} was deleted in #{channelName}"); } } public async Task SendChannelDeletedAsync(string channelName) { foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { await conn.SendAsync($":{_gateway.Options.ServerName} NOTICE {conn.Nickname ?? "*"} :Channel #{channelName} has been deleted"); } } public async Task SendChannelNukedAsync(string channelName) { foreach (var conn in _gateway.GetConnectionsInChannel(channelName)) { await conn.SendAsync($":{_gateway.Options.ServerName} NOTICE {conn.Nickname ?? "*"} :All messages in #{channelName} have been cleared"); } } public async Task SendErrorAsync(string connectionId, string message) { if (!connectionId.StartsWith("irc-")) return; if (_gateway.Connections.TryGetValue(connectionId, out var conn)) { await conn.SendAsync($":{_gateway.Options.ServerName} NOTICE {conn.Nickname ?? "*"} :{message}"); } } public async Task ForceDisconnectUserAsync(List connectionIds, string reason) { foreach (var connId in connectionIds) { if (!connId.StartsWith("irc-")) continue; if (_gateway.Connections.TryGetValue(connId, out var conn)) { try { await conn.SendAsync($"ERROR :Closing Link: {reason}"); await conn.DisposeAsync(); } catch { /* connection may already be closed */ } } } } }