From 72c574fb4809768a3a81d7c517ee9ef1131dd2a0 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 10 Feb 2021 17:57:19 +0900 Subject: [PATCH] Extended role list --- youmubot-core/src/community/roles.rs | 52 ++++++++++++++-------- youmubot-core/src/db.rs | 65 +++++++++++++++++++++++++++- youmubot-core/src/lib.rs | 6 ++- youmubot-db/src/lib.rs | 5 +++ 4 files changed, 107 insertions(+), 21 deletions(-) diff --git a/youmubot-core/src/community/roles.rs b/youmubot-core/src/community/roles.rs index 4062ed1..d56ad4c 100644 --- a/youmubot-core/src/community/roles.rs +++ b/youmubot-core/src/community/roles.rs @@ -1,7 +1,11 @@ use crate::db::Roles as DB; use serenity::{ framework::standard::{macros::command, Args, CommandResult}, - model::{channel::Message, guild::Role, id::RoleId}, + model::{ + channel::{Message, ReactionType}, + guild::Role, + id::RoleId, + }, utils::MessageBuilder, }; use youmubot_prelude::*; @@ -18,7 +22,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult { let roles = db .borrow()? .get(&guild_id) - .filter(|v| !v.is_empty()) + .filter(|v| !v.roles.is_empty()) .cloned(); match roles { None => { @@ -27,6 +31,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult { Some(v) => { let roles = guild_id.to_partial_guild(&ctx).await?.roles; let roles: Vec<_> = v + .roles .into_iter() .filter_map(|(_, role)| roles.get(&role.id).cloned().map(|r| (r, role.description))) .collect(); @@ -128,7 +133,7 @@ async fn toggle(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { if !DB::open(&*ctx.data.read().await) .borrow()? .get(&guild_id) - .map(|g| g.contains_key(&role.id)) + .map(|g| g.roles.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.").await?; @@ -150,16 +155,32 @@ async fn toggle(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { } #[command("addrole")] -#[description = "Add a role as the assignable role"] -#[usage = "{role-name-or-id} / {description}"] -#[example = "hd820 / Headphones role"] -#[num_args(2)] +#[description = "Add a role as the assignable role. Overrides the old entry."] +#[usage = "{role-name-or-id} / {description} / [representing emoji = none]"] +#[example = "hd820 / Headphones role / 🎧"] +#[min_args(2)] +#[max_args(3)] #[required_permissions(MANAGE_ROLES)] #[only_in(guilds)] async fn add(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { let role = args.single_quoted::()?; let data = ctx.data.read().await; - let description = args.single::()?; + let description = args.single_quoted::()?; + let reaction = match args.single::() { + Ok(v) => match &v { + ReactionType::Custom { id, .. } => { + // Verify that the reaction type is from the server. + if m.guild_id.unwrap().emoji(&ctx, *id).await.is_err() { + m.reply(&ctx, "Emote cannot be used as I cannot send this back.") + .await?; + return Ok(()); + } + Some(v) + } + _ => Some(v), + }, + _ => None, + }; let guild_id = m.guild_id.unwrap(); let roles = guild_id.to_partial_guild(&ctx).await?.roles; let role = role_from_string(&role, &roles); @@ -167,26 +188,18 @@ async fn add(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { None => { m.reply(&ctx, "No such role exists").await?; } - Some(role) - if DB::open(&*data) - .borrow()? - .get(&guild_id) - .map(|g| g.contains_key(&role.id)) - .unwrap_or(false) => - { - m.reply(&ctx, "This role already exists in the database.") - .await?; - } Some(role) => { DB::open(&*data) .borrow_mut()? .entry(guild_id) .or_default() + .roles .insert( role.id, crate::db::Role { id: role.id, description, + reaction, }, ); m.react(&ctx, '👌').await?; @@ -216,7 +229,7 @@ async fn remove(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { if !DB::open(&*data) .borrow()? .get(&guild_id) - .map(|g| g.contains_key(&role.id)) + .map(|g| g.roles.contains_key(&role.id)) .unwrap_or(false) => { m.reply(&ctx, "This role does not exist in the assignable list.") @@ -227,6 +240,7 @@ async fn remove(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { .borrow_mut()? .entry(guild_id) .or_default() + .roles .remove(&role.id); m.react(&ctx, '👌').await?; } diff --git a/youmubot-core/src/db.rs b/youmubot-core/src/db.rs index 02d0bc8..9dcda2e 100644 --- a/youmubot-core/src/db.rs +++ b/youmubot-core/src/db.rs @@ -4,12 +4,13 @@ use serde::{Deserialize, Serialize}; use serenity::model::id::{RoleId, UserId}; use std::collections::HashMap; use youmubot_db::{GuildMap, DB}; +use youmubot_prelude::*; /// A list of SoftBans for all servers. pub type SoftBans = DB>; /// A list of assignable roles for all servers. -pub type Roles = DB>>; +pub type Roles = DB>; /// For the admin commands: /// - Each server might have a `soft ban` role implemented. @@ -33,9 +34,71 @@ impl ServerSoftBans { } } +/// Represents a server's role list. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct RoleList { + /// `reaction_message` handles the reaction-handling message. + pub reaction_message: Option, + pub roles: HashMap, +} + +/// Load the file list, handling migration from v1. +pub fn load_role_list( + map: &mut TypeMap, + path: impl AsRef, + v1_path: impl AsRef, +) -> Result<()> { + // Try to load v2 first + let v2 = Roles::load_from_path(path.as_ref()); + let v2 = match v2 { + Ok(v2) => { + map.insert::(v2); + return Ok(()); + } + Err(v2) => v2, + }; + // Try migrating from v1. + match legacy::RolesV1::load_from_path(v1_path.as_ref()) { + Ok(v1) => { + Roles::insert_into(map, path)?; + *Roles::open(&map).borrow_mut()? = v1 + .get_data(true)? + .into_iter() + .map(|(guild, roles)| { + ( + guild, + RoleList { + reaction_message: None, + roles, + }, + ) + }) + .collect(); + std::fs::remove_file(v1_path.as_ref()).pls_ok(); + eprintln!("Migrated roles v1 to v2."); + Ok(()) + } + Err(v1) => Err(Error::msg(format!( + "failed with v2 ({}) and v1 ({})", + v2, v1 + ))), + } +} + /// Role represents an assignable role. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Role { pub id: RoleId, pub description: String, + #[serde(default)] + pub reaction: Option, +} + +mod legacy { + use super::Role; + use serenity::model::id::RoleId; + use std::collections::HashMap; + use youmubot_db::{GuildMap, DB}; + /// (Depreciated) A list of assignable roles for all servers. + pub type RolesV1 = DB>>; } diff --git a/youmubot-core/src/lib.rs b/youmubot-core/src/lib.rs index c9db469..31f7385 100644 --- a/youmubot-core/src/lib.rs +++ b/youmubot-core/src/lib.rs @@ -23,7 +23,11 @@ pub fn setup( data: &mut TypeMap, ) -> 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"))?; + db::load_role_list( + &mut *data, + &path.join("roles_v2.yaml"), + &path.join("roles.yaml"), + )?; // Create handler threads tokio::spawn(admin::watch_soft_bans( diff --git a/youmubot-db/src/lib.rs b/youmubot-db/src/lib.rs index 69a44f6..ec512f5 100644 --- a/youmubot-db/src/lib.rs +++ b/youmubot-db/src/lib.rs @@ -23,6 +23,11 @@ impl T: Deserialize<'de>, { + /// Load the DB from a path. + pub fn load_from_path(path: impl AsRef) -> Result, DBError> { + Ok(Database::::load_from_path(path)?) + } + /// Insert into a ShareMap. pub fn insert_into(data: &mut TypeMap, path: impl AsRef) -> Result<(), DBError> { let db = Database::::load_from_path_or_default(path)?;