use std::
{
result,
fmt::
{
Display,
Formatter,
Result,
}
};
use tokio::net::tcp::OwnedWriteHalf;
use crate::
{
options,
role::Role,
network::
{
self,
codes::PacketCode,
},
};
#[cfg(feature = "client_screen")]
use crate::network::screen::client::
{
capture as screen_capture,
options as screen_options,
};
#[derive(Clone, PartialEq)]
pub enum Command
{
Exit, Logout, #[cfg(feature = "client_voice")] Voice, #[cfg(feature = "client_voice")] Mute, Channel, Help, Info, List, Files, #[cfg(feature = "client_screen")] Screens, Upload, Download, #[cfg(feature = "client_screen")] Screen, #[cfg(feature = "client_screen")] Attach, #[cfg(feature = "client_screen")] Deattach, PrivateMessage, Settings, Server, UsernameColor, MessageColor, Invalid, }
#[derive(Clone, PartialEq)]
pub enum Subcommand
{
Mute, Kick, Ban, BanIp, Bans, Pardon, PardonIp, Say, Role, Settings, }
#[derive(Clone, Copy, PartialEq)]
pub enum ArgValues
{
Free, Colors, Monitors, Roles, }
pub struct CommandArg {
pub name: &'static str,
pub description: &'static str,
pub required: bool,
pub values: ArgValues, }
pub struct SubcommandInfo {
pub subcommand: Subcommand,
pub triggers: &'static [&'static str],
pub minimal_role: Role,
pub args: &'static [CommandArg],
pub description: &'static str,
}
pub struct CommandInfo {
pub command: Command,
pub triggers: &'static [&'static str],
pub shortcut: Option<char>,
pub minimal_role: Role,
pub subcommands: &'static [SubcommandInfo], pub args: &'static [CommandArg],
pub description: &'static str,
}
pub const SERVER_SUBCOMMANDS: &[SubcommandInfo] =
&[
SubcommandInfo
{
subcommand: Subcommand::Mute,
triggers: &[ "MUTE", "SILENCE", "STFU" ],
minimal_role: Role::Moderator,
args:
&[
CommandArg
{
name: "ID",
description: "Target user",
required: true,
values: ArgValues::Free,
},
],
description: "Mutes a user server-side",
},
SubcommandInfo
{
subcommand: Subcommand::Kick,
triggers: &[ "KICK", "BOOT", "REMOVE" ],
minimal_role: Role::Moderator,
args:
&[
CommandArg
{
name: "ID",
description: "Target user",
required: true,
values: ArgValues::Free,
},
],
description: "Disconnects a user from the server",
},
SubcommandInfo
{
subcommand: Subcommand::Ban,
triggers: &[ "BAN", "DISABLE", "KILL" ],
minimal_role: Role::Owner,
args:
&[
CommandArg
{
name: "ID",
description: "Target user",
required: true,
values: ArgValues::Free,
},
],
description: "Bans a user from the server",
},
SubcommandInfo
{
subcommand: Subcommand::BanIp,
triggers: &[ "BANIP", "DISABLEIP", "BLOCKIP" ],
minimal_role: Role::Owner,
args:
&[
CommandArg
{
name: "ID",
description: "Target user",
required: true,
values: ArgValues::Free,
},
],
description: "Bans a user's IP from the server",
},
SubcommandInfo
{
subcommand: Subcommand::Bans,
triggers: &[ "BANLIST", "BANS", "BANNED", "DISABLED", "BLOCKED" ],
minimal_role: Role::Owner,
args: &[],
description: "Lists every ban",
},
SubcommandInfo
{
subcommand: Subcommand::Pardon,
triggers: &[ "PARDON", "UNBAN", "FORGIVE", "UNBLOCK" ],
minimal_role: Role::Owner,
args:
&[
CommandArg
{
name: "ID",
description: "Banned user ID",
required: true,
values: ArgValues::Free,
},
],
description: "Removes a user ban",
},
SubcommandInfo
{
subcommand: Subcommand::PardonIp,
triggers: &[ "PARDONIP", "UNBANIP", "FORGIVEIP", "UNBLOCKIP" ],
minimal_role: Role::Owner,
args:
&[
CommandArg
{
name: "ID",
description: "Banned address ID",
required: true,
values: ArgValues::Free,
},
],
description: "Removes an IP ban",
},
SubcommandInfo
{
subcommand: Subcommand::Say,
triggers: &[ "SAY", "ECHO", "BROADCAST", "NOTICE", "MESSAGE" ],
minimal_role: Role::Owner,
args:
&[
CommandArg
{
name: "MESSAGE",
description: "Message to broadcast",
required: true,
values: ArgValues::Free,
},
],
description: "Broadcasts message as server",
},
SubcommandInfo
{
subcommand: Subcommand::Role,
triggers: &[ "ROLE", "RANK", "PROMOTE", "DEMOTE" ],
minimal_role: Role::Owner,
args:
&[
CommandArg
{
name: "ID",
description: "Target user",
required: true,
values: ArgValues::Free,
},
CommandArg
{
name: "ROLE",
description: "Role to grant",
required: true,
values: ArgValues::Roles,
},
],
description: "Sets a user's role on the server",
},
SubcommandInfo
{
subcommand: Subcommand::Settings,
triggers: &[ "SETTINGS", "CONFIG", "SETUP" ],
minimal_role: Role::Owner,
args: &[],
description: "Opens the server configuration",
},
];
pub const COMMAND_LIST: &[CommandInfo] =
&[
CommandInfo
{
command: Command::Help,
triggers: &[ "HELP", "H", "COMMANDS", "USAGE", "GUIDE" ],
shortcut: Some('h'),
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Prints all available commands",
},
CommandInfo
{
command: Command::Info,
triggers: &[ "INFO", "COMMAND", "MAN" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "COMMAND",
description: "Target command",
required: true,
values: ArgValues::Free,
},
],
description: "Shows command info",
},
#[cfg(feature = "client_voice")]
CommandInfo
{
command: Command::Voice,
triggers: &[ "VOICE", "VOIP", "CALL" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Toggles voice chat",
},
#[cfg(feature = "client_voice")]
CommandInfo
{
command: Command::Mute,
triggers: &[ "MUTE", "UNMUTE", "SILENCE", "STFU" ],
shortcut: Some('s'),
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "ID",
description: "ID of target user",
required: false,
values: ArgValues::Free,
},
],
description: "Toggle-mutes user/yourself",
},
CommandInfo
{
command: Command::Channel,
triggers: &[ "CHANNEL", "SWITCH", "CHECKOUT", "AREA" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "NAME",
description: "Name of channel",
required: false,
values: ArgValues::Free,
},
],
description: "Switches to channel/lobby if NAME is omitted",
},
CommandInfo
{
command: Command::Upload,
triggers: &[ "UPLOAD", "FILEUP", "PUSH", "UP" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "PATH",
description: "Path of target file",
required: true,
values: ArgValues::Free,
},
],
description: "Uploads file to server",
},
CommandInfo
{
command: Command::Download,
triggers: &[ "DOWNLOAD", "FILEDOWN", "PULL", "DOWN", "FETCH" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "USER ID",
description: "ID of uploader",
required: true,
values: ArgValues::Free,
},
CommandArg
{
name: "FILE ID",
description: "ID of target file",
required: true,
values: ArgValues::Free,
},
],
description: "Downloads file from server",
},
#[cfg(feature = "client_screen")]
CommandInfo
{
command: Command::Screen,
triggers: &[ "SCREEN", "SCREENSHARE", "PRESENTATION", "SHARE" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "MONITOR",
description: "Index or name of the monitor to share",
required: false,
values: ArgValues::Monitors,
},
],
description: "Toggles screensharing, or swaps the shared monitor while it runs",
},
#[cfg(feature = "client_screen")]
CommandInfo
{
command: Command::Attach,
triggers: &[ "ATTACH", "WATCH", "DISPLAY", "JOIN" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "ID",
description: "ID of screensharing user",
required: true,
values: ArgValues::Free,
},
],
description: "Attaches client screenshare.",
},
#[cfg(feature = "client_screen")]
CommandInfo
{
command: Command::Deattach,
triggers: &[ "DEATTACH", "STOP", "CLOSE" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Dettaches client screenshare.",
},
CommandInfo
{
command: Command::List,
triggers: &[ "LIST", "USERS", "CLIENTS", "CHANNELS", "IDS", "ID" ],
shortcut: Some('l'),
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Shows connected users and their IDs",
},
CommandInfo
{
command: Command::Files,
triggers: &[ "FILES", "LISTFILES", "UPLOADS", "DOWNLOADS" ],
shortcut: Some('u'),
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Shows available files and their IDs",
},
#[cfg(feature = "client_screen")]
CommandInfo
{
command: Command::Screens,
triggers: &[ "SCREENS", "LISTSCREENS", "SCREENSHARES", "SHARES" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Shows all screensharing clients.",
},
CommandInfo
{
command: Command::PrivateMessage,
triggers: &[ "PM", "DM", "MSG", "TELL" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "ID",
description: "ID of target user",
required: true,
values: ArgValues::Free,
},
CommandArg
{
name: "MESSAGE",
description: "Message content",
required: true,
values: ArgValues::Free,
},
],
description: "Sends private message",
},
CommandInfo
{
command: Command::Settings,
triggers: &[ "SETTINGS", "SETUP", "CONFIG", "PREFERENCES", "AUDIO" ],
shortcut: Some(','),
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Opens audio and interface settings",
},
CommandInfo
{
command: Command::UsernameColor,
triggers: &[ "UCOLOR", "USERNAME" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "COLOR",
description: "Target color",
required: true,
values: ArgValues::Colors,
},
],
description: "Sets color of username",
},
CommandInfo
{
command: Command::MessageColor,
triggers: &[ "COLOR", "MESSAGE" ],
shortcut: None,
minimal_role: Role::User,
subcommands: &[],
args:
&[
CommandArg
{
name: "COLOR",
description: "Target color",
required: true,
values: ArgValues::Colors,
},
],
description: "Sets color of message",
},
CommandInfo
{
command: Command::Server,
triggers: &[ "SERVER", "ADMIN", "MOD" ],
shortcut: None,
minimal_role: Role::Moderator,
subcommands: SERVER_SUBCOMMANDS,
args:
&[
CommandArg
{
name: "ACTION",
description: "Moderation action",
required: true,
values: ArgValues::Free,
},
],
description: "Moderation actions",
},
CommandInfo
{
command: Command::Logout,
triggers: &[ "LOGOUT", "SIGNOUT", "SWITCH" ],
shortcut: Some('o'),
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Disconnects from the server and returns to the login screen",
},
CommandInfo
{
command: Command::Exit,
triggers: &[ "EXIT", "LEAVE", "QUIT", "DISCONNECT" ],
shortcut: Some('c'),
minimal_role: Role::User,
subcommands: &[],
args: &[],
description: "Disconnects from the server",
},
];
pub const COMMAND_PREFIX: &str = "/";
impl CommandInfo
{
pub fn available(&self, role: Role) -> bool
{
role >= self.minimal_role && (self.subcommands.is_empty() || self.actions(role).next().is_some())
}
pub fn actions(&self, role: Role) -> impl Iterator<Item = &'static SubcommandInfo> {
self.subcommands.iter().filter(move |sub| sub.available(role))
}
pub fn action(&self, word: &str) -> Option<&'static SubcommandInfo> {
self.subcommands.iter().find(|sub| sub.triggers.iter().any(|t| t.eq_ignore_ascii_case(word)))
}
}
impl SubcommandInfo
{
pub fn available(&self, role: Role) -> bool { role >= self.minimal_role }
pub fn takes_id(&self) -> bool
{
matches!(self.subcommand, Subcommand::Mute
| Subcommand::Kick
| Subcommand::Ban
| Subcommand::BanIp
| Subcommand::Pardon
| Subcommand::PardonIp)
}
}
impl Command
{
pub fn build_message(&self, parameters: Option<&str>) -> Option<result::Result<PacketCode, ()>>
{
match self
{
Command::PrivateMessage =>
{
let parsed = parameters
.and_then(|p| p.split_once(' '))
.and_then(|(id, text)| Some((id.parse::<usize>().ok()?, text.to_string())));
Some(match parsed
{
Some((id, text)) => Ok(PacketCode::PrivateMessage { id, text, username: None }),
None => Err(()),
})
},
Command::Download =>
{
let parsed = parameters
.and_then(|p| p.split_once(' '))
.and_then(|(uid, fid)| Some((uid.parse::<usize>().ok()?, fid.parse::<usize>().ok()?)));
Some(match parsed
{
Some((id, file_id)) => Ok(PacketCode::Download { id: Some(id), file_id: Some(file_id), token: None }),
None => Err(()),
})
},
#[cfg(feature = "client_screen")]
Command::Attach =>
{
let target_id = parameters.and_then(|p| p.parse::<usize>().ok());
Some(match target_id
{
Some(id) => Ok(PacketCode::Attach { id: Some(id), token: None, username: None }), None => Err(()),
})
},
Command::Channel => Some(Ok(PacketCode::Channel { channel: parameters.map(str::to_string) })),
Command::List => Some(Ok(PacketCode::List { users: None })),
Command::Files => Some(Ok(PacketCode::Files { users: None })),
#[cfg(feature = "client_screen")]
Command::Screen =>
{
let sharing = screen_options::get_use_screen();
let Some(selection) = parameters.map(str::trim).filter(|m| !m.is_empty()) else
{
if !sharing { screen_options::set_monitor(None); }
return Some(Ok(PacketCode::Screen { token: None }));
};
let Ok(monitor) = screen_capture::resolve_monitor(selection) else { return Some(Err(())) };
if sharing && screen_capture::current_monitor().is_some_and(|current| current == monitor)
{
return Some(Ok(PacketCode::Screen { token: None }));
}
screen_options::set_monitor(Some(monitor));
if sharing { return None; }
Some(Ok(PacketCode::Screen { token: None }))
},
#[cfg(feature = "client_screen")] Command::Deattach => Some(Ok(PacketCode::Deattach { username: None } )),
#[cfg(feature = "client_screen")] Command::Screens => Some(Ok(PacketCode::Screens { users: None })),
Command::Exit | Command::Logout => Some(Ok(PacketCode::Disconnect)),
#[cfg(feature = "client_voice")] Command::Voice => Some(Ok(PacketCode::Voice { token: None })),
_ => None,
}
}
}
impl Display for Command
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result
{
let name = COMMAND_LIST.iter()
.find(|info| info.command == *self)
.map(|info| info.triggers[0].to_lowercase())
.unwrap_or_default();
write!(f, "{}{}", COMMAND_PREFIX, name)
}
}
pub fn get_command(input: &str) -> (Option<Command>, Option<String>) {
if !input.starts_with(COMMAND_PREFIX) { return (None, None); }
let no_prefix = &input[COMMAND_PREFIX.len()..]; let (command, parameters) = match no_prefix.split_once(' ') {
Some((command, parameters)) => (command.to_ascii_uppercase(), Some(parameters.trim().to_string())),
None => (no_prefix.to_ascii_uppercase(), None)
};
for info in COMMAND_LIST
{
if info.triggers.contains(&command.as_str())
{
return (Some(info.command.clone()), parameters);
}
}
(Some(Command::Invalid), None)
}
pub async fn send_command_code(write_stream: &mut OwnedWriteHalf, command: &Command, parameters: &Option<String>) -> Option<bool> {
match command.build_message(parameters.as_deref())?
{
Ok(message) =>
{
network::send(write_stream, message, options::get_keys().as_ref()).await;
Some(true)
},
Err(()) => Some(false),
}
}