using EchoHub.Core.Models; namespace EchoHub.Client.Commands; public record CommandResult(bool Handled, string? Message = null, bool IsError = false); public class CommandHandler { public event Func? OnSetStatus; public event Func? OnSetNick; public event Func? OnSetColor; public event Func? OnSetTheme; public event Func? OnSendFile; public event Func? OnOpenProfile; public event Func? OnOpenServers; public event Func? OnJoinChannel; public event Func? OnLeaveChannel; public event Func? OnSetTopic; public event Func? OnListUsers; public event Func? OnSetAvatar; public event Func? OnKickUser; public event Func? OnBanUser; public event Func? OnUnbanUser; public event Func? OnMuteUser; public event Func? OnUnmuteUser; public event Func? OnAssignRole; public event Func? OnNukeChannel; public event Func? OnTestSound; public event Func? OnQuit; public event Func? OnHelp; public bool IsCommand(string input) => input.StartsWith('/'); public async Task HandleAsync(string input) { if (!IsCommand(input)) return new CommandResult(false); var parts = input[1..].Split(' ', 2, StringSplitOptions.TrimEntries); var command = parts[0].ToLowerInvariant(); var args = parts.Length > 1 ? parts[1] : string.Empty; return command switch { "status" => await HandleStatus(args), "nick" => await HandleNick(args), "color" => await HandleColor(args), "theme" => await HandleTheme(args), "send" => await HandleSend(args), "profile" => await HandleProfile(args), "avatar" => await HandleAvatar(args), "servers" => await HandleServers(), "join" => await HandleJoin(args), "leave" => await HandleLeave(), "topic" => await HandleTopic(args), "users" => await HandleUsers(), "kick" => await HandleKick(args), "ban" => await HandleBan(args), "unban" => await HandleUnban(args), "mute" => await HandleMute(args), "unmute" => await HandleUnmute(args), "role" => await HandleRole(args), "nuke" => await HandleNuke(), "test-sound" => await HandleTestSound(), "quit" or "exit" => await HandleQuit(), "help" or "?" => await HandleHelp(), _ => new CommandResult(true, $"Unknown command: /{command}. Type /help for available commands.", IsError: true), }; } private async Task HandleStatus(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /status or /status ", IsError: true); var statusArg = args.ToLowerInvariant().Trim(); UserStatus? status = statusArg switch { "online" => UserStatus.Online, "away" => UserStatus.Away, "dnd" or "donotdisturb" => UserStatus.DoNotDisturb, "invisible" => UserStatus.Invisible, _ => null, }; if (status.HasValue) { if (OnSetStatus is not null) await OnSetStatus(status.Value, null); return new CommandResult(true, $"Status set to {status.Value}"); } // Treat as custom status message (keep current status) if (OnSetStatus is not null) await OnSetStatus(UserStatus.Online, args); return new CommandResult(true, $"Status message set: {args}"); } private async Task HandleNick(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /nick ", IsError: true); if (OnSetNick is not null) await OnSetNick(args.Trim()); return new CommandResult(true, $"Display name set to: {args.Trim()}"); } private async Task HandleColor(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /color (e.g., /color #FF5733)", IsError: true); var color = args.Trim(); if (!color.StartsWith('#')) color = "#" + color; if (color.Length != 7 || !IsValidHex(color[1..])) return new CommandResult(true, "Invalid color. Use hex format: #RRGGBB", IsError: true); if (OnSetColor is not null) await OnSetColor(color); return new CommandResult(true, $"Nickname color set to: {color}"); } private async Task HandleTheme(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /theme (Default, Dark, Light, Hacker, Solarized)", IsError: true); if (OnSetTheme is not null) await OnSetTheme(args.Trim()); return new CommandResult(true, $"Theme switched to: {args.Trim()}"); } private async Task HandleSend(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /send [-s|-m|-l]", IsError: true); // Extract optional size flag from end or start, respecting quoted paths var (target, size) = ParsePathAndSizeFlag(args); if (string.IsNullOrWhiteSpace(target)) return new CommandResult(true, "Usage: /send [-s|-m|-l]", IsError: true); if (Uri.TryCreate(target, UriKind.Absolute, out var uri) && (uri.Scheme == "http" || uri.Scheme == "https")) { if (OnSendFile is not null) await OnSendFile(target, size); var fileName = Path.GetFileName(uri.LocalPath); if (string.IsNullOrWhiteSpace(fileName)) fileName = "image"; return new CommandResult(true, $"Sending: {fileName}..."); } if (!File.Exists(target)) return new CommandResult(true, $"File not found: {target}", IsError: true); if (OnSendFile is not null) await OnSendFile(target, size); return new CommandResult(true, $"Uploading: {Path.GetFileName(target)}..."); } private async Task HandleProfile(string args) { var username = string.IsNullOrWhiteSpace(args) ? null : args.Trim(); if (OnOpenProfile is not null) await OnOpenProfile(username); return new CommandResult(true); } private async Task HandleAvatar(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /avatar ", IsError: true); var target = StripQuotes(args.Trim()); if (OnSetAvatar is not null) await OnSetAvatar(target); return new CommandResult(true, "Uploading avatar..."); } private async Task HandleServers() { if (OnOpenServers is not null) await OnOpenServers(); return new CommandResult(true); } private async Task HandleJoin(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /join ", IsError: true); var channel = args.Trim().TrimStart('#'); if (OnJoinChannel is not null) await OnJoinChannel(channel); return new CommandResult(true); } private async Task HandleLeave() { if (OnLeaveChannel is not null) await OnLeaveChannel(); return new CommandResult(true); } private async Task HandleTopic(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /topic ", IsError: true); if (OnSetTopic is not null) await OnSetTopic(args.Trim()); return new CommandResult(true, $"Topic set to: {args.Trim()}"); } private async Task HandleUsers() { if (OnListUsers is not null) await OnListUsers(); return new CommandResult(true); } private async Task HandleQuit() { if (OnQuit is not null) await OnQuit(); return new CommandResult(true); } private async Task HandleKick(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /kick [reason]", IsError: true); var parts = args.Split(' ', 2, StringSplitOptions.TrimEntries); var username = parts[0]; var reason = parts.Length > 1 ? parts[1] : null; if (OnKickUser is not null) await OnKickUser(username, reason); return new CommandResult(true, $"Kicking {username}..."); } private async Task HandleBan(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /ban [reason]", IsError: true); var parts = args.Split(' ', 2, StringSplitOptions.TrimEntries); var username = parts[0]; var reason = parts.Length > 1 ? parts[1] : null; if (OnBanUser is not null) await OnBanUser(username, reason); return new CommandResult(true, $"Banning {username}..."); } private async Task HandleUnban(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /unban ", IsError: true); if (OnUnbanUser is not null) await OnUnbanUser(args.Trim()); return new CommandResult(true, $"Unbanning {args.Trim()}..."); } private async Task HandleMute(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /mute [duration_minutes]", IsError: true); var parts = args.Split(' ', 2, StringSplitOptions.TrimEntries); var username = parts[0]; int? duration = parts.Length > 1 && int.TryParse(parts[1], out var d) ? d : null; if (OnMuteUser is not null) await OnMuteUser(username, duration); return new CommandResult(true, $"Muting {username}..."); } private async Task HandleUnmute(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /unmute ", IsError: true); if (OnUnmuteUser is not null) await OnUnmuteUser(args.Trim()); return new CommandResult(true, $"Unmuting {args.Trim()}..."); } private async Task HandleRole(string args) { if (string.IsNullOrWhiteSpace(args)) return new CommandResult(true, "Usage: /role ", IsError: true); var parts = args.Split(' ', 2, StringSplitOptions.TrimEntries); if (parts.Length < 2) return new CommandResult(true, "Usage: /role ", IsError: true); var username = parts[0]; var role = parts[1].ToLowerInvariant(); if (role is not ("admin" or "mod" or "member")) return new CommandResult(true, "Invalid role. Use: admin, mod, or member", IsError: true); if (OnAssignRole is not null) await OnAssignRole(username, role); return new CommandResult(true, $"Setting {username} to {role}..."); } private async Task HandleNuke() { if (OnNukeChannel is not null) await OnNukeChannel(); return new CommandResult(true, "Nuking channel history..."); } private async Task HandleTestSound() { if (OnTestSound is not null) await OnTestSound(); return new CommandResult(true, "Playing notification sound..."); } private async Task HandleHelp() { if (OnHelp is not null) await OnHelp(); return new CommandResult(true, """ Available commands: /status - Set your status /status - Set status message /nick - Set display name /color <#hex> - Set nickname color /theme - Switch theme /send [-s|-m|-l] - Send file/image/audio (size flag for images) /avatar - Set your avatar /profile [username] - View a profile /servers - Open saved servers /join - Join a channel /leave - Leave current channel /topic - Set channel topic /users - List online users Moderation: /kick [reason] - Kick a user (Mod+) /ban [reason] - Ban a user (Admin+) /unban - Unban a user (Admin+) /mute [minutes] - Mute a user (Mod+) /unmute - Unmute a user (Mod+) /role - Assign role (Admin+) /nuke - Clear channel history (Mod+) /test-sound - Play notification sound /quit - Exit the app """); } /// /// Extract a file path (possibly quoted) and an optional size flag (-s, -m, -l). /// The flag can appear before or after the path. /// private static (string Path, string? Size) ParsePathAndSizeFlag(string args) { var trimmed = args.Trim(); string? size = null; // Check for flag at the end: "path" -m or path -m if (trimmed.Length > 3) { var suffix = trimmed[^2..]; if (suffix is "-s" or "-m" or "-l" && trimmed[^3] == ' ') { size = suffix[1..]; trimmed = trimmed[..^3].TrimEnd(); } } // Check for flag at the start: -m "path" or -m path if (size is null && trimmed.Length > 3) { var prefix = trimmed[..2]; if (prefix is "-s" or "-m" or "-l" && trimmed[2] == ' ') { size = prefix[1..]; trimmed = trimmed[3..].TrimStart(); } } return (StripQuotes(trimmed), size); } /// /// Remove matching surrounding quotes (double or single) from a string. /// private static string StripQuotes(string s) { if (s.Length >= 2 && ((s[0] == '"' && s[^1] == '"') || (s[0] == '\'' && s[^1] == '\''))) return s[1..^1]; return s; } private static bool IsValidHex(string s) => s.All(c => char.IsAsciiHexDigit(c)); }