aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

This commit is contained in:
jellywx 2021-06-14 21:35:38 +01:00
parent b8bbfbfade
commit 60ead9a1ef
3 changed files with 231 additions and 165 deletions

View File

@ -171,7 +171,7 @@ pub fn create_declaration_validations(fun: &mut CommandFun) -> SynResult<()> {
let context: Type = parse_quote!(&serenity::client::Context); let context: Type = parse_quote!(&serenity::client::Context);
let message: Type = parse_quote!(&(dyn crate::framework::CommandInvoke + Sync + Send)); let message: Type = parse_quote!(&(dyn crate::framework::CommandInvoke + Sync + Send));
let args: Type = parse_quote!(serenity::framework::standard::Args); let args: Type = parse_quote!(crate::framework::Args);
let mut index = 0; let mut index = 0;

View File

@ -3,17 +3,14 @@ use serenity::{
builder::CreateEmbed, builder::CreateEmbed,
cache::Cache, cache::Cache,
client::Context, client::Context,
framework::{ framework::{standard::CommandResult, Framework},
standard::{Args, CommandResult, Delimiter},
Framework,
},
futures::prelude::future::BoxFuture, futures::prelude::future::BoxFuture,
http::Http, http::Http,
model::{ model::{
channel::{Channel, GuildChannel, Message}, channel::{Channel, GuildChannel, Message},
guild::{Guild, Member}, guild::{Guild, Member},
id::{ChannelId, GuildId, UserId}, id::{ChannelId, GuildId, UserId},
interactions::Interaction, interactions::{ApplicationCommand, Interaction, InteractionType},
prelude::{ApplicationCommandOptionType, InteractionResponseType}, prelude::{ApplicationCommandOptionType, InteractionResponseType},
}, },
prelude::TypeMapKey, prelude::TypeMapKey,
@ -24,11 +21,9 @@ use log::{error, info, warn};
use regex::{Match, Regex, RegexBuilder}; use regex::{Match, Regex, RegexBuilder};
use std::{collections::HashMap, env, fmt}; use std::{collections::HashMap, env, fmt, sync::Arc};
use crate::{guild_data::CtxGuildData, MySQL}; use crate::{guild_data::CtxGuildData, MySQL};
use serenity::model::prelude::InteractionType;
use std::sync::Arc;
type CommandFn = for<'fut> fn( type CommandFn = for<'fut> fn(
&'fut Context, &'fut Context,
@ -36,6 +31,54 @@ type CommandFn = for<'fut> fn(
Args, Args,
) -> BoxFuture<'fut, CommandResult>; ) -> BoxFuture<'fut, CommandResult>;
pub struct Args {
args: HashMap<String, String>,
}
impl Args {
pub fn from(message: &str, arg_schema: &'static [&'static Arg]) -> Self {
// construct regex from arg schema
let mut re = arg_schema
.iter()
.map(|a| a.to_regex())
.collect::<Vec<String>>()
.join(r#"\s*"#);
re.push_str("$");
let regex = Regex::new(&re).unwrap();
let capture_names = regex.capture_names();
let captures = regex.captures(message);
let mut args = HashMap::new();
if let Some(captures) = captures {
for name in capture_names.filter(|n| n.is_some()).map(|n| n.unwrap()) {
args.insert(
name.to_string(),
captures.name(name).unwrap().as_str().to_string(),
);
}
}
Self { args }
}
pub fn len(&self) -> usize {
self.args.len()
}
pub fn is_empty(&self) -> bool {
self.args.is_empty()
}
pub fn named<D: ToString>(&self, name: D) -> Option<&String> {
let name = name.to_string();
self.args.get(&name)
}
}
pub struct CreateGenericResponse { pub struct CreateGenericResponse {
content: String, content: String,
embed: Option<CreateEmbed>, embed: Option<CreateEmbed>,
@ -203,6 +246,23 @@ pub struct Arg {
pub required: bool, pub required: bool,
} }
impl Arg {
pub fn to_regex(&self) -> String {
match self.kind {
ApplicationCommandOptionType::String => format!(r#"(?P<{}>.*?)"#, self.name),
ApplicationCommandOptionType::Integer => format!(r#"(?P<{}>\d+)"#, self.name),
ApplicationCommandOptionType::Boolean => format!(r#"(?P<{0}>{0})?"#, self.name),
ApplicationCommandOptionType::User => format!(r#"<(@|@!)(?P<{}>\d+)>"#, self.name),
ApplicationCommandOptionType::Channel => format!(r#"<#(?P<{}>\d+)>"#, self.name),
ApplicationCommandOptionType::Role => format!(r#"<@&(?P<{}>\d+)>"#, self.name),
ApplicationCommandOptionType::Mentionable => {
format!(r#"<(?P<{0}_pref>@|@!|@&|#)(?P<{0}>\d+)>"#, self.name)
}
_ => String::new(),
}
}
}
pub struct Command { pub struct Command {
pub fun: CommandFn, pub fun: CommandFn,
pub names: &'static [&'static str], pub names: &'static [&'static str],
@ -403,7 +463,29 @@ impl RegexFramework {
count += 1; count += 1;
} }
} else { } else {
// register application commands globally for (handle, command) in self.commands.iter().filter(|(_, c)| c.allow_slash) {
ApplicationCommand::create_global_application_command(&http, |a| {
a.name(handle).description(command.desc);
for arg in command.args {
a.create_option(|o| {
o.name(arg.name)
.description(arg.description)
.kind(arg.kind)
.required(arg.required)
});
}
a
})
.await
.expect(&format!(
"Failed to create application command for {}",
handle
));
count += 1;
}
} }
info!("{} slash commands built! Ready to go", count); info!("{} slash commands built! Ready to go", count);
@ -411,40 +493,48 @@ impl RegexFramework {
pub async fn execute(&self, ctx: Context, interaction: Interaction) { pub async fn execute(&self, ctx: Context, interaction: Interaction) {
if interaction.kind == InteractionType::ApplicationCommand { if interaction.kind == InteractionType::ApplicationCommand {
let command = { if let Some(data) = interaction.data.clone() {
let name = &interaction.data.as_ref().unwrap().name; let command = {
let name = data.name;
self.commands self.commands
.get(name) .get(&name)
.expect(&format!("Received invalid command: {}", name)) .expect(&format!("Received invalid command: {}", name))
}; };
if command if command
.check_permissions( .check_permissions(
&ctx, &ctx,
&interaction.guild(ctx.cache.clone()).await.unwrap(), &interaction.guild(ctx.cache.clone()).await.unwrap(),
&interaction.member(&ctx).await.unwrap(), &interaction.member(&ctx).await.unwrap(),
) )
.await
{
(command.fun)(&ctx, &interaction, Args::new("", &[Delimiter::Single(' ')]))
.await .await
.unwrap(); {
} else if command.required_permissions == PermissionLevel::Managed { let mut args = HashMap::new();
let _ = interaction
.respond( for arg in data.options.iter().filter(|o| o.value.is_some()) {
ctx.http.clone(), args.insert(arg.name.clone(), arg.value.clone().unwrap().to_string());
CreateGenericResponse::new().content("You must either be an Admin or have a role specified in `?roles` to do this command") }
)
.await; (command.fun)(&ctx, &interaction, Args { args })
} else if command.required_permissions == PermissionLevel::Restricted { .await
let _ = interaction .unwrap();
.respond( } else if command.required_permissions == PermissionLevel::Managed {
ctx.http.clone(), let _ = interaction
CreateGenericResponse::new() .respond(
.content("You must be an Admin to do this command"), ctx.http.clone(),
) CreateGenericResponse::new().content("You must either be an Admin or have a role specified in `?roles` to do this command")
.await; )
.await;
} else if command.required_permissions == PermissionLevel::Restricted {
let _ = interaction
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("You must be an Admin to do this command"),
)
.await;
}
} }
} }
} }
@ -513,13 +603,9 @@ impl Framework for RegexFramework {
let member = guild.member(&ctx, &msg.author).await.unwrap(); let member = guild.member(&ctx, &msg.author).await.unwrap();
if command.check_permissions(&ctx, &guild, &member).await { if command.check_permissions(&ctx, &guild, &member).await {
(command.fun)( (command.fun)(&ctx, &msg, Args::from(&args, command.args))
&ctx, .await
&msg, .unwrap();
Args::new(&args, &[Delimiter::Single(' ')]),
)
.await
.unwrap();
} else if command.required_permissions == PermissionLevel::Managed { } else if command.required_permissions == PermissionLevel::Managed {
let _ = msg.channel_id.say(&ctx, "You must either be an Admin or have a role specified in `?roles` to do this command").await; let _ = msg.channel_id.say(&ctx, "You must either be an Admin or have a role specified in `?roles` to do this command").await;
} else if command.required_permissions } else if command.required_permissions

View File

@ -9,7 +9,7 @@ mod sound;
use crate::{ use crate::{
event_handlers::{Handler, RestartTrack}, event_handlers::{Handler, RestartTrack},
framework::{CommandInvoke, CreateGenericResponse, RegexFramework}, framework::{Args, CommandInvoke, CreateGenericResponse, RegexFramework},
guild_data::{CtxGuildData, GuildData}, guild_data::{CtxGuildData, GuildData},
sound::{JoinSoundCtx, Sound}, sound::{JoinSoundCtx, Sound},
}; };
@ -20,7 +20,7 @@ use regex_command_attr::command;
use serenity::{ use serenity::{
client::{bridge::gateway::GatewayIntents, Client, Context}, client::{bridge::gateway::GatewayIntents, Client, Context},
framework::standard::{Args, CommandResult}, framework::standard::CommandResult,
http::Http, http::Http,
model::{ model::{
guild::Guild, guild::Guild,
@ -297,41 +297,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[command] #[command]
#[description("Get information on the commands of the bot")] #[description("Get information on the commands of the bot")]
#[arg(
name = "category",
description = "Get help for a specific category",
kind = "String",
required = false
)]
async fn help( async fn help(
ctx: &Context, ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send), invoke: &(dyn CommandInvoke + Sync + Send),
args: Args, args: Args,
) -> CommandResult { ) -> CommandResult {
if args.is_empty() { if let Some(category) = args.named("category") {
let description = { let body = match category.to_lowercase().as_str() {
let guild_data = ctx.guild_data(invoke.guild_id().unwrap()).await.unwrap();
let read_lock = guild_data.read().await;
format!(
"Type `{}help category` to view help for a command category below:",
read_lock.prefix
)
};
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().embed(|e| {
e.title("Help")
.color(THEME_COLOR)
.description(description)
.field("Info", "`help` `info` `invite` `donate`", false)
.field("Play", "`play` `p` `stop` `dc` `loop`", false)
.field("Manage", "`upload` `delete` `list` `public`", false)
.field("Settings", "`prefix` `roles` `volume` `allow_greet`", false)
.field("Search", "`search` `random` `popular`", false)
.field("Other", "`greet` `ambience`", false)
}),
)
.await?;
} else {
let body = match args.rest().to_lowercase().as_str() {
"info" => { "info" => {
"__Info Commands__ "__Info Commands__
`help` - view all commands `help` - view all commands
@ -421,6 +399,34 @@ Please select a category from the following:
.embed(|e| e.title("Help").color(THEME_COLOR).description(body)), .embed(|e| e.title("Help").color(THEME_COLOR).description(body)),
) )
.await?; .await?;
} else {
let description = {
let guild_data = ctx.guild_data(invoke.guild_id().unwrap()).await.unwrap();
let read_lock = guild_data.read().await;
format!(
"Type `{}help category` to view help for a command category below:",
read_lock.prefix
)
};
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().embed(|e| {
e.title("Help")
.color(THEME_COLOR)
.description(description)
.field("Info", "`help` `info` `invite` `donate`", false)
.field("Play", "`play` `p` `stop` `dc` `loop`", false)
.field("Manage", "`upload` `delete` `list` `public`", false)
.field("Settings", "`prefix` `roles` `volume` `allow_greet`", false)
.field("Search", "`search` `random` `popular`", false)
.field("Other", "`greet` `ambience`", false)
}),
)
.await?;
} }
Ok(()) Ok(())
@ -436,12 +442,6 @@ Please select a category from the following:
kind = "String", kind = "String",
required = true required = true
)] )]
#[arg(
name = "loop",
description = "Whether to loop the sound or not (default: no)",
kind = "Boolean",
required = false
)]
async fn play( async fn play(
ctx: &Context, ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send), invoke: &(dyn CommandInvoke + Sync + Send),
@ -497,7 +497,7 @@ async fn play_cmd(ctx: &Context, guild: Guild, user_id: UserId, args: Args, loop
match channel_to_join { match channel_to_join {
Some(user_channel) => { Some(user_channel) => {
let search_term = args.rest(); let search_term = args.named("query").unwrap();
let pool = ctx let pool = ctx
.data .data
@ -569,7 +569,7 @@ async fn play_ambience(
match channel_to_join { match channel_to_join {
Some(user_channel) => { Some(user_channel) => {
let search_name = args.rest().to_lowercase(); let search_name = args.named("query").unwrap().to_lowercase();
let audio_index = ctx.data.read().await.get::<AudioIndex>().cloned().unwrap(); let audio_index = ctx.data.read().await.get::<AudioIndex>().cloned().unwrap();
if let Some(filename) = audio_index.get(&search_name) { if let Some(filename) = audio_index.get(&search_name) {
@ -724,10 +724,16 @@ There is a maximum sound limit per user. This can be removed by subscribing at *
#[aliases("vol")] #[aliases("vol")]
#[required_permissions(Managed)] #[required_permissions(Managed)]
#[description("Change the bot's volume in this server")] #[description("Change the bot's volume in this server")]
#[arg(
name = "volume",
description = "New volume for the bot to use",
kind = "Integer",
required = false
)]
async fn change_volume( async fn change_volume(
ctx: &Context, ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send), invoke: &(dyn CommandInvoke + Sync + Send),
mut args: Args, args: Args,
) -> CommandResult { ) -> CommandResult {
let pool = ctx let pool = ctx
.data .data
@ -740,36 +746,17 @@ async fn change_volume(
let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await; let guild_data_opt = ctx.guild_data(invoke.guild_id().unwrap()).await;
let guild_data = guild_data_opt.unwrap(); let guild_data = guild_data_opt.unwrap();
if args.len() == 1 { if let Some(volume) = args.named("volume").map(|i| i.parse::<u8>().ok()).flatten() {
match args.single::<u8>() { guild_data.write().await.volume = volume;
Ok(volume) => {
guild_data.write().await.volume = volume;
guild_data.read().await.commit(pool).await?; guild_data.read().await.commit(pool).await?;
invoke invoke
.respond( .respond(
ctx.http.clone(), ctx.http.clone(),
CreateGenericResponse::new() CreateGenericResponse::new().content(format!("Volume changed to {}%", volume)),
.content(format!("Volume changed to {}%", volume)), )
) .await?;
.await?;
}
Err(_) => {
let read = guild_data.read().await;
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(format!(
"Current server volume: {vol}%. Change the volume with `/volume <new volume>`",
vol = read.volume
)),
)
.await?;
}
}
} else { } else {
let read = guild_data.read().await; let read = guild_data.read().await;
@ -793,7 +780,7 @@ async fn change_volume(
async fn change_prefix( async fn change_prefix(
ctx: &Context, ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send), invoke: &(dyn CommandInvoke + Sync + Send),
mut args: Args, args: Args,
) -> CommandResult { ) -> CommandResult {
let pool = ctx let pool = ctx
.data .data
@ -811,50 +798,34 @@ async fn change_prefix(
guild_data = guild_data_opt.unwrap(); guild_data = guild_data_opt.unwrap();
} }
if args.len() == 1 { if let Some(prefix) = args.named("prefix") {
match args.single::<String>() { if prefix.len() <= 5 {
Ok(prefix) => { let reply = format!("Prefix changed to `{}`", prefix);
if prefix.len() <= 5 {
let reply = format!("Prefix changed to `{}`", prefix);
{ {
guild_data.write().await.prefix = prefix; guild_data.write().await.prefix = prefix.to_string();
}
{
let read = guild_data.read().await;
read.commit(pool).await?;
}
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(reply),
)
.await?;
} else {
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Prefix must be less than 5 characters long"),
)
.await?;
}
} }
Err(_) => { {
invoke let read = guild_data.read().await;
.respond(
ctx.http.clone(), read.commit(pool).await?;
CreateGenericResponse::new().content(format!(
"Usage: `{prefix}prefix <new prefix>`",
prefix = guild_data.read().await.prefix
)),
)
.await?;
} }
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new().content(reply),
)
.await?;
} else {
invoke
.respond(
ctx.http.clone(),
CreateGenericResponse::new()
.content("Prefix must be less than 5 characters long"),
)
.await?;
} }
} else { } else {
invoke invoke
@ -873,6 +844,12 @@ async fn change_prefix(
#[command("upload")] #[command("upload")]
#[allow_slash(false)] #[allow_slash(false)]
#[arg(
name = "name",
description = "Name to upload sound to",
kind = "String",
required = true
)]
async fn upload_new_sound( async fn upload_new_sound(
ctx: &Context, ctx: &Context,
invoke: &(dyn CommandInvoke + Sync + Send), invoke: &(dyn CommandInvoke + Sync + Send),
@ -891,7 +868,10 @@ async fn upload_new_sound(
true true
} }
let new_name = args.rest().to_string(); let new_name = args
.named("name")
.map(|n| n.to_string())
.unwrap_or(String::new());
if !new_name.is_empty() && new_name.len() <= 20 { if !new_name.is_empty() && new_name.len() <= 20 {
if !is_numeric(&new_name) { if !is_numeric(&new_name) {
@ -1023,7 +1003,7 @@ async fn set_allowed_roles(
.cloned() .cloned()
.expect("Could not get SQLPool from data"); .expect("Could not get SQLPool from data");
if args.len() == 0 { if args.is_empty() {
let roles = sqlx::query!( let roles = sqlx::query!(
" "
SELECT role SELECT role
@ -1117,7 +1097,7 @@ async fn list_sounds(
let sounds; let sounds;
let mut message_buffer; let mut message_buffer;
if args.rest() == "me" { if args.named("me").is_some() {
sounds = Sound::get_user_sounds(invoke.author_id(), pool).await?; sounds = Sound::get_user_sounds(invoke.author_id(), pool).await?;
message_buffer = "All your sounds: ".to_string(); message_buffer = "All your sounds: ".to_string();
@ -1178,7 +1158,7 @@ async fn change_public(
let uid = invoke.author_id().as_u64().to_owned(); let uid = invoke.author_id().as_u64().to_owned();
let name = args.rest(); let name = args.named("query").unwrap();
let gid = *invoke.guild_id().unwrap().as_u64(); let gid = *invoke.guild_id().unwrap().as_u64();
let mut sound_vec = Sound::search_for_sound(name, gid, uid, pool.clone(), true).await?; let mut sound_vec = Sound::search_for_sound(name, gid, uid, pool.clone(), true).await?;
@ -1245,7 +1225,7 @@ async fn delete_sound(
let uid = invoke.author_id().0; let uid = invoke.author_id().0;
let gid = invoke.guild_id().unwrap().0; let gid = invoke.guild_id().unwrap().0;
let name = args.rest(); let name = args.named("query").unwrap();
let sound_vec = Sound::search_for_sound(name, gid, uid, pool.clone(), true).await?; let sound_vec = Sound::search_for_sound(name, gid, uid, pool.clone(), true).await?;
let sound_result = sound_vec.first(); let sound_result = sound_vec.first();
@ -1347,7 +1327,7 @@ async fn search_sounds(
.cloned() .cloned()
.expect("Could not get SQLPool from data"); .expect("Could not get SQLPool from data");
let query = args.rest(); let query = args.named("query").unwrap();
let search_results = Sound::search_for_sound( let search_results = Sound::search_for_sound(
query, query,
@ -1451,7 +1431,7 @@ async fn set_greet_sound(
.cloned() .cloned()
.expect("Could not get SQLPool from data"); .expect("Could not get SQLPool from data");
let query = args.rest(); let query = args.named("query").unwrap();
let user_id = invoke.author_id(); let user_id = invoke.author_id();
if query.len() == 0 { if query.len() == 0 {