From 5dd26d747466e66b50818f9375b499385a8f8a73 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Fri, 27 Mar 2020 21:22:15 +0000 Subject: [PATCH] Implement `roles` management (#18) Fix formatting Implement `roles` management Add a 'roles' mod and DB --- youmubot-core/src/community/mod.rs | 4 +- youmubot-core/src/community/roles.rs | 236 +++++++++++++++++++++++++++ youmubot-core/src/db.rs | 10 ++ youmubot-core/src/lib.rs | 1 + 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 youmubot-core/src/community/roles.rs diff --git a/youmubot-core/src/community/mod.rs b/youmubot-core/src/community/mod.rs index 4d90da4..0169677 100644 --- a/youmubot-core/src/community/mod.rs +++ b/youmubot-core/src/community/mod.rs @@ -15,14 +15,16 @@ use serenity::{ }; use youmubot_prelude::*; +mod roles; mod votes; +use roles::{ADD_COMMAND, LIST_COMMAND, REMOVE_COMMAND, TOGGLE_COMMAND}; use votes::VOTE_COMMAND; #[group] #[description = "Community related commands. Usually comes with some sort of delays, since it involves pinging"] #[only_in("guilds")] -#[commands(choose, vote)] +#[commands(choose, vote, add, list, remove, toggle)] struct Community; #[command] diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs new file mode 100644 index 0000000..84cd87b --- /dev/null +++ b/youmubot-core/src/community/roles.rs @@ -0,0 +1,236 @@ +use crate::db::Roles as DB; +use serenity::{ + framework::standard::{macros::command, Args, CommandError as Error, CommandResult}, + model::{channel::Message, id::RoleId}, + utils::MessageBuilder, +}; +use youmubot_prelude::*; + +#[command("listroles")] +#[description = "List all available roles in the server."] +#[num_args(0)] +#[only_in(guilds)] +fn list(ctx: &mut Context, m: &Message, _: Args) -> CommandResult { + let guild_id = m.guild_id.unwrap(); // only_in(guilds) + + let db = DB::open(&*ctx.data.read()); + let db = db.borrow()?; + let roles = db.get(&guild_id).filter(|v| !v.is_empty()); + match roles { + None => { + m.reply(&ctx, "No roles available for assigning.")?; + } + Some(v) => { + let roles = guild_id.to_partial_guild(&ctx)?.roles; + let roles: Vec<_> = v + .iter() + .filter_map(|(_, role)| roles.get(&role.id).map(|r| (r, &role.description))) + .collect(); + const ROLES_PER_PAGE: usize = 5; + let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE; + + let watcher = ctx.data.get_cloned::(); + watcher.paginate_fn( + ctx.clone(), + m.channel_id, + |page, e| { + let page = page as usize; + let start = page * ROLES_PER_PAGE; + let end = roles.len().min(start + ROLES_PER_PAGE); + if end <= start { + return (e, Err(Error::from("No more roles to display"))); + } + let roles = &roles[start..end]; + let nw = roles // name width + .iter() + .map(|(r, _)| r.name.len()) + .max() + .unwrap() + .max(6); + let idw = roles[0].0.id.to_string().len(); + let dw = roles + .iter() + .map(|v| v.1.len()) + .max() + .unwrap() + .max(" Description ".len()); + let mut m = MessageBuilder::new(); + m.push_line("```"); + + // Table header + m.push_line(format!( + "{:nw$} | {:idw$} | {:dw$}", + "Name", + "ID", + "Description", + nw = nw, + idw = idw, + dw = dw, + )); + m.push_line(format!( + "{:->nw$}---{:->idw$}---{:->dw$}", + "", + "", + "", + nw = nw, + idw = idw, + dw = dw, + )); + + for (role, description) in roles.iter() { + m.push_line(format!( + "{:nw$} | {:idw$} | {:dw$}", + role.name, + role.id, + description, + nw = nw, + idw = idw, + dw = dw, + )); + } + m.push_line("```"); + m.push(format!("Page **{}/{}**", page + 1, pages)); + + (e.content(m.build()), Ok(())) + }, + std::time::Duration::from_secs(60 * 10), + )?; + } + }; + Ok(()) +} + +#[command("role")] +#[description = "Toggle a role by its name or ID."] +#[num_args(1)] +#[only_in(guilds)] +fn toggle(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { + let role = args.single::()?; + let guild_id = m.guild_id.unwrap(); + let roles = guild_id.to_partial_guild(&ctx)?.roles; + let role = match role.parse::() { + Ok(id) => roles.get(&RoleId(id)).cloned(), + Err(_) => roles + .iter() + .find_map(|(_, r)| if r.name == role { Some(r) } else { None }) + .cloned(), + }; + match role { + None => { + m.reply(&ctx, "No such role exists")?; + } + Some(role) + if !DB::open(&*ctx.data.read()) + .borrow()? + .get(&guild_id) + .map(|g| g.contains_key(&role.id)) + .unwrap_or(false) => + { + m.reply(&ctx, "This role is not self-assignable. Check the `listroles` command to see which role can be assigned.")?; + } + Some(role) => { + let mut member = m.member(&ctx).ok_or(Error::from("Cannot find member"))?; + if member.roles.contains(&role.id) { + member.remove_role(&ctx, &role)?; + m.reply(&ctx, format!("Role `{}` has been removed.", role.name))?; + } else { + member.add_role(&ctx, &role)?; + m.reply(&ctx, format!("Role `{}` has been assigned.", role.name))?; + } + } + }; + Ok(()) +} + +#[command("addrole")] +#[description = "Add a role as the assignable role"] +#[usage = "{role-name-or-id} / {description}"] +#[example = "hd820 / Headphones role"] +#[num_args(2)] +#[required_permissions(MANAGE_ROLES)] +#[only_in(guilds)] +fn add(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { + let role = args.single::()?; + let description = args.single::()?; + let guild_id = m.guild_id.unwrap(); + let roles = guild_id.to_partial_guild(&ctx)?.roles; + let role = match role.parse::() { + Ok(id) => roles.get(&RoleId(id)).cloned(), + Err(_) => roles + .iter() + .find_map(|(_, r)| if r.name == role { Some(r) } else { None }) + .cloned(), + }; + match role { + None => { + m.reply(&ctx, "No such role exists")?; + } + Some(role) + if DB::open(&*ctx.data.read()) + .borrow()? + .get(&guild_id) + .map(|g| g.contains_key(&role.id)) + .unwrap_or(false) => + { + m.reply(&ctx, "This role already exists in the database.")?; + } + Some(role) => { + DB::open(&*ctx.data.read()) + .borrow_mut()? + .entry(guild_id) + .or_default() + .insert( + role.id, + crate::db::Role { + id: role.id, + description, + }, + ); + m.react(&ctx, "👌🏼")?; + } + }; + Ok(()) +} + +#[command("removerole")] +#[description = "Remove a role from the assignable roles list."] +#[usage = "{role-name-or-id}"] +#[example = "hd820"] +#[num_args(1)] +#[required_permissions(MANAGE_ROLES)] +#[only_in(guilds)] +fn remove(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult { + let role = args.single::()?; + let guild_id = m.guild_id.unwrap(); + let roles = guild_id.to_partial_guild(&ctx)?.roles; + let role = match role.parse::() { + Ok(id) => roles.get(&RoleId(id)).cloned(), + Err(_) => roles + .iter() + .find_map(|(_, r)| if r.name == role { Some(r) } else { None }) + .cloned(), + }; + match role { + None => { + m.reply(&ctx, "No such role exists")?; + } + Some(role) + if !DB::open(&*ctx.data.read()) + .borrow()? + .get(&guild_id) + .map(|g| g.contains_key(&role.id)) + .unwrap_or(false) => + { + m.reply(&ctx, "This role does not exist in the assignable list.")?; + } + Some(role) => { + DB::open(&*ctx.data.read()) + .borrow_mut()? + .entry(guild_id) + .or_default() + .remove(&role.id); + m.react(&ctx, "👌🏼")?; + } + }; + Ok(()) +} diff --git a/youmubot-core/src/db.rs b/youmubot-core/src/db.rs index 452562e..0d601c3 100644 --- a/youmubot-core/src/db.rs +++ b/youmubot-core/src/db.rs @@ -8,6 +8,9 @@ use youmubot_db::{GuildMap, DB}; /// A list of SoftBans for all servers. pub type SoftBans = DB>; +/// A list of assignable roles for all servers. +pub type Roles = DB>>; + /// For the admin commands: /// - Each server might have a `soft ban` role implemented. /// - We allow periodical `soft ban` applications. @@ -34,3 +37,10 @@ pub struct ImplementedSoftBans { /// List of all to-unban people. pub periodical_bans: HashMap>, } + +/// Role represents an assignable role. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Role { + pub id: RoleId, + pub description: String, +} diff --git a/youmubot-core/src/lib.rs b/youmubot-core/src/lib.rs index b039c75..19dcbe2 100644 --- a/youmubot-core/src/lib.rs +++ b/youmubot-core/src/lib.rs @@ -23,6 +23,7 @@ pub fn setup( data: &mut youmubot_prelude::ShareMap, ) -> serenity::framework::standard::CommandResult { db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?; + db::Roles::insert_into(&mut *data, &path.join("roles.yaml"))?; // Create handler threads std::thread::spawn(admin::watch_soft_bans(client));