From 257bc55b754044292aa596101a30682559ff805c Mon Sep 17 00:00:00 2001 From: yourfriendoss Date: Sun, 14 Sep 2025 14:59:16 +0300 Subject: [PATCH] feat: add dms, hieracthical arguments, permission and rank system --- migrations/20250914092514_rank.down.sql | 4 + migrations/20250914092514_rank.up.sql | 3 + src/client.rs | 4 + src/commands/argument.rs | 173 ++++++++++++----------- src/commands/eco/balance.rs | 35 +++-- src/commands/eco/cf.rs | 17 +-- src/commands/eco/farm.rs | 10 +- src/commands/eco/fish.rs | 16 +-- src/commands/eco/inv.rs | 28 ++-- src/commands/eco/shop.rs | 11 +- src/commands/midi/play.rs | 4 +- src/commands/midi/playlist.rs | 4 +- src/commands/midi/queue.rs | 3 +- src/commands/midi/skip.rs | 3 +- src/commands/midi/stop.rs | 3 +- src/commands/mod.rs | 3 +- src/commands/system/about.rs | 4 +- src/commands/system/follow.rs | 5 +- src/commands/system/help.rs | 11 +- src/commands/system/launch.rs | 9 +- src/commands/system/mod.rs | 2 +- src/commands/system/rank.rs | 177 ++++++++++++++++++++++++ src/commands/system/test.rs | 3 +- src/commands/system/translate.rs | 17 ++- src/main.rs | 163 +++++++++++++++++----- 25 files changed, 503 insertions(+), 209 deletions(-) create mode 100644 migrations/20250914092514_rank.down.sql create mode 100644 migrations/20250914092514_rank.up.sql create mode 100644 src/commands/system/rank.rs diff --git a/migrations/20250914092514_rank.down.sql b/migrations/20250914092514_rank.down.sql new file mode 100644 index 0000000..5135f65 --- /dev/null +++ b/migrations/20250914092514_rank.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here +ALTER TABLE users + DROP COLUMN rank, + DROP COLUMN "extra_permissions"; diff --git a/migrations/20250914092514_rank.up.sql b/migrations/20250914092514_rank.up.sql new file mode 100644 index 0000000..63b7913 --- /dev/null +++ b/migrations/20250914092514_rank.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE users + ADD COLUMN rank TEXT NOT NULL DEFAULT 'user', + ADD COLUMN "extra_permissions" JSON NOT NULL DEFAULT '[]'; diff --git a/src/client.rs b/src/client.rs index dfdfae7..7fa66bf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -471,6 +471,10 @@ impl Client { self.send_command(json!({"m": "userset", "set": set})).await; } + pub async fn dm>(&self, message: S, _id: S) { + self.send_command(json!({"m": "dm", "_id": _id.as_ref(), "message": message.as_ref()})) + .await; + } pub async fn channelset(&self, settings: ChannelSettings) { self.send_command(json!({"m": "chset", "set": settings})) .await; diff --git a/src/commands/argument.rs b/src/commands/argument.rs index 84379d8..b781654 100644 --- a/src/commands/argument.rs +++ b/src/commands/argument.rs @@ -29,6 +29,7 @@ pub struct ArgumentSpec { pub arg_type: ArgumentType, pub required: bool, pub default: Option<&'static str>, + pub children: &'static [ArgumentSpec], } #[derive(Debug, Clone)] @@ -60,16 +61,16 @@ pub fn parse_argument(arg: &str, spec: &ArgumentSpec) -> Result arg .parse::() .map(ParsedArgument::Integer) - .map_err(|_| format!("Expected integer for '{}', got '{}'", spec.name, arg)), + .map_err(|_| format!("Argument '{}' must be an integer, got '{}'", spec.name, arg)), ArgumentType::Float => arg .parse::() .map(ParsedArgument::Float) - .map_err(|_| format!("Expected float for '{}', got '{}'", spec.name, arg)), + .map_err(|_| format!("Argument '{}' must be a float, got '{}'", spec.name, arg)), ArgumentType::Boolean => match arg.to_lowercase().as_str() { "true" | "yes" | "1" => Ok(ParsedArgument::Boolean(true)), "false" | "no" | "0" => Ok(ParsedArgument::Boolean(false)), _ => Err(format!( - "Expected boolean for '{}', got '{}'", + "Argument '{}' must be a boolean, got '{}'", spec.name, arg )), }, @@ -99,90 +100,102 @@ impl ParsedArguments { } } +fn token_matches_spec(token: &str, spec: &ArgumentSpec) -> bool { + match &spec.arg_type { + ArgumentType::String => true, + ArgumentType::Integer => token.parse::().is_ok(), + ArgumentType::Float => token.parse::().is_ok(), + ArgumentType::Boolean => { + matches!( + token.to_lowercase().as_str(), + "true" | "yes" | "1" | "false" | "no" | "0" + ) + } + ArgumentType::Enum(variants) => variants.contains(&token), + ArgumentType::GreedyString => true, + } +} + +fn parse_spec_chain( + specs: &[ArgumentSpec], + raw_args: &[String], + parsed: &mut HashMap<&'static str, ParsedArgument>, + depth: usize, +) -> Result { + if depth > 10 { + return Err("Too many nested arguments (depth > 10)".into()); + } + + let mut consumed: usize = 0; + + for spec in specs { + if let ArgumentType::GreedyString = spec.arg_type { + if consumed >= raw_args.len() { + if let Some(def) = spec.default { + let parsed_arg = parse_argument(def, spec)?; + parsed.insert(spec.name, parsed_arg); + } else if spec.required { + return Err(format!("Missing required argument '{}'", spec.name)); + } + } else { + let rest = raw_args[consumed..].join(" "); + let parsed_arg = parse_argument(rest.as_str(), spec)?; + parsed.insert(spec.name, parsed_arg); + consumed = raw_args.len(); + } + break; + } + + if consumed < raw_args.len() { + let next = &raw_args[consumed]; + if token_matches_spec(next, spec) { + let parsed_arg = parse_argument(next.as_str(), spec)?; + parsed.insert(spec.name, parsed_arg); + consumed += 1; + + if !spec.children.is_empty() { + let used = + parse_spec_chain(spec.children, &raw_args[consumed..], parsed, depth + 1)?; + consumed += used; + } + } else if let Some(def) = spec.default { + let parsed_arg = parse_argument(def, spec)?; + parsed.insert(spec.name, parsed_arg); + + if !spec.children.is_empty() { + let used = + parse_spec_chain(spec.children, &raw_args[consumed..], parsed, depth + 1)?; + consumed += used; + } + } else if spec.required { + return Err(format!("Missing required argument '{}'", spec.name)); + } + } else if let Some(def) = spec.default { + let parsed_arg = parse_argument(def, spec)?; + parsed.insert(spec.name, parsed_arg); + + if !spec.children.is_empty() { + let used = + parse_spec_chain(spec.children, &raw_args[consumed..], parsed, depth + 1)?; + consumed += used; + } + } else if spec.required { + return Err(format!("Missing required argument '{}'", spec.name)); + } + } + + Ok(consumed) +} + pub fn parse_arguments( specs: &[ArgumentSpec], raw_args: &[String], ) -> Result { let mut parsed = HashMap::new(); - let raw_vec = raw_args.to_vec(); - - let mut i = 0; - while i < specs.len() { - let spec = &specs[i]; - - let raw_value = match &spec.arg_type { - ArgumentType::GreedyString => { - if i >= raw_args.len() { - spec.default.map(|s| s.to_string()) - } else { - Some(raw_args[i..].join(" ")) - } - } - _ => raw_args - .get(i) - .cloned() - .or_else(|| spec.default.map(|s| s.to_string())), - }; - - match raw_value { - Some(ref value) => { - let parsed_arg = match &spec.arg_type { - ArgumentType::String => ParsedArgument::String(value.clone()), - ArgumentType::Integer => value - .parse::() - .map(ParsedArgument::Integer) - .map_err(|_| { - format!( - "Argument '{}' must be an integer, got '{}'", - spec.name, value - ) - })?, - ArgumentType::Float => value - .parse::() - .map(ParsedArgument::Float) - .map_err(|_| { - format!("Argument '{}' must be a float, got '{}'", spec.name, value) - })?, - ArgumentType::Boolean => match value.to_lowercase().as_str() { - "true" | "yes" | "1" => ParsedArgument::Boolean(true), - "false" | "no" | "0" => ParsedArgument::Boolean(false), - _ => { - return Err(format!( - "Argument '{}' must be a boolean, got '{}'", - spec.name, value - )); - } - }, - ArgumentType::Enum(variants) => { - if variants.contains(&value.as_str()) { - ParsedArgument::Enum(value.clone()) - } else { - return Err(format!( - "Argument '{}' must be one of {:?}, got '{}'", - spec.name, variants, value - )); - } - } - ArgumentType::GreedyString => ParsedArgument::GreedyString(value.clone()), - }; - - parsed.insert(spec.name, parsed_arg); - - if let ArgumentType::GreedyString = spec.arg_type { - break; - } - } - None if spec.required => { - return Err(format!("Missing required argument '{}'", spec.name)); - } - None => {} - } - - i += 1; - } + let _ = parse_spec_chain(specs, raw_args, &mut parsed, 0)?; Ok(ParsedArguments { - raw: raw_vec, + raw: raw_args.to_vec(), parsed, }) } diff --git a/src/commands/eco/balance.rs b/src/commands/eco/balance.rs index 37752df..f6e2e92 100644 --- a/src/commands/eco/balance.rs +++ b/src/commands/eco/balance.rs @@ -41,27 +41,40 @@ impl Command for BalanceCommand { arg_type: ArgumentType::String, required: false, default: None, + children: &[], }] } async fn constructed(&mut self, _: Client) {} async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { - let id = match args.get("user_id") { - Some(ParsedArgument::String(s)) => s.as_str(), - _ => player._id.as_str(), + async fn execute( + &mut self, + client: Client, + player: Player, + args: ParsedArguments, + command_user: User, + ) { + let user = match args.get("user_id") { + Some(ParsedArgument::String(s)) => { + let id = s.as_str(); + sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap() + } + _ => Some(command_user), }; - let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") - .bind(id) - .fetch_optional(self.pool.as_ref()) - .await - .unwrap(); - match user { Some(user) => { + let mut who: String = "You have".to_string(); + if user._id != player._id.as_str() { + who = format!("{} has ", player._id); + } + client - .message(format!("You have {} coins.", user.balance)) + .message(format!("{} {} coins.", who, user.balance)) .await; } None => { diff --git a/src/commands/eco/cf.rs b/src/commands/eco/cf.rs index 0ebd12b..378c605 100644 --- a/src/commands/eco/cf.rs +++ b/src/commands/eco/cf.rs @@ -47,29 +47,16 @@ impl Command for CoinflipCommand { arg_type: ArgumentType::Integer, required: false, default: None, + children: &[], }] } - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, mut user: User) { let bet_amount: Option = match args.get("bet_amount") { Some(ParsedArgument::Integer(i)) => Some(*i as i32), _ => None, }; - let user_opt = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") - .bind(&player._id) - .fetch_optional(self.pool.as_ref()) - .await - .unwrap(); - - let mut user = match user_opt { - Some(u) => u, - None => { - client.message("No user found.").await; - return; - } - }; - let choice = if rand::rng().random_bool(0.5) { "Heads" } else { diff --git a/src/commands/eco/farm.rs b/src/commands/eco/farm.rs index 0a32f91..f30daf6 100644 --- a/src/commands/eco/farm.rs +++ b/src/commands/eco/farm.rs @@ -272,6 +272,7 @@ impl Command for FarmCommand { arg_type: ArgumentType::Enum(&["sell", "all", "harvest"]), required: false, default: None, + children: &[], }] } @@ -384,14 +385,7 @@ impl Command for FarmCommand { } async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { - let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") - .bind(&player._id) - .fetch_optional(self.pool.as_ref()) - .await - .unwrap() - .unwrap(); - + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, mut user: User) { let command = match args.get("action") { Some(ParsedArgument::Enum(s)) => s.as_str(), _ => "", diff --git a/src/commands/eco/fish.rs b/src/commands/eco/fish.rs index adb8664..404e091 100644 --- a/src/commands/eco/fish.rs +++ b/src/commands/eco/fish.rs @@ -185,18 +185,18 @@ impl Command for FishCommand { arg_type: ArgumentType::String, required: false, default: None, + children: &[], }] } async fn constructed(&mut self, _: Client) {} async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { - let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") - .bind(&player._id) - .fetch_optional(self.pool.as_ref()) - .await - .unwrap() - .unwrap(); - + async fn execute( + &mut self, + client: Client, + player: Player, + args: ParsedArguments, + mut user: User, + ) { let action = match args.get("action") { Some(ParsedArgument::String(s)) => s.as_str(), _ => "", diff --git a/src/commands/eco/inv.rs b/src/commands/eco/inv.rs index e2dee87..a555695 100644 --- a/src/commands/eco/inv.rs +++ b/src/commands/eco/inv.rs @@ -43,24 +43,32 @@ impl Command for InventoryCommand { arg_type: ArgumentType::String, required: false, default: None, + children: &[], }] } async fn constructed(&mut self, _: Client) {} async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { - let id = match args.get("user_id") { - Some(ParsedArgument::String(s)) => s.as_str(), - _ => player._id.as_str(), + async fn execute( + &mut self, + client: Client, + _: Player, + args: ParsedArguments, + command_user: User, + ) { + let user = match args.get("user_id") { + Some(ParsedArgument::String(s)) => { + let id = s.as_str(); + sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap() + } + _ => Some(command_user), }; - let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") - .bind(id) - .fetch_optional(self.pool.as_ref()) - .await - .unwrap(); - if let Some(user) = user { if user.items.is_empty() { client.message("Your inventory is empty.".to_string()).await; diff --git a/src/commands/eco/shop.rs b/src/commands/eco/shop.rs index 8b6f3ae..3bb449a 100644 --- a/src/commands/eco/shop.rs +++ b/src/commands/eco/shop.rs @@ -119,24 +119,19 @@ impl Command for ShopCommand { arg_type: ArgumentType::String, required: false, default: None, + children: &[], }, ArgumentSpec { name: "quantity", arg_type: ArgumentType::Integer, required: false, default: Some("1"), + children: &[], }, ] } - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { - let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") - .bind(&player._id) - .fetch_optional(self.pool.as_ref()) - .await - .unwrap() - .unwrap(); - + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, mut user: User) { let item_name = match args.get("item_id") { Some(ParsedArgument::String(s)) => s.as_str(), _ => "empty", diff --git a/src/commands/midi/play.rs b/src/commands/midi/play.rs index 99c597b..0f41d6e 100644 --- a/src/commands/midi/play.rs +++ b/src/commands/midi/play.rs @@ -1,4 +1,5 @@ use crate::Configuration; +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Note; @@ -47,6 +48,7 @@ impl Command for PlayCommand { arg_type: ArgumentType::String, required: true, default: None, + children: &[], }] } @@ -105,7 +107,7 @@ impl Command for PlayCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, _: User) { let file_arg = match args.get("file") { Some(crate::commands::argument::ParsedArgument::String(s)) => s, _ => "", diff --git a/src/commands/midi/playlist.rs b/src/commands/midi/playlist.rs index 6e09e98..d509cab 100644 --- a/src/commands/midi/playlist.rs +++ b/src/commands/midi/playlist.rs @@ -3,6 +3,7 @@ use crate::client::ClientEvent; use crate::client::Player; use crate::Configuration; +use crate::User; use crate::commands::Command; use crate::commands::MidiState; use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; @@ -45,6 +46,7 @@ impl Command for PlaylistCommand { arg_type: ArgumentType::String, required: true, default: None, + children: &[], }] } @@ -52,7 +54,7 @@ impl Command for PlaylistCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, _: User) { let joined_args = match args.get("playlist") { Some(ParsedArgument::String(s)) => s.as_str(), _ => "", diff --git a/src/commands/midi/queue.rs b/src/commands/midi/queue.rs index 41c1e15..f6e0e96 100644 --- a/src/commands/midi/queue.rs +++ b/src/commands/midi/queue.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -41,7 +42,7 @@ impl Command for QueueCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments, _: User) { let midi_state = self.midi_state.lock().await; let locked_queue = midi_state.queue.lock().await; let queue_len = locked_queue.len(); diff --git a/src/commands/midi/skip.rs b/src/commands/midi/skip.rs index ec99df6..0f18f94 100644 --- a/src/commands/midi/skip.rs +++ b/src/commands/midi/skip.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -42,7 +43,7 @@ impl Command for SkipCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments, _: User) { let mut midi_state = self.midi_state.lock().await; if let Some(handle) = midi_state.midi_handle.as_ref() && !handle.is_finished() diff --git a/src/commands/midi/stop.rs b/src/commands/midi/stop.rs index a7f73ae..c57865c 100644 --- a/src/commands/midi/stop.rs +++ b/src/commands/midi/stop.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -39,7 +40,7 @@ impl Command for StopCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, _: ParsedArguments, _: User) { let mut midi_state = self.midi_state.lock().await; if let Some(handle) = midi_state.midi_handle.as_ref() && !handle.is_finished() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0bb68ca..ab1bc3a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,5 @@ use crate::{ + User, client::{Client, ClientEvent, Player}, commands::argument::{ArgumentSpec, ParsedArguments}, }; @@ -45,7 +46,7 @@ pub trait Command: Send + Sync { async fn event(&mut self, client: Client, event: ClientEvent); async fn constructed(&mut self, client: Client); - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments); + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments, user: User); } pub type CommandArc = Arc>; diff --git a/src/commands/system/about.rs b/src/commands/system/about.rs index 543ad49..37a170a 100644 --- a/src/commands/system/about.rs +++ b/src/commands/system/about.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -31,6 +32,7 @@ impl Command for AboutCommand { arg_type: ArgumentType::Enum(&["midi", "arguments"]), required: false, default: None, + children: &[], }] } @@ -38,7 +40,7 @@ impl Command for AboutCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, _: User) { let section = match args.get("section") { Some(ParsedArgument::Enum(s)) => s.as_str(), _ => "", diff --git a/src/commands/system/follow.rs b/src/commands/system/follow.rs index f9530fb..07f24ea 100644 --- a/src/commands/system/follow.rs +++ b/src/commands/system/follow.rs @@ -6,6 +6,7 @@ use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; +use crate::User; use crate::commands::Command; use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; use async_trait::async_trait; @@ -107,17 +108,19 @@ impl Command for FollowCommand { arg_type: ArgumentType::String, required: false, default: None, + children: &[], }, ArgumentSpec { name: "animation", arg_type: ArgumentType::Enum(&["default"]), required: false, default: None, + children: &[], }, ] } - async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments, _: User) { let target = match args.get("target") { Some(ParsedArgument::String(s)) => s.as_str(), _ => player._id.as_str(), diff --git a/src/commands/system/help.rs b/src/commands/system/help.rs index 20a80f9..bf3e41a 100644 --- a/src/commands/system/help.rs +++ b/src/commands/system/help.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -38,12 +39,13 @@ impl Command for HelpCommand { arg_type: ArgumentType::String, required: false, default: None, + children: &[], }] } async fn event(&mut self, _: Client, _: ClientEvent) {} async fn constructed(&mut self, _: Client) {} - async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, _: User) { let cmd_name = match args.get("command") { Some(crate::commands::argument::ParsedArgument::String(s)) if !s.is_empty() => s, _ => "", @@ -63,13 +65,6 @@ impl Command for HelpCommand { ("(", ")") } }; - /*let default_text = { - if arg.default.is_some() { - format!("= {}", arg.default.unwrap()) - } else { - "".to_string() - } - };*/ arg_string.push(format!( "{}{}{}", diff --git a/src/commands/system/launch.rs b/src/commands/system/launch.rs index d6ce1e5..c971f3d 100644 --- a/src/commands/system/launch.rs +++ b/src/commands/system/launch.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -28,17 +29,13 @@ impl Command for LaunchCommand { arg_type: ArgumentType::String, required: true, default: None, + children: &[], }] } async fn constructed(&mut self, _: Client) {} async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, mut client: Client, player: Player, args: ParsedArguments) { - if player._id != "3bff3f33e6dc0410fdc61d13" { - client - .message("You are not `3bff3f33e6dc0410fdc61d13`.") - .await; - } + async fn execute(&mut self, mut client: Client, _: Player, args: ParsedArguments, _: User) { let channel = match args.get("channel") { Some(crate::commands::argument::ParsedArgument::String(s)) => s, _ => "", diff --git a/src/commands/system/mod.rs b/src/commands/system/mod.rs index 661db96..763697b 100644 --- a/src/commands/system/mod.rs +++ b/src/commands/system/mod.rs @@ -1,3 +1,3 @@ use crate::submods; -submods!(follow, help, launch, test, translate, about); +submods!(follow, help, launch, test, translate, about, rank); diff --git a/src/commands/system/rank.rs b/src/commands/system/rank.rs new file mode 100644 index 0000000..05a69af --- /dev/null +++ b/src/commands/system/rank.rs @@ -0,0 +1,177 @@ +use crate::Rank; +use crate::User; +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use sqlx::Pool; +use sqlx::Postgres; + +pub struct RankCommand { + pool: Arc>, + ranks: HashMap, +} + +impl RankCommand { + pub fn new(pool: Arc>, ranks: HashMap) -> Self { + Self { pool, ranks } + } +} +#[async_trait] +impl Command for RankCommand { + fn name(&self) -> &'static str { + "rank" + } + fn aliases(&self) -> &[&'static str] { + &[] + } + + fn category(&self) -> &'static str { + "system" + } + fn description(&self) -> &'static str { + "View your rank." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ + ArgumentSpec { + name: "action", + arg_type: ArgumentType::Enum(&["rank", "permission"]), + required: false, + default: None, + children: &[ArgumentSpec { + name: "sub_action", + arg_type: ArgumentType::Enum(&["set", "delete"]), + required: true, + default: None, + children: &[ArgumentSpec { + name: "permission_or_rank", + arg_type: ArgumentType::String, + required: true, + default: None, + children: &[], + }], + }], + }, + ArgumentSpec { + name: "user_id", + arg_type: ArgumentType::String, + required: false, + default: None, + children: &[ArgumentSpec { + name: "computed", + arg_type: ArgumentType::Boolean, + required: false, + default: Some("false"), + children: &[], + }], + }, + ] + } + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute( + &mut self, + client: Client, + player: Player, + args: ParsedArguments, + command_user: User, + ) { + let sub_action = match args.get("sub_action") { + Some(ParsedArgument::Enum(s)) => s.as_str(), + _ => "", + }; + + let user_db = match args.get("user_id") { + Some(ParsedArgument::String(s)) => { + let id = s.as_str(); + sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap() + } + _ => Some(command_user), + }; + + if user_db.is_none() { + client.message("No user found.").await; + return; + } + + let user = user_db.unwrap(); + + if !sub_action.is_empty() { + let action = match args.get("action") { + Some(ParsedArgument::Enum(s)) => s.as_str(), + _ => "", + }; + + let permission_or_rank = match args.get("permission_or_rank") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => "", + }; + match action { + "rank" => match action { + "set" => {} + "delete" => {} + _ => {} + }, + "permission" => match action { + "set" => {} + "delete" => {} + _ => {} + }, + _ => {} + } + + client + .message(format!( + "action: {}, sub_action: {}, permission_or_rank: {}", + action, sub_action, permission_or_rank + )) + .await; + } else { + let computed = match args.get("computed") { + Some(ParsedArgument::Boolean(s)) => s, + _ => &false, + }; + + let mut who: String = "Your".to_string(); + if user._id != player._id.as_str() { + who = format!("{}'s", user._id); + } + + if !user.extra_permissions.is_empty() { + who = format!( + "{} rank is `{}`, they have the following extra permissions: `{}`", + who, + user.rank, + user.extra_permissions.join(", ") + ) + } else { + who = format!("{} rank is `{}`", who, user.rank) + } + client.message(who).await; + if *computed { + let mut perms = self.ranks.get(&user.rank).unwrap().permissions.clone(); + + perms.extend(user.extra_permissions.0.iter().cloned()); + + client + .dm( + format!("All computed permissions: {}", perms.join(", ")), + user._id, + ) + .await; + } + } + } +} diff --git a/src/commands/system/test.rs b/src/commands/system/test.rs index 8ff8f88..8f03df0 100644 --- a/src/commands/system/test.rs +++ b/src/commands/system/test.rs @@ -2,6 +2,7 @@ use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; +use crate::User; use crate::commands::Command; use crate::commands::argument::{ArgumentSpec, ParsedArguments}; use async_trait::async_trait; @@ -39,7 +40,7 @@ impl Command for TestCommand { async fn constructed(&mut self, _: Client) {} async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, _: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, _: ParsedArguments, _: User) { client.message("Ping.").await; } } diff --git a/src/commands/system/translate.rs b/src/commands/system/translate.rs index 2ea01df..2cdbee6 100644 --- a/src/commands/system/translate.rs +++ b/src/commands/system/translate.rs @@ -1,3 +1,4 @@ +use crate::User; use crate::client::Client; use crate::client::ClientEvent; use crate::client::Player; @@ -98,18 +99,20 @@ impl Command for TranslateCommand { arg_type: ArgumentType::Enum(&["ru", "lv", "en", "ko"]), required: true, default: None, - }, - ArgumentSpec { - name: "from_language", - arg_type: ArgumentType::Enum(&["ru", "lv", "en", "ko", "auto"]), - required: true, - default: None, + children: &[ArgumentSpec { + name: "from_language", + arg_type: ArgumentType::Enum(&["ru", "lv", "en", "ko", "auto"]), + required: false, + default: Some("auto"), + children: &[], + }], }, ArgumentSpec { name: "text", arg_type: ArgumentType::GreedyString, required: true, default: None, + children: &[], }, ] } @@ -118,7 +121,7 @@ impl Command for TranslateCommand { async fn event(&mut self, _: Client, _: ClientEvent) {} - async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments, _: User) { let to_language = match args.get("to_language") { Some(ParsedArgument::Enum(s)) => s.as_str(), _ => "", diff --git a/src/main.rs b/src/main.rs index 5d7e452..c0b69ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,10 +35,63 @@ use std::sync::Arc; static ALLOCATOR: Cap = Cap::new(std::alloc::System, usize::MAX); #[derive(sqlx::FromRow)] -struct User { +pub struct User { _id: String, balance: i32, items: sqlx::types::Json>, + extra_permissions: sqlx::types::Json>, + rank: String, +} + +#[derive(Clone, Debug)] +pub struct Rank { + name: String, + permissions: Vec, +} + +pub async fn get_ranks(registry: CommandRegistry) -> HashMap { + let mut ranks: HashMap = HashMap::new(); + let mut command_permissions: Vec = Vec::new(); + + for command in registry.values() { + let locked_command = command.lock().await; + command_permissions.push(format!("commands.{}", locked_command.name())); + drop(locked_command); + } + + ranks.insert( + "owner".to_string(), + Rank { + name: "owner".to_string(), + permissions: { + let mut perms = command_permissions.clone(); + perms.push("play.high_note_counts".to_string()); + perms + }, + }, + ); + + ranks.insert( + "user".to_string(), + Rank { + name: "user".to_string(), + permissions: { + let mut perms = command_permissions.clone(); + perms.retain(|p| p != "commands.launch"); + perms + }, + }, + ); + + ranks +} + +pub fn has_permission(user: &User, ranks: HashMap, permission: String) -> bool { + let rank = ranks.get(&user.rank); + + rank.map(|r| r.permissions.contains(&permission)) + .unwrap_or(false) + || user.extra_permissions.0.contains(&permission) } macro_rules! register_all { @@ -97,6 +150,7 @@ async fn main() -> Result<(), Box> { let midi_state = Arc::new(Mutex::new(MidiState::new())); let mut registry = CommandRegistry::new(); + let ranks: HashMap = HashMap::new(); register_all!( registry, @@ -118,10 +172,23 @@ async fn main() -> Result<(), Box> { CoinflipCommand::new(arc_pool.clone()), TranslateCommand, AboutCommand, - HelpCommand::new(registry.clone()), + RankCommand::new(arc_pool.clone(), ranks.clone()), ] ); + registry + .register(HelpCommand::new(registry.clone()), client.clone()) + .await; + + let ranks = get_ranks(registry.clone()).await; + + registry + .register( + RankCommand::new(arc_pool.clone(), ranks.clone()), + client.clone(), + ) + .await; + let client_events = client.clone(); let events_pool = arc_pool.clone(); @@ -151,46 +218,63 @@ async fn main() -> Result<(), Box> { if let Some(no_prefix) = message.strip_prefix(conf.commands.prefix.as_str()) { let mut parts = no_prefix.split_whitespace(); if let Some(cmd_name) = parts.next() { - let args = Arguments::new(parts.map(|s| s.to_string()).collect()); + let user = + sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&player._id) + .fetch_optional(events_pool.as_ref()) + .await + .unwrap() + .unwrap(); - let mut cmd_opt: Option = None; - for cmd in registry.values() { - let cmd_lock = cmd.lock().await; - if cmd_lock.name() == cmd_name - || cmd_lock.aliases().contains(&cmd_name) - { - cmd_opt = Some(cmd.clone()); - break; - } - } + if !has_permission( + &user, + ranks.clone(), + format!("commands.{}", cmd_name), + ) { + client_events + .message(format!("You do not have permission \"commands.{}\" to run this command.", cmd_name)) + .await; + } else { + let args = Arguments::new(parts.map(|s| s.to_string()).collect()); - if let Some(cmd) = cmd_opt { - let mut cmd_lock = cmd.lock().await; - let specs = cmd_lock.argument_spec(); - match crate::commands::argument::parse_arguments(specs, &args.args) - { - Ok(parsed_args_struct) => { - cmd_lock - .execute( - client_events.clone(), - player.clone(), - parsed_args_struct, - ) - .await; + let mut cmd_opt: Option = None; + for cmd in registry.values() { + let cmd_lock = cmd.lock().await; + if cmd_lock.name() == cmd_name + || cmd_lock.aliases().contains(&cmd_name) + { + cmd_opt = Some(cmd.clone()); + break; } - Err(e) => { - client_events - .message(format!("Argument error: {}", e)) - .await; + } + + if let Some(cmd) = cmd_opt { + let mut cmd_lock = cmd.lock().await; + let specs = cmd_lock.argument_spec(); + match crate::commands::argument::parse_arguments( + specs, &args.args, + ) { + Ok(parsed_args_struct) => { + cmd_lock + .execute( + client_events.clone(), + player.clone(), + parsed_args_struct, + user, + ) + .await; + } + Err(e) => { + client_events + .message(format!("Argument error: {}", e)) + .await; + } } } } } } - if message == "!ping" { - client_events.message("pong").await; - } log!(MSG, player.name, message); } ClientEvent::PlayerJoined(player) => { @@ -204,12 +288,15 @@ async fn main() -> Result<(), Box> { let user_exists = user.unwrap().is_some(); if !user_exists { - let _ = sqlx::query!( - "INSERT INTO users (_id, balance, items) VALUES ($1, $2, $3)", - player.id, - 0, - serde_json::json!([]) + // INSERT with extra_permissions and rank columns (defaults) + let _ = sqlx::query( + "INSERT INTO users (_id, balance, items, extra_permissions, rank) VALUES ($1, $2, $3, $4, $5)", ) + .bind(&player._id) + .bind(0_i32) + .bind(serde_json::json!([])) + .bind(serde_json::json!([])) + .bind("user") .execute(events_pool.as_ref()) .await; }