mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
411 lines
15 KiB
C#
411 lines
15 KiB
C#
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<UserStatus, string?, Task>? OnSetStatus;
|
|
public event Func<string, Task>? OnSetNick;
|
|
public event Func<string, Task>? OnSetColor;
|
|
public event Func<string, Task>? OnSetTheme;
|
|
public event Func<string, string?, Task>? OnSendFile;
|
|
public event Func<string?, Task>? OnOpenProfile;
|
|
public event Func<Task>? OnOpenServers;
|
|
public event Func<string, Task>? OnJoinChannel;
|
|
public event Func<Task>? OnLeaveChannel;
|
|
public event Func<string, Task>? OnSetTopic;
|
|
public event Func<Task>? OnListUsers;
|
|
public event Func<string, Task>? OnSetAvatar;
|
|
public event Func<string, string?, Task>? OnKickUser;
|
|
public event Func<string, string?, Task>? OnBanUser;
|
|
public event Func<string, Task>? OnUnbanUser;
|
|
public event Func<string, int?, Task>? OnMuteUser;
|
|
public event Func<string, Task>? OnUnmuteUser;
|
|
public event Func<string, string, Task>? OnAssignRole;
|
|
public event Func<Task>? OnNukeChannel;
|
|
public event Func<Task>? OnTestSound;
|
|
public event Func<Task>? OnQuit;
|
|
public event Func<Task>? OnHelp;
|
|
|
|
public bool IsCommand(string input) => input.StartsWith('/');
|
|
|
|
public async Task<CommandResult> 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<CommandResult> HandleStatus(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /status <online|away|dnd|invisible> or /status <message>", 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<CommandResult> HandleNick(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /nick <display name>", IsError: true);
|
|
|
|
if (OnSetNick is not null)
|
|
await OnSetNick(args.Trim());
|
|
return new CommandResult(true, $"Display name set to: {args.Trim()}");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleColor(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /color <hex> (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<CommandResult> HandleTheme(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /theme <name> (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<CommandResult> HandleSend(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /send <filepath or URL> [-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 <filepath or URL> [-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<CommandResult> 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<CommandResult> HandleAvatar(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /avatar <URL or filepath>", IsError: true);
|
|
|
|
var target = StripQuotes(args.Trim());
|
|
|
|
if (OnSetAvatar is not null)
|
|
await OnSetAvatar(target);
|
|
return new CommandResult(true, "Uploading avatar...");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleServers()
|
|
{
|
|
if (OnOpenServers is not null)
|
|
await OnOpenServers();
|
|
return new CommandResult(true);
|
|
}
|
|
|
|
private async Task<CommandResult> HandleJoin(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /join <channel>", IsError: true);
|
|
|
|
var channel = args.Trim().TrimStart('#');
|
|
if (OnJoinChannel is not null)
|
|
await OnJoinChannel(channel);
|
|
return new CommandResult(true);
|
|
}
|
|
|
|
private async Task<CommandResult> HandleLeave()
|
|
{
|
|
if (OnLeaveChannel is not null)
|
|
await OnLeaveChannel();
|
|
return new CommandResult(true);
|
|
}
|
|
|
|
private async Task<CommandResult> HandleTopic(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /topic <text>", IsError: true);
|
|
|
|
if (OnSetTopic is not null)
|
|
await OnSetTopic(args.Trim());
|
|
return new CommandResult(true, $"Topic set to: {args.Trim()}");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleUsers()
|
|
{
|
|
if (OnListUsers is not null)
|
|
await OnListUsers();
|
|
return new CommandResult(true);
|
|
}
|
|
|
|
private async Task<CommandResult> HandleQuit()
|
|
{
|
|
if (OnQuit is not null)
|
|
await OnQuit();
|
|
return new CommandResult(true);
|
|
}
|
|
|
|
private async Task<CommandResult> HandleKick(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /kick <username> [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<CommandResult> HandleBan(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /ban <username> [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<CommandResult> HandleUnban(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /unban <username>", IsError: true);
|
|
|
|
if (OnUnbanUser is not null)
|
|
await OnUnbanUser(args.Trim());
|
|
return new CommandResult(true, $"Unbanning {args.Trim()}...");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleMute(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /mute <username> [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<CommandResult> HandleUnmute(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /unmute <username>", IsError: true);
|
|
|
|
if (OnUnmuteUser is not null)
|
|
await OnUnmuteUser(args.Trim());
|
|
return new CommandResult(true, $"Unmuting {args.Trim()}...");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleRole(string args)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(args))
|
|
return new CommandResult(true, "Usage: /role <username> <admin|mod|member>", IsError: true);
|
|
|
|
var parts = args.Split(' ', 2, StringSplitOptions.TrimEntries);
|
|
if (parts.Length < 2)
|
|
return new CommandResult(true, "Usage: /role <username> <admin|mod|member>", 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<CommandResult> HandleNuke()
|
|
{
|
|
if (OnNukeChannel is not null)
|
|
await OnNukeChannel();
|
|
return new CommandResult(true, "Nuking channel history...");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleTestSound()
|
|
{
|
|
if (OnTestSound is not null)
|
|
await OnTestSound();
|
|
return new CommandResult(true, "Playing notification sound...");
|
|
}
|
|
|
|
private async Task<CommandResult> HandleHelp()
|
|
{
|
|
if (OnHelp is not null)
|
|
await OnHelp();
|
|
return new CommandResult(true, """
|
|
Available commands:
|
|
/status <online|away|dnd|invisible> - Set your status
|
|
/status <message> - Set status message
|
|
/nick <name> - Set display name
|
|
/color <#hex> - Set nickname color
|
|
/theme <name> - Switch theme
|
|
/send <filepath or URL> [-s|-m|-l] - Send file/image/audio (size flag for images)
|
|
/avatar <URL or filepath> - Set your avatar
|
|
/profile [username] - View a profile
|
|
/servers - Open saved servers
|
|
/join <channel> - Join a channel
|
|
/leave - Leave current channel
|
|
/topic <text> - Set channel topic
|
|
/users - List online users
|
|
Moderation:
|
|
/kick <user> [reason] - Kick a user (Mod+)
|
|
/ban <user> [reason] - Ban a user (Admin+)
|
|
/unban <user> - Unban a user (Admin+)
|
|
/mute <user> [minutes] - Mute a user (Mod+)
|
|
/unmute <user> - Unmute a user (Mod+)
|
|
/role <user> <admin|mod|member> - Assign role (Admin+)
|
|
/nuke - Clear channel history (Mod+)
|
|
/test-sound - Play notification sound
|
|
/quit - Exit the app
|
|
""");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extract a file path (possibly quoted) and an optional size flag (-s, -m, -l).
|
|
/// The flag can appear before or after the path.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove matching surrounding quotes (double or single) from a string.
|
|
/// </summary>
|
|
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));
|
|
}
|