feat: add dms, hieracthical arguments, permission and rank system
This commit is contained in:
parent
544bbf73cb
commit
257bc55b75
25 changed files with 503 additions and 209 deletions
4
migrations/20250914092514_rank.down.sql
Normal file
4
migrations/20250914092514_rank.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-- Add down migration script here
|
||||
ALTER TABLE users
|
||||
DROP COLUMN rank,
|
||||
DROP COLUMN "extra_permissions";
|
||||
3
migrations/20250914092514_rank.up.sql
Normal file
3
migrations/20250914092514_rank.up.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE users
|
||||
ADD COLUMN rank TEXT NOT NULL DEFAULT 'user',
|
||||
ADD COLUMN "extra_permissions" JSON NOT NULL DEFAULT '[]';
|
||||
|
|
@ -471,6 +471,10 @@ impl Client {
|
|||
self.send_command(json!({"m": "userset", "set": set})).await;
|
||||
}
|
||||
|
||||
pub async fn dm<S: AsRef<str>>(&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;
|
||||
|
|
|
|||
|
|
@ -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<ParsedArgument,
|
|||
ArgumentType::Integer => arg
|
||||
.parse::<i64>()
|
||||
.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::<f64>()
|
||||
.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::<i64>().is_ok(),
|
||||
ArgumentType::Float => token.parse::<f64>().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<usize, String> {
|
||||
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<ParsedArguments, String> {
|
||||
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::<i64>()
|
||||
.map(ParsedArgument::Integer)
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"Argument '{}' must be an integer, got '{}'",
|
||||
spec.name, value
|
||||
)
|
||||
})?,
|
||||
ArgumentType::Float => value
|
||||
.parse::<f64>()
|
||||
.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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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<i32> = 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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
_ => "",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
_ => "",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
_ => "",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
_ => "",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Mutex<dyn Command + Send + Sync>>;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
_ => "",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
"{}{}{}",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
_ => "",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
use crate::submods;
|
||||
|
||||
submods!(follow, help, launch, test, translate, about);
|
||||
submods!(follow, help, launch, test, translate, about, rank);
|
||||
|
|
|
|||
177
src/commands/system/rank.rs
Normal file
177
src/commands/system/rank.rs
Normal file
|
|
@ -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<Pool<Postgres>>,
|
||||
ranks: HashMap<String, Rank>,
|
||||
}
|
||||
|
||||
impl RankCommand {
|
||||
pub fn new(pool: Arc<Pool<Postgres>>, ranks: HashMap<String, Rank>) -> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
_ => "",
|
||||
|
|
|
|||
163
src/main.rs
163
src/main.rs
|
|
@ -35,10 +35,63 @@ use std::sync::Arc;
|
|||
static ALLOCATOR: Cap<std::alloc::System> = Cap::new(std::alloc::System, usize::MAX);
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct User {
|
||||
pub struct User {
|
||||
_id: String,
|
||||
balance: i32,
|
||||
items: sqlx::types::Json<Vec<String>>,
|
||||
extra_permissions: sqlx::types::Json<Vec<String>>,
|
||||
rank: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Rank {
|
||||
name: String,
|
||||
permissions: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn get_ranks(registry: CommandRegistry) -> HashMap<String, Rank> {
|
||||
let mut ranks: HashMap<String, Rank> = HashMap::new();
|
||||
let mut command_permissions: Vec<String> = 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<String, Rank>, 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<dyn std::error::Error + Send + Sync>> {
|
|||
let midi_state = Arc::new(Mutex::new(MidiState::new()));
|
||||
|
||||
let mut registry = CommandRegistry::new();
|
||||
let ranks: HashMap<String, Rank> = HashMap::new();
|
||||
|
||||
register_all!(
|
||||
registry,
|
||||
|
|
@ -118,10 +172,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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<dyn std::error::Error + Send + Sync>> {
|
|||
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<CommandArc> = 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<CommandArc> = 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<dyn std::error::Error + Send + Sync>> {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue