diff --git a/README.md b/README.md index 368db55..acb926c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python | `ASSISTANT_R_NAME` | The username of the Reddit bot/account. | | `ASSISTANT_R_PASS` | The password for the Reddit bot/account. | | `ASSISTANT_OWNERS` | (OPTIONAL) A list of Discord user IDs that "own" the bot. Separate each ID with a single comma and **no** spaces. This will allow the specified user IDs to run root commands such as `/stop`, it will also DM these users when *certain* errors occur. | -| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/bk_week` commands. | +| `ASSISTANT_BK_MODS` | (OPTIONAL) Same format as `ASSISTANT_OWNERS` but for people who are allowed to use the `/re_*` commands. | ### Required permissions: **These are automatically set if you use the [official invite link](https://discord.com/oauth2/authorize?client_id=1212127255795335208&permissions=84992&integration_type=0&scope=bot) or an invite link with the permissions integer set to `84992`.** (The permission integer is this part of the URL `&permissions=84992`) @@ -55,7 +55,7 @@ You can install Python modules by running `$ pip install {module}` or `$ python * Restart the terminal. * Navigate to the project root directory. * Run with `cargo run`. - * Alrenatively, run `cargo run -- {args here}` to add args. + * Alternatively, run `cargo run -- {args here}` to add args. * For help, run `cargo run -- -h` or `cargo run -- --help`. * To only run the Python part, use `cargo run -- --py`, or for a better error output, `python ./src/python/main.py` diff --git a/TODO.md b/TODO.md index fa6c258..e242ade 100644 --- a/TODO.md +++ b/TODO.md @@ -7,25 +7,11 @@ ### Medium priority: -- [ ] View single rule (/rule {rule_name}) - [ ] Postfix calculator - [ ] Postfix generator -- [ ] JSON -> BPS class init -- [ ] BPS args -> JSON - [ ] Random tip (from ByteDice.net/data/loadingScreenTips.json) - [ ] A command that just sends my socials ### Low priority: -- [ ] Particle of the week - * Starts a 1 week contest where people make particles based on a theme using BDE_ParticleSys -- [ ] Weekly coding competition - * Same as particle of the week but with coding - [ ] Content update sender - * Automatically sends sneak peeks (like commit history or manual) of projects when they're updated -- [ ] Language TLDR command - * Shows a TLDR with pros/cons on a programming language -- [ ] PowerPlate info viewer - * Shows basic info on a PowerPlate - -### Random little things: -- [ ] Update ASSISTANT_BK_MODS env var name to be reddit focused instead of bk focused \ No newline at end of file + * Automatically sends sneak peeks (like commit history or manual) of projects when they're updated \ No newline at end of file diff --git a/src/cmds.rs b/src/cmds.rs deleted file mode 100644 index 5fb9afb..0000000 --- a/src/cmds.rs +++ /dev/null @@ -1,495 +0,0 @@ -use std::collections::HashMap; -use std::process; -use std::error::Error as StdErr; - -use crate::data::{dc_add_server, get_mutex_data, read_cfg_data}; -use crate::re_cmds::generic_fns::to_shorturl; -use crate::websocket::send_cmd_json; -use crate::{data, lang, Context, Data, Error}; -use crate::messages::{edit_reply, send_embed, send_msg, Author, EmbedOptions}; - -use poise::serenity_prelude::{OnlineStatus, Timestamp}; -use poise::Command; -use rand::{seq::IteratorRandom, Rng}; -use serde_json::json; -use tokio::fs; - - -// TODO: separate to multiple files - - -#[derive(poise::ChoiceParameter, PartialEq)] -enum HelpOptions { - Admin, - All, - BkWeek, - BkWeekReddit, - Generic -} - - -type Cmd = Command>; - - -#[poise::command( - slash_command, - prefix_command, - category = "fun", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Check if you have connection to the bot. -pub async fn ping( - ctx: Context<'_>, - #[description = "The text to echo back."] text: Option, -) -> Result<(), Error> -{ - send_msg(ctx, text.unwrap_or_else(|| "Pong".to_string()), true, true).await; - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// I have security measures, even in developer mode. You wont access this without being a bot "owner". -pub async fn stop( - ctx: Context<'_>, - #[description = "Type \"i want to stop the bot now\" to confirm."] confirmation: Option, -) -> Result<(), Error> -{ - let should_stop = ctx.data().args.dev - || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; - - if should_stop { - let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); - data::write_dc_data(ctx.data()).await; - data::write_re_data().await; - send_cmd_json("stop_praw", None).await; - - edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await; - ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); - ctx.framework().shard_manager.shutdown_all().await; - - process::exit(0); - } - else { - send_msg(ctx, lang!("dc_msg_owner_shutdown_failed_confirmation"), true, true).await; - } - - return Ok(()); -} - - -#[allow(clippy::too_many_arguments)] -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" -)] -/// Creates an embed. -pub async fn embed( - ctx: Context<'_>, - #[description = "Title of embed."] title: Option, - #[description = "Body text of embed."] description: String, - #[description = "Color of side strip."] color: Option, - #[description = "A URL the title is bound to."] url: Option, - #[description = "Timestamp at bottom (best to leave empty)."] timestamp: Option, - #[description = "Ephemeral (only visible to you)."] ephemeral: Option, - #[description = "Shows \"used {Command}\" reply text."] reply: Option, - #[description = "Text that appears above and outside of the embed."] message: Option, - #[description = "A URL for a thumbnail image."] thumbnail: Option, - #[description = "Sets yourself as the author."] author: Option -) -> Result<(), Error> -{ - let reply_unwrap = reply.unwrap_or(false); - - send_embed( - ctx, - EmbedOptions { - desc: description.replace("\\n", "\n"), - title: title.map(|t| t.replace("\\n", "\n")), - col: color, - url, - ts: timestamp, - ephemeral: ephemeral.unwrap_or(false), - message, - thumbnail, - author: if author.unwrap_or(false) { Some(Author { name: ctx.author().name.clone(), url: "".to_string(), icon_url: ctx.author().avatar_url().unwrap() }) } else { None } - }, - reply_unwrap - ).await; - - if !reply_unwrap { - send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; - } - - return Ok(()); -} - - - -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Sends a message. -pub async fn send( - ctx: Context<'_>, - #[description = "The message to send (NOT EPHEMERAL)"] msg: String -) -> Result<(), Error> -{ - send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; - send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; - return Ok(()); -} - - - -#[poise::command( - slash_command, - prefix_command, - category = "fun", - rename = "8_ball", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Magic 8-ball. Ask a question, get an answer. -pub async fn eight_ball( - ctx: Context<'_>, - #[description = "Question to ask."] question: String -) -> Result<(), Error> -{ - let is_quirky = rand::rng().random_bool(0.2); - let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }]; - let rand_item = list.iter().choose(&mut rand::rng()); - - send_msg( - ctx, - lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), - true, - true - ).await; - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Convert a long reddit URL to a short one. The bot ONLY uses shortURLs when asking for one. -pub async fn re_shorturl( - ctx: Context<'_>, - #[description = "A Reddit post URL"] url: String -) -> Result<(), Error> -{ - let shorturl = to_shorturl(&url); - - if shorturl.is_ok() { - send_msg(ctx, lang!("dc_msg_shorturl", shorturl.unwrap()), true, true).await; - } - else { - send_msg(ctx, lang!("dc_msg_failed_shorturl_conversion"), true, true).await; - } - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "admin", - default_member_permissions = "ADMINISTRATOR", - guild_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Add your server to my database so I can sell it! (/s), I only store some minimal data the bot needs. -pub async fn add_server( - ctx: Context<'_> -) -> Result<(), Error> -{ - let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; - - if r.is_ok() { - send_msg(ctx, lang!("dc_msg_added_to_data"), true, true).await; - } - else { - send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await; - } - - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "owner", - owners_only, - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Reloads the entire config file. -pub async fn reload_cfg( - ctx: Context<'_> -) -> Result<(), Error> -{ - read_cfg_data(&ctx.data(), false).await; - let d = get_mutex_data(&ctx.data().cfg).await?; - let d_str = serde_json::to_string(&d)?; - let r = send_cmd_json("update_cfg", Some(json!([d_str]))).await; - - if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { - send_msg( - ctx, - lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), - true, - true - ).await; - return Ok(()); - } - - send_msg(ctx, lang!("dc_msg_reload_cfg_python_fail"), true, true).await; - return Ok(()); -} - - -#[poise::command( - slash_command, - prefix_command, - category = "help", - required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" -)] -/// Shows helpful information on how to use the bk_week section of the bot. -pub async fn help( - ctx: Context<'_>, - #[description = "A full category of commands."] category: Option, - #[description = "The name of a single command. This argument will be prioritized over `category`."] command: Option -) -> Result<(), Error> -{ - match (category.is_some(), command.is_some()) { - (false, false) => send_category_help(ctx, HelpOptions::Generic).await, - (true, false) => send_category_help(ctx, category.unwrap()).await, - _ => send_single_help(ctx, command.unwrap()).await - } - - return Ok(()); -} - - -fn format_cmd(cmd: &Cmd) -> String { - let arg_names: Vec<_> = cmd.parameters - .iter() - .map(|a| a.name.as_str()) - .collect(); - - let cmds_max_len = arg_names - .iter() - .max_by_key(|&&s| s.len()) - .map(|&s| s.len()) - .unwrap_or(0); - - let args_format: Vec<_> = cmd.parameters - .iter() - .map( - |a| - format!( - "{} {}{}{}", - a.name, - " ".repeat(cmds_max_len - a.name.len()), - if !a.required { "(OPTIONAL) " } else { "" }, - a.description.as_ref().unwrap_or(&"".to_string()) - ) - ) - .collect(); - - let t = format!( - "**`{}`**:\n```{}```", - cmd.name, - args_format.join("\n") - ); - - return t; -} - - -async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { - if cmd_name.starts_with("/") { cmd_name = cmd_name[1..].to_string(); } - let cmds = &ctx.framework().options().commands; - let cmd = cmds.iter().find(|c| c.name == cmd_name); - - if cmd.is_none() { - send_msg( - ctx, - lang!("dc_msg_cmd_404", cmd_name), - true, - true - ).await; - - return; - } - - let t = format_cmd(cmd.unwrap()); - send_msg(ctx, t, true, true).await; -} - - -async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { - match category { - HelpOptions::BkWeekReddit => send_bk_week_help_re(ctx).await, - HelpOptions::BkWeek => send_bk_week_help (ctx).await, - HelpOptions::Generic => send_generic_help (ctx).await, - HelpOptions::Admin => send_admin_help (ctx).await, - HelpOptions::All => send_all_help (ctx).await - } -} - - -async fn send_bk_week_help_re(ctx: Context<'_>) { - let t: String = fs::read_to_string("./bk_week_help_re.md").await - .unwrap_or(lang!("dc_msg_re_help_removed")); - - send_msg(ctx, t, true, true).await; -} - - -fn format_cmds(cmds: Vec<(&str, Vec<&Cmd>)>) -> String { - let cmd_names: Vec<_> = cmds - .iter() - .flat_map( - |t| - t.1.iter().map(|c| c.name.as_str()) - ) - .collect(); - - let cmds_max_len = cmd_names - .iter() - .max_by_key(|&&s| s.len()) - .map(|&s| s.len()) - .unwrap_or(0); - - let mut categories: Vec<(&str, Vec)> = Vec::new(); - - for c_tuple in cmds { - let cmds_format: Vec<_> = c_tuple.1 - .iter() - .map( - |c| - format!( - "{} {}{}", - c.name, - " ".repeat(cmds_max_len - c.name.len()), - c.description.as_ref().unwrap_or(&"".to_string()) - ) - ) - .collect(); - - categories.push((c_tuple.0, cmds_format)); - } - - let c_text: Vec = categories - .iter() - .map(|i| format!("{}:\n {}", i.0, i.1.join("\n "))) - .collect(); - - let t = format!("```{}```", c_text.join("\n\n")); - return t; -} - - -fn separate_by_category(cmds: Vec<&Cmd>) -> Vec<(String, Vec<&Cmd>)> { - let mut grouped: HashMap> = HashMap::new(); - - for cmd in cmds { - grouped - .entry(cmd.category.clone().unwrap_or("No category".to_string())) - .or_insert_with(Vec::new).push(cmd); - } - - return grouped.into_iter().collect(); -} - - -async fn send_bk_week_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let bk_week_cmds: Vec<_> = cmds - .iter() - .filter(|cmd| cmd.category == Some("re".to_string())) - .collect(); - - let t = format_cmds(vec![("re", bk_week_cmds)]); - send_msg(ctx, t, true, true).await; -} - - -async fn send_generic_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let filtered_cmds: Vec<_> = cmds - .iter() - .filter( - |cmd| - cmd.category != Some("re".to_string()) - || cmd.category != Some("owner".to_string()) - || cmd.category != Some("admin".to_string()) - ) - .collect(); - - let categories = separate_by_category(filtered_cmds); - - let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories - .iter() - .map(|c| (c.0.as_str(), c.1.clone())) - .collect(); - - let t = format_cmds(cmds_format); - send_msg(ctx, t, true, true).await; -} - - -async fn send_admin_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let filtered_cmds: Vec<_> = cmds - .iter() - .filter( - |cmd| - cmd.category == Some("owner".to_string()) - || cmd.category == Some("admin".to_string()) - ) - .collect(); - - let categories = separate_by_category(filtered_cmds); - - let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories - .iter() - .map(|c| (c.0.as_str(), c.1.clone())) - .collect(); - - let t = format_cmds(cmds_format); - send_msg(ctx, t, true, true).await; -} - - -async fn send_all_help(ctx: Context<'_>) { - let cmds = &ctx.framework().options().commands; - let cmds_clone: Vec<_> = cmds.iter().clone().collect(); - let categories = separate_by_category(cmds_clone); - - let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories - .iter() - .map(|c| (c.0.as_str(), c.1.clone())) - .collect(); - - let t = format_cmds(cmds_format); - send_msg(ctx, t, true, true).await; -} \ No newline at end of file diff --git a/src/cmds/add_server.rs b/src/cmds/add_server.rs new file mode 100644 index 0000000..c09f3a4 --- /dev/null +++ b/src/cmds/add_server.rs @@ -0,0 +1,28 @@ +use crate::{data::dc_add_server, lang, messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "add_server", + category = "admin", + default_member_permissions = "ADMINISTRATOR", + guild_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Add your server to my database so I can sell it! (/s), I only store some minimal data the bot needs. +pub async fn cmd( + ctx: Context<'_> +) -> Result<(), Error> +{ + let r = dc_add_server(ctx.data(), ctx.guild_id().unwrap().into()).await; + + if r.is_ok() { + send_msg(ctx, lang!("dc_msg_added_to_data"), true, true).await; + } + else { + send_msg(ctx, lang!("dc_msg_corrupted_data"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/eight_ball.rs b/src/cmds/eight_ball.rs new file mode 100644 index 0000000..8100688 --- /dev/null +++ b/src/cmds/eight_ball.rs @@ -0,0 +1,31 @@ +use rand::{seq::IteratorRandom, Rng}; + +use crate::{lang, messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + category = "fun", + rename = "8_ball", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Magic 8-ball. Ask a question, get an answer. +pub async fn cmd( + ctx: Context<'_>, + #[description = "Question to ask."] question: String +) -> Result<(), Error> +{ + let is_quirky = rand::rng().random_bool(0.2); + let list = &ctx.data().ball_prompts[if is_quirky { 1 } else { 0 }]; + let rand_item = list.iter().choose(&mut rand::rng()); + + send_msg( + ctx, + lang!("dc_msg_8-ball_answer", question, rand_item.unwrap()), + true, + true + ).await; + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/embed.rs b/src/cmds/embed.rs new file mode 100644 index 0000000..ebae7ae --- /dev/null +++ b/src/cmds/embed.rs @@ -0,0 +1,52 @@ +use poise::serenity_prelude::Timestamp; + +use crate::{lang, messages::{send_embed, send_msg, Author, EmbedOptions}, Context, Error}; + +#[allow(clippy::too_many_arguments)] +#[poise::command( + slash_command, + prefix_command, + rename = "embed", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL | EMBED_LINKS" +)] +/// Creates an embed. +pub async fn cmd( + ctx: Context<'_>, + #[description = "Title of embed."] title: Option, + #[description = "Body text of embed."] description: String, + #[description = "Color of side strip."] color: Option, + #[description = "A URL the title is bound to."] url: Option, + #[description = "Timestamp at bottom (best to leave empty)."] timestamp: Option, + #[description = "Ephemeral (only visible to you)."] ephemeral: Option, + #[description = "Shows \"used {Command}\" reply text."] reply: Option, + #[description = "Text that appears above and outside of the embed."] message: Option, + #[description = "A URL for a thumbnail image."] thumbnail: Option, + #[description = "Sets yourself as the author."] author: Option +) -> Result<(), Error> +{ + let reply_unwrap = reply.unwrap_or(false); + + send_embed( + ctx, + EmbedOptions { + desc: description.replace("\\n", "\n"), + title: title.map(|t| t.replace("\\n", "\n")), + col: color, + url, + ts: timestamp, + ephemeral: ephemeral.unwrap_or(false), + message, + thumbnail, + author: if author.unwrap_or(false) { Some(Author { name: ctx.author().name.clone(), url: "".to_string(), icon_url: ctx.author().avatar_url().unwrap() }) } else { None } + }, + reply_unwrap + ).await; + + if !reply_unwrap { + send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/help.rs b/src/cmds/help.rs new file mode 100644 index 0000000..d197600 --- /dev/null +++ b/src/cmds/help.rs @@ -0,0 +1,250 @@ +use poise::Command; +use std::{collections::HashMap, error::Error as StdErr}; +use tokio::fs; + +use crate::{lang, messages::send_msg, Context, Data, Error}; + + +#[derive(poise::ChoiceParameter, PartialEq)] +enum HelpOptions { + Admin, + All, + BkWeek, + BkWeekReddit, + Generic +} + + +type Cmd = Command>; + + + +#[poise::command( + slash_command, + prefix_command, + rename = "help", + category = "help", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Shows helpful information on how to use the bk_week section of the bot. +pub async fn cmd( + ctx: Context<'_>, + #[description = "A full category of commands."] category: Option, + #[description = "The name of a single command. This argument will be prioritized over `category`."] command: Option +) -> Result<(), Error> +{ + match (category.is_some(), command.is_some()) { + (false, false) => send_category_help(ctx, HelpOptions::Generic).await, + (true, false) => send_category_help(ctx, category.unwrap()).await, + _ => send_single_help(ctx, command.unwrap()).await + } + + return Ok(()); +} + + +fn format_cmd(cmd: &Cmd) -> String { + let arg_names: Vec<_> = cmd.parameters + .iter() + .map(|a| a.name.as_str()) + .collect(); + + let cmds_max_len = arg_names + .iter() + .max_by_key(|&&s| s.len()) + .map(|&s| s.len()) + .unwrap_or(0); + + let args_format: Vec<_> = cmd.parameters + .iter() + .map( + |a| + format!( + "{} {}{}{}", + a.name, + " ".repeat(cmds_max_len - a.name.len()), + if !a.required { "(OPTIONAL) " } else { "" }, + a.description.as_ref().unwrap_or(&"".to_string()) + ) + ) + .collect(); + + let t = format!( + "**`{}`**:\n```{}```", + cmd.name, + args_format.join("\n") + ); + + return t; +} + + +async fn send_single_help(ctx: Context<'_>, mut cmd_name: String) { + if cmd_name.starts_with("/") { cmd_name = cmd_name[1..].to_string(); } + let cmds = &ctx.framework().options().commands; + let cmd = cmds.iter().find(|c| c.name == cmd_name); + + if cmd.is_none() { + send_msg( + ctx, + lang!("dc_msg_cmd_404", cmd_name), + true, + true + ).await; + + return; + } + + let t = format_cmd(cmd.unwrap()); + send_msg(ctx, t, true, true).await; +} + + +async fn send_category_help(ctx: Context<'_>, category: HelpOptions) { + match category { + HelpOptions::BkWeekReddit => send_bk_week_help_re(ctx).await, + HelpOptions::BkWeek => send_bk_week_help (ctx).await, + HelpOptions::Generic => send_generic_help (ctx).await, + HelpOptions::Admin => send_admin_help (ctx).await, + HelpOptions::All => send_all_help (ctx).await + } +} + + +async fn send_bk_week_help_re(ctx: Context<'_>) { + let t: String = fs::read_to_string("./bk_week_help_re.md").await + .unwrap_or(lang!("dc_msg_re_help_removed")); + + send_msg(ctx, t, true, true).await; +} + + +fn format_cmds(cmds: Vec<(&str, Vec<&Cmd>)>) -> String { + let cmd_names: Vec<_> = cmds + .iter() + .flat_map( + |t| + t.1.iter().map(|c| c.name.as_str()) + ) + .collect(); + + let cmds_max_len = cmd_names + .iter() + .max_by_key(|&&s| s.len()) + .map(|&s| s.len()) + .unwrap_or(0); + + let mut categories: Vec<(&str, Vec)> = Vec::new(); + + for c_tuple in cmds { + let cmds_format: Vec<_> = c_tuple.1 + .iter() + .map( + |c| + format!( + "{} {}{}", + c.name, + " ".repeat(cmds_max_len - c.name.len()), + c.description.as_ref().unwrap_or(&"".to_string()) + ) + ) + .collect(); + + categories.push((c_tuple.0, cmds_format)); + } + + let c_text: Vec = categories + .iter() + .map(|i| format!("{}:\n {}", i.0, i.1.join("\n "))) + .collect(); + + let t = format!("```{}```", c_text.join("\n\n")); + return t; +} + + +fn separate_by_category(cmds: Vec<&Cmd>) -> Vec<(String, Vec<&Cmd>)> { + let mut grouped: HashMap> = HashMap::new(); + + for cmd in cmds { + grouped + .entry(cmd.category.clone().unwrap_or("No category".to_string())) + .or_insert_with(Vec::new).push(cmd); + } + + return grouped.into_iter().collect(); +} + + +async fn send_bk_week_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let bk_week_cmds: Vec<_> = cmds + .iter() + .filter(|cmd| cmd.category == Some("re".to_string())) + .collect(); + + let t = format_cmds(vec![("re", bk_week_cmds)]); + send_msg(ctx, t, true, true).await; +} + + +async fn send_generic_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let filtered_cmds: Vec<_> = cmds + .iter() + .filter( + |cmd| + cmd.category != Some("re".to_string()) + || cmd.category != Some("owner".to_string()) + || cmd.category != Some("admin".to_string()) + ) + .collect(); + + let categories = separate_by_category(filtered_cmds); + + let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories + .iter() + .map(|c| (c.0.as_str(), c.1.clone())) + .collect(); + + let t = format_cmds(cmds_format); + send_msg(ctx, t, true, true).await; +} + + +async fn send_admin_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let filtered_cmds: Vec<_> = cmds + .iter() + .filter( + |cmd| + cmd.category == Some("owner".to_string()) + || cmd.category == Some("admin".to_string()) + ) + .collect(); + + let categories = separate_by_category(filtered_cmds); + + let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories + .iter() + .map(|c| (c.0.as_str(), c.1.clone())) + .collect(); + + let t = format_cmds(cmds_format); + send_msg(ctx, t, true, true).await; +} + + +async fn send_all_help(ctx: Context<'_>) { + let cmds = &ctx.framework().options().commands; + let cmds_clone: Vec<_> = cmds.iter().clone().collect(); + let categories = separate_by_category(cmds_clone); + + let cmds_format: Vec<(&str, Vec<&Cmd>)> = categories + .iter() + .map(|c| (c.0.as_str(), c.1.clone())) + .collect(); + + let t = format_cmds(cmds_format); + send_msg(ctx, t, true, true).await; +} \ No newline at end of file diff --git a/src/cmds/ping.rs b/src/cmds/ping.rs new file mode 100644 index 0000000..d62b114 --- /dev/null +++ b/src/cmds/ping.rs @@ -0,0 +1,20 @@ +use crate::{messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "ping", + category = "fun", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Check if you have connection to the bot. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The text to echo back."] text: Option, +) -> Result<(), Error> +{ + send_msg(ctx, text.unwrap_or_else(|| "Pong".to_string()), true, true).await; + + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/reload_cfg.rs b/src/cmds/reload_cfg.rs new file mode 100644 index 0000000..737fbb1 --- /dev/null +++ b/src/cmds/reload_cfg.rs @@ -0,0 +1,36 @@ +use serde_json::json; + +use crate::{data::{get_mutex_data, read_cfg_data}, lang, messages::send_msg, websocket::send_cmd_json, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "reload_cfg", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Reloads the entire config file. +pub async fn cmd( + ctx: Context<'_> +) -> Result<(), Error> +{ + read_cfg_data(&ctx.data(), false).await; + let d = get_mutex_data(&ctx.data().cfg).await?; + let d_str = serde_json::to_string(&d)?; + let r = send_cmd_json("update_cfg", Some(json!([d_str]))).await; + + if r.is_some() && r.unwrap()["value"].as_bool().unwrap() { + send_msg( + ctx, + lang!("dc_msg_reload_cfg_success", serde_json::to_string_pretty(&d).unwrap()), + true, + true + ).await; + return Ok(()); + } + + send_msg(ctx, lang!("dc_msg_reload_cfg_python_fail"), true, true).await; + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/send.rs b/src/cmds/send.rs new file mode 100644 index 0000000..eee3085 --- /dev/null +++ b/src/cmds/send.rs @@ -0,0 +1,21 @@ +use crate::{lang, messages::send_msg, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "send", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Sends a message. +pub async fn cmd( + ctx: Context<'_>, + #[description = "The message to send (NOT EPHEMERAL)"] msg: String +) -> Result<(), Error> +{ + send_msg(ctx, msg.replace("\\n", "\n"), false, false).await; + send_msg(ctx, lang!("dc_msg_mandatory_response"), true, true).await; + return Ok(()); +} \ No newline at end of file diff --git a/src/cmds/stop.rs b/src/cmds/stop.rs new file mode 100644 index 0000000..ad38001 --- /dev/null +++ b/src/cmds/stop.rs @@ -0,0 +1,42 @@ +use std::process; + +use poise::serenity_prelude::OnlineStatus; + +use crate::{data, lang, messages::{edit_reply, send_msg}, websocket::send_cmd_json, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "stop", + category = "owner", + owners_only, + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// I have security measures, even in developer mode. You wont access this without being a bot "owner". +pub async fn cmd( + ctx: Context<'_>, + #[description = "Type \"i want to stop the bot now\" to confirm."] confirmation: Option, +) -> Result<(), Error> +{ + let should_stop = ctx.data().args.dev + || confirmation.unwrap_or_default().to_lowercase() == "i want to stop the bot now"; + + if should_stop { + let msg = send_msg(ctx, lang!("dc_msg_owner_data_save"), true, true).await.unwrap(); + data::write_dc_data(ctx.data()).await; + data::write_re_data().await; + send_cmd_json("stop_praw", None).await; + + edit_reply(ctx, msg, lang!("dc_msg_owner_data_save_complete")).await; + ctx.serenity_context().set_presence(None, OnlineStatus::Invisible); + ctx.framework().shard_manager.shutdown_all().await; + + process::exit(0); + } + else { + send_msg(ctx, lang!("dc_msg_owner_shutdown_failed_confirmation"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file diff --git a/src/gen.rs b/src/gen.rs index c93b32c..01e32e4 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -57,14 +57,13 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { .options(poise::FrameworkOptions { owners: own, commands: vec![ - cmds::help(), - cmds::ping(), - cmds::embed(), - cmds::send(), - cmds::stop(), - cmds::eight_ball(), - cmds::re_shorturl(), - cmds::add_server(), + cmds::help::cmd(), + cmds::ping::cmd(), + cmds::embed::cmd(), + cmds::send::cmd(), + cmds::stop::cmd(), + cmds::eight_ball::cmd(), + cmds::add_server::cmd(), // reddit re_cmds::add::cmd(), re_cmds::approve::cmd(), @@ -73,10 +72,11 @@ pub async fn gen_bot(data: Data, args: Args) -> Client { re_cmds::top::cmd(), re_cmds::update::cmd(), re_cmds::vote::cmd(), + re_cmds::shorturl::cmd(), // reddit admin re_cmds::admin_bind::cmd(), // cfg - cmds::reload_cfg() + cmds::reload_cfg::cmd() ], event_handler: events::event_handler, ..Default::default() diff --git a/src/main.rs b/src/main.rs index 09ba2d3..1bf13b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,16 @@ #![warn(unused_extern_crates)] #![allow(clippy::needless_return)] -mod cmds; +mod cmds { + pub mod add_server; + pub mod eight_ball; + pub mod embed; + pub mod help; + pub mod ping; + pub mod reload_cfg; + pub mod send; + pub mod stop; +} mod re_cmds { pub mod add; pub mod admin_bind; @@ -9,6 +18,7 @@ mod re_cmds { pub mod generic_fns; pub mod get; pub mod remove; + pub mod shorturl; pub mod top; pub mod update; pub mod vote; diff --git a/src/re_cmds/shorturl.rs b/src/re_cmds/shorturl.rs new file mode 100644 index 0000000..f22b6b1 --- /dev/null +++ b/src/re_cmds/shorturl.rs @@ -0,0 +1,26 @@ +use crate::{lang, messages::send_msg, re_cmds::generic_fns::to_shorturl, Context, Error}; + + +#[poise::command( + slash_command, + prefix_command, + rename = "re_shorturl", + required_bot_permissions = "SEND_MESSAGES | VIEW_CHANNEL" +)] +/// Convert a long reddit URL to a short one. The bot ONLY uses shortURLs when asking for one. +pub async fn cmd( + ctx: Context<'_>, + #[description = "A Reddit post URL"] url: String +) -> Result<(), Error> +{ + let shorturl = to_shorturl(&url); + + if shorturl.is_ok() { + send_msg(ctx, lang!("dc_msg_shorturl", shorturl.unwrap()), true, true).await; + } + else { + send_msg(ctx, lang!("dc_msg_failed_shorturl_conversion"), true, true).await; + } + + return Ok(()); +} \ No newline at end of file