Extended role list

This commit is contained in:
Natsu Kagami 2021-02-10 17:57:19 +09:00
parent 0bbd50386e
commit 72c574fb48
Signed by: nki
GPG key ID: 7306B3D3C3AD6E51
4 changed files with 107 additions and 21 deletions

View file

@ -1,7 +1,11 @@
use crate::db::Roles as DB; use crate::db::Roles as DB;
use serenity::{ use serenity::{
framework::standard::{macros::command, Args, CommandResult}, framework::standard::{macros::command, Args, CommandResult},
model::{channel::Message, guild::Role, id::RoleId}, model::{
channel::{Message, ReactionType},
guild::Role,
id::RoleId,
},
utils::MessageBuilder, utils::MessageBuilder,
}; };
use youmubot_prelude::*; use youmubot_prelude::*;
@ -18,7 +22,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
let roles = db let roles = db
.borrow()? .borrow()?
.get(&guild_id) .get(&guild_id)
.filter(|v| !v.is_empty()) .filter(|v| !v.roles.is_empty())
.cloned(); .cloned();
match roles { match roles {
None => { None => {
@ -27,6 +31,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
Some(v) => { Some(v) => {
let roles = guild_id.to_partial_guild(&ctx).await?.roles; let roles = guild_id.to_partial_guild(&ctx).await?.roles;
let roles: Vec<_> = v let roles: Vec<_> = v
.roles
.into_iter() .into_iter()
.filter_map(|(_, role)| roles.get(&role.id).cloned().map(|r| (r, role.description))) .filter_map(|(_, role)| roles.get(&role.id).cloned().map(|r| (r, role.description)))
.collect(); .collect();
@ -128,7 +133,7 @@ async fn toggle(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
if !DB::open(&*ctx.data.read().await) if !DB::open(&*ctx.data.read().await)
.borrow()? .borrow()?
.get(&guild_id) .get(&guild_id)
.map(|g| g.contains_key(&role.id)) .map(|g| g.roles.contains_key(&role.id))
.unwrap_or(false) => .unwrap_or(false) =>
{ {
m.reply(&ctx, "This role is not self-assignable. Check the `listroles` command to see which role can be assigned.").await?; 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")] #[command("addrole")]
#[description = "Add a role as the assignable role"] #[description = "Add a role as the assignable role. Overrides the old entry."]
#[usage = "{role-name-or-id} / {description}"] #[usage = "{role-name-or-id} / {description} / [representing emoji = none]"]
#[example = "hd820 / Headphones role"] #[example = "hd820 / Headphones role / 🎧"]
#[num_args(2)] #[min_args(2)]
#[max_args(3)]
#[required_permissions(MANAGE_ROLES)] #[required_permissions(MANAGE_ROLES)]
#[only_in(guilds)] #[only_in(guilds)]
async fn add(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { async fn add(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
let role = args.single_quoted::<String>()?; let role = args.single_quoted::<String>()?;
let data = ctx.data.read().await; let data = ctx.data.read().await;
let description = args.single::<String>()?; let description = args.single_quoted::<String>()?;
let reaction = match args.single::<ReactionType>() {
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 guild_id = m.guild_id.unwrap();
let roles = guild_id.to_partial_guild(&ctx).await?.roles; let roles = guild_id.to_partial_guild(&ctx).await?.roles;
let role = role_from_string(&role, &roles); let role = role_from_string(&role, &roles);
@ -167,26 +188,18 @@ async fn add(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
None => { None => {
m.reply(&ctx, "No such role exists").await?; 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) => { Some(role) => {
DB::open(&*data) DB::open(&*data)
.borrow_mut()? .borrow_mut()?
.entry(guild_id) .entry(guild_id)
.or_default() .or_default()
.roles
.insert( .insert(
role.id, role.id,
crate::db::Role { crate::db::Role {
id: role.id, id: role.id,
description, description,
reaction,
}, },
); );
m.react(&ctx, '👌').await?; m.react(&ctx, '👌').await?;
@ -216,7 +229,7 @@ async fn remove(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
if !DB::open(&*data) if !DB::open(&*data)
.borrow()? .borrow()?
.get(&guild_id) .get(&guild_id)
.map(|g| g.contains_key(&role.id)) .map(|g| g.roles.contains_key(&role.id))
.unwrap_or(false) => .unwrap_or(false) =>
{ {
m.reply(&ctx, "This role does not exist in the assignable list.") 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()? .borrow_mut()?
.entry(guild_id) .entry(guild_id)
.or_default() .or_default()
.roles
.remove(&role.id); .remove(&role.id);
m.react(&ctx, '👌').await?; m.react(&ctx, '👌').await?;
} }

View file

@ -4,12 +4,13 @@ use serde::{Deserialize, Serialize};
use serenity::model::id::{RoleId, UserId}; use serenity::model::id::{RoleId, UserId};
use std::collections::HashMap; use std::collections::HashMap;
use youmubot_db::{GuildMap, DB}; use youmubot_db::{GuildMap, DB};
use youmubot_prelude::*;
/// A list of SoftBans for all servers. /// A list of SoftBans for all servers.
pub type SoftBans = DB<GuildMap<ServerSoftBans>>; pub type SoftBans = DB<GuildMap<ServerSoftBans>>;
/// A list of assignable roles for all servers. /// A list of assignable roles for all servers.
pub type Roles = DB<GuildMap<HashMap<RoleId, Role>>>; pub type Roles = DB<GuildMap<RoleList>>;
/// For the admin commands: /// For the admin commands:
/// - Each server might have a `soft ban` role implemented. /// - 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<serenity::model::id::MessageId>,
pub roles: HashMap<RoleId, Role>,
}
/// Load the file list, handling migration from v1.
pub fn load_role_list(
map: &mut TypeMap,
path: impl AsRef<std::path::Path>,
v1_path: impl AsRef<std::path::Path>,
) -> Result<()> {
// Try to load v2 first
let v2 = Roles::load_from_path(path.as_ref());
let v2 = match v2 {
Ok(v2) => {
map.insert::<Roles>(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. /// Role represents an assignable role.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Role { pub struct Role {
pub id: RoleId, pub id: RoleId,
pub description: String, pub description: String,
#[serde(default)]
pub reaction: Option<serenity::model::channel::ReactionType>,
}
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<GuildMap<HashMap<RoleId, Role>>>;
} }

View file

@ -23,7 +23,11 @@ pub fn setup(
data: &mut TypeMap, data: &mut TypeMap,
) -> serenity::framework::standard::CommandResult { ) -> serenity::framework::standard::CommandResult {
db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?; 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 // Create handler threads
tokio::spawn(admin::watch_soft_bans( tokio::spawn(admin::watch_soft_bans(

View file

@ -23,6 +23,11 @@ impl<T: std::any::Any + Default + Send + Sync + Clone + Serialize + std::fmt::De
where where
for<'de> T: Deserialize<'de>, for<'de> T: Deserialize<'de>,
{ {
/// Load the DB from a path.
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Database<T>, DBError> {
Ok(Database::<T>::load_from_path(path)?)
}
/// Insert into a ShareMap. /// Insert into a ShareMap.
pub fn insert_into(data: &mut TypeMap, path: impl AsRef<Path>) -> Result<(), DBError> { pub fn insert_into(data: &mut TypeMap, path: impl AsRef<Path>) -> Result<(), DBError> {
let db = Database::<T>::load_from_path_or_default(path)?; let db = Database::<T>::load_from_path_or_default(path)?;