From 030d3b2f38e17de57cc1b402b4be14662fb4c25b Mon Sep 17 00:00:00 2001 From: Angad Tendulkar Date: Sat, 23 Dec 2023 15:49:51 -0500 Subject: [PATCH] /role command *custom and color role manipulation *new methods for Color struct *command descriptions (from inline docs) --- crates/bot/src/commands/impersonate.rs | 2 + crates/bot/src/commands/ping.rs | 1 + crates/bot/src/commands/role/color.rs | 99 +++++++++++++++ crates/bot/src/commands/role/custom.rs | 164 +++++++++++++++++++++++++ crates/bot/src/commands/role/mod.rs | 9 ++ crates/bot/src/error/errors.rs | 53 ++++++-- crates/logger/src/database/role.rs | 25 ++-- crates/logger/src/main.rs | 1 + crates/prelude/src/color.rs | 16 +++ crates/prisma/src/lib.rs | 2 +- crates/router/src/servers/logging.rs | 1 + schema.prisma | 5 +- 12 files changed, 358 insertions(+), 20 deletions(-) create mode 100644 crates/bot/src/commands/role/color.rs create mode 100644 crates/bot/src/commands/role/custom.rs create mode 100644 crates/bot/src/commands/role/mod.rs diff --git a/crates/bot/src/commands/impersonate.rs b/crates/bot/src/commands/impersonate.rs index 670b6033..b36ac9a1 100644 --- a/crates/bot/src/commands/impersonate.rs +++ b/crates/bot/src/commands/impersonate.rs @@ -5,6 +5,7 @@ pub async fn impersonate(_: Context<'_>) -> Result<(), CommandError> { Ok(()) } +/// Become someone else. Might be illegal. Who knows? #[poise::command(slash_command)] async fn set(ctx: Context<'_>, to: Member) -> Result<(), CommandError> { let mut cache = Client::::new().await?; @@ -42,6 +43,7 @@ async fn set(ctx: Context<'_>, to: Member) -> Result<(), CommandError> { Ok(()) } +/// Become yourself again. #[poise::command(slash_command)] async fn unset(ctx: Context<'_>) -> Result<(), CommandError> { let mut cache = Client::::new().await?; diff --git a/crates/bot/src/commands/ping.rs b/crates/bot/src/commands/ping.rs index 65bb0abb..cff1e92c 100644 --- a/crates/bot/src/commands/ping.rs +++ b/crates/bot/src/commands/ping.rs @@ -1,5 +1,6 @@ use crate::prelude::*; +/// Ping the bot to see if it's alive. #[poise::command(slash_command)] pub async fn ping(ctx: Context<'_>) -> Result<(), CommandError> { let sent = ctx.created_at().timestamp_millis(); diff --git a/crates/bot/src/commands/role/color.rs b/crates/bot/src/commands/role/color.rs new file mode 100644 index 00000000..1bac0ec4 --- /dev/null +++ b/crates/bot/src/commands/role/color.rs @@ -0,0 +1,99 @@ +use crate::prelude::*; + +/// Update the color or name of your color role. +#[poise::command(slash_command)] +pub async fn color( + ctx: Context<'_>, + name: Option, + color: Option, + #[description = "Update values for someone else. Admin only."] member: Option, +) -> Result<(), CommandError> { + let mut logger = Client::::new().await?; + + // check if member is admin + let LoggingResponse::MemberOk(prisma::data::UserData { + admin: is_admin, .. + }) = logger + .send(LoggingRequest::MemberRead(ctx.author().id)) + .await? + else { + bail!(RouterError::::InvalidResponse); + }; + + // needs admin to update someone else + if member.is_some() && !is_admin { + bail!(CommandError::AdminRequired); + } + + // get the member to update + let member = member + .map(|m| m.user) + .unwrap_or_else(|| ctx.author().clone()); + + // request member data + let LoggingResponse::MemberOk(member_data) = + logger.send(LoggingRequest::MemberRead(member.id)).await? + else { + bail!(RouterError::::InvalidResponse); + }; + + // get color role of member + let role_data = member_data + .roles()? + .into_iter() + .filter(|r| r.color_role) + .next() + .make_error(CommandError::NoColorRole( + member_data + .nickname + .as_ref() + .unwrap_or(&member_data.username) + .clone(), + ))?; + + let role_id = serenity::RoleId::new(role_data.id as u64); + + // update role + let mut new_name = &role_data.name; + let mut new_color = Color::try_from(&role_data.color)?; + let mut edit = serenity::EditRole::default() + .hoist(true) + .position(10) + .mentionable(false); + + if let Some(color) = color { + edit = edit.colour(color); + new_color = color; + } + + if let Some(name) = name.as_ref() { + edit = edit.name(name); + new_name = name; + } + + ctx.http() + .edit_role(nci::ID, role_id, &edit, Some("Oreo2: command: /role color")) + .await?; + + // create embed and reply + let embed = embed::default(EmbedStatus::Success) + .title("Oreo2 | Color Role") + .description(format!( + "Updated color role of {}: {}", + mention::create(member.id, MentionType::User), + mention::create(role_id, MentionType::Role) + )) + .fields(vec![ + ("Name", new_name, true), + ("Color", &new_color.to_hex(), true), + ]); + + let reply = poise::CreateReply::default() + .embed(embed) + .components(vec![share::row()]) + .ephemeral(true); + + ctx.send(reply).await?; + + Ok(()) +} diff --git a/crates/bot/src/commands/role/custom.rs b/crates/bot/src/commands/role/custom.rs new file mode 100644 index 00000000..01ffa08e --- /dev/null +++ b/crates/bot/src/commands/role/custom.rs @@ -0,0 +1,164 @@ +use crate::prelude::*; + +#[poise::command(slash_command, subcommands("add", "remove", "edit"))] +pub async fn custom(_: Context<'_>) -> Result<(), CommandError> { + Ok(()) +} + +/// Add a custom role. Admins only. +#[poise::command(slash_command, required_permissions = "MANAGE_ROLES")] +async fn add( + ctx: Context<'_>, + name: String, + color: Color, + add_to: Vec, +) -> Result<(), CommandError> { + // create role + let role = serenity::EditRole::default().name(&name).colour(color); + let role = ctx + .http() + .create_role(nci::ID, &role, Some("Oreo2: command: /role custom add")) + .await?; + + // add role to members + for member in add_to { + ctx.http() + .add_member_role( + nci::ID, + member.user.id, + role.id, + Some("Oreo2: command: /role custom add"), + ) + .await?; + } + + // send reply + let embed = embed::default(EmbedStatus::Success) + .title("Oreo2 | Custom Role | Add") + .description(format!( + "Successfully added role {}", + mention::create(role.id, MentionType::Role) + )) + .fields(vec![("Name", name, true), ("Color", color.to_hex(), true)]); + + let reply = poise::CreateReply::default() + .embed(embed) + .components(vec![share::row()]) + .ephemeral(true); + + ctx.send(reply).await?; + + // log: set custom + let mut logger = Client::::new().await?; + + logger + .send(LoggingRequest::RoleSetBlacklisted(role.id)) + .await?; + + Ok(()) +} + +/// Remove a custom role. Admins only. +#[poise::command(slash_command, required_permissions = "MANAGE_ROLES")] +async fn remove(ctx: Context<'_>, mut role: Role) -> Result<(), CommandError> { + role.delete(&ctx).await?; + + let embed = embed::default(EmbedStatus::Success) + .title("Oreo2 | Custom Role | Remove") + .description(format!( + "Successfully removed role {}", + mention::create(role.id, MentionType::Role) + )); + + let reply = poise::CreateReply::default() + .embed(embed) + .components(vec![share::row()]) + .ephemeral(true); + + ctx.send(reply).await?; + + let mut logger = Client::::new().await?; + + logger + .send(LoggingRequest::RoleDeleteBlacklisted(role.id)) + .await?; + + Ok(()) +} + +/// Edit a custom role. Admins only. +#[poise::command(slash_command, required_permissions = "MANAGE_ROLES")] +async fn edit( + ctx: Context<'_>, + role: Role, + name: Option, + color: Option, + add_to: Vec, + remove_from: Vec, +) -> Result<(), CommandError> { + // edit role + let mut edit = serenity::EditRole::default(); + + if let Some(name) = name.as_ref() { + edit = edit.name(name); + } + + if let Some(color) = color.as_ref() { + edit = edit.colour(*color); + } + + let role = ctx + .http() + .edit_role( + nci::ID, + role.id, + &edit, + Some("Oreo2: command: /role custom edit"), + ) + .await?; + + // add role to members + for member in add_to { + ctx.http() + .add_member_role( + nci::ID, + member.user.id, + role.id, + Some("Oreo2: command: /role custom edit"), + ) + .await?; + } + + // remove role from members + for member in remove_from { + ctx.http() + .remove_member_role( + nci::ID, + member.user.id, + role.id, + Some("Oreo2: command: /role custom edit"), + ) + .await?; + } + + // send reply + let embed = embed::default(EmbedStatus::Success) + .title("Oreo2 | Custom Role | Edit") + .description(format!( + "Successfully edited role {}", + mention::create(role.id, MentionType::Role) + )) + .fields(vec![ + ("Name", role.name, true), + ("Color", Color::from(role.colour).to_hex(), true), + ]); + + let reply = poise::CreateReply::default() + .embed(embed) + .components(vec![share::row()]) + .ephemeral(true); + + ctx.send(reply).await?; + + Ok(()) +} diff --git a/crates/bot/src/commands/role/mod.rs b/crates/bot/src/commands/role/mod.rs new file mode 100644 index 00000000..6c5332d1 --- /dev/null +++ b/crates/bot/src/commands/role/mod.rs @@ -0,0 +1,9 @@ +mod color; +mod custom; + +use crate::prelude::*; + +#[poise::command(slash_command, subcommands("color::color", "custom::custom"))] +pub async fn role(_: Context<'_>) -> Result<(), CommandError> { + Ok(()) +} diff --git a/crates/bot/src/error/errors.rs b/crates/bot/src/error/errors.rs index aa58d4da..8acfe5b0 100644 --- a/crates/bot/src/error/errors.rs +++ b/crates/bot/src/error/errors.rs @@ -5,7 +5,7 @@ use std::backtrace::Backtrace; #[derive(Error, Debug)] pub enum BotServerError { #[error("Problem starting logger: {error}")] - Logger { + SetLogger { #[from] error: SetLoggerError, backtrace: Backtrace, @@ -19,7 +19,7 @@ pub enum BotServerError { }, } -#[derive(Error, Debug)] +#[derive(Error, Debug, FromPrismaError)] pub enum CommandError { #[error("Serenity error: {error}")] Serenity { @@ -36,14 +36,21 @@ pub enum CommandError { }, #[error("Error starting server: {error}")] - Server { + StartServer { #[from] error: RouterError, backtrace: Backtrace, }, + #[error("Prisma error: {error}")] + Prisma { + #[from] + error: prisma::Error, + backtrace: Backtrace, + }, + #[error("Event error: {error}")] - EventError { + Event { #[from] error: EventError, backtrace: Backtrace, @@ -53,11 +60,37 @@ pub enum CommandError { IllegalArgument(String), #[error("Error communicating with cache server: {error}")] - CacheServerError { + CacheServer { #[from] error: RouterError, backtrace: Backtrace, }, + + #[error("Error communicating with logging server: {error}")] + LoggerServer { + #[from] + error: RouterError, + backtrace: Backtrace, + }, + + #[error("Admin permissions are necessary to run this command")] + AdminRequired, + + #[error("No color role for user: {0}")] + NoColorRole(String), + + #[error("Error parsing color: {error}")] + ColorParse { + #[from] + error: ColorParseError, + backtrace: Backtrace, + }, + + #[error("This command can only be used in a guild")] + NotInGuild, + + #[error("Role ({{ id: {0} }}) could not be found")] + RoleNotFound(RoleId), } #[derive(Error, Debug)] @@ -70,7 +103,7 @@ pub enum MessageCloneError { }, #[error("Logger error: {error}")] - Router { + LoggerServer { #[from] error: RouterError, backtrace: Backtrace, @@ -90,14 +123,14 @@ pub enum EventError { }, #[error("Error communicating with logging server: {error}")] - LoggingServerError { + LoggingServer { #[from] error: RouterError, backtrace: Backtrace, }, #[error("Error communicating with cache server: {error}")] - CacheServerError { + CacheServer { #[from] error: RouterError, backtrace: Backtrace, @@ -125,7 +158,7 @@ pub enum EventError { }, #[error("Error with starboard: {error}")] - StarboardError { + Starboard { #[from] error: StarboardError, backtrace: Backtrace, @@ -158,7 +191,7 @@ pub enum NewsCloneError { #[derive(Error, Debug)] pub enum StarboardError { #[error("Error with logging server: {error}")] - LoggingServerError { + LoggingServer { #[from] error: RouterError, backtrace: Backtrace, diff --git a/crates/logger/src/database/role.rs b/crates/logger/src/database/role.rs index 70e9a230..d6f22a06 100644 --- a/crates/logger/src/database/role.rs +++ b/crates/logger/src/database/role.rs @@ -11,8 +11,8 @@ pub async fn log_check_error(role: impl Into) -> Result<(), RoleLogError> { // custom roles handled seperately if prisma::create() .await? - .logless_roles() - .find_unique(logless_roles::id::equals(role)) + .logless_role() + .find_unique(logless_role::id::equals(role)) .exec() .await? .is_some() @@ -56,11 +56,7 @@ pub async fn set_blacklisted(role_id: serenity::RoleId) -> Result<(), RoleLogErr .exec() .await?; - prisma - .logless_roles() - .create(role_id, vec![]) - .exec() - .await?; + prisma.logless_role().create(role_id, vec![]).exec().await?; Ok(()) } @@ -146,6 +142,21 @@ pub async fn delete( Ok(()) } +pub async fn delete_blacklisted(role_id: serenity::RoleId) -> Result<(), RoleLogError> { + let prisma = prisma::create().await?; + + prisma + .logless_role() + .update( + logless_role::id::equals(role_id), + vec![logless_role::deleted::set(true)], + ) + .exec() + .await?; + + Ok(()) +} + pub async fn all() -> Result, RoleLogError> { let prisma = prisma::create().await?; diff --git a/crates/logger/src/main.rs b/crates/logger/src/main.rs index 443935a1..97cfacc1 100644 --- a/crates/logger/src/main.rs +++ b/crates/logger/src/main.rs @@ -52,6 +52,7 @@ async fn on(request: LoggingRequest) -> Result { RoleReadAll => role::all() => LoggingResponse::AllRolesOk(out), RoleUpdate(r) => role::update(r) => LoggingResponse::UpdateOk, RoleDelete(r) => role::delete(r, &mut bot) => LoggingResponse::UpdateOk, + RoleDeleteBlacklisted(r) => role::delete_blacklisted(r) => LoggingResponse::UpdateOk, // member MemberCreate(m) => member::create(m, &mut bot) => LoggingResponse::UpdateOk, diff --git a/crates/prelude/src/color.rs b/crates/prelude/src/color.rs index 99fff514..f34bb230 100644 --- a/crates/prelude/src/color.rs +++ b/crates/prelude/src/color.rs @@ -133,6 +133,22 @@ impl From for Color { } } +impl TryFrom for Color { + type Error = ColorParseError; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +impl TryFrom<&String> for Color { + type Error = ColorParseError; + + fn try_from(value: &String) -> Result { + Self::from_str(value) + } +} + impl FromStr for Color { type Err = ColorParseError; diff --git a/crates/prisma/src/lib.rs b/crates/prisma/src/lib.rs index 4e19327d..359b8359 100644 --- a/crates/prisma/src/lib.rs +++ b/crates/prisma/src/lib.rs @@ -10,7 +10,7 @@ mod generated; #[cfg(not(feature = "bin"))] pub mod prelude { pub use super::generated::{ - attachment, channel, channel_category, interaction, logless_roles, message, message_clone, + attachment, channel, channel_category, interaction, logless_role, message, message_clone, role, user, user_settings_data, ChannelType, InteractionType, MessageCloneReason, PrismaClient, }; diff --git a/crates/router/src/servers/logging.rs b/crates/router/src/servers/logging.rs index a1a28924..9f1f688a 100644 --- a/crates/router/src/servers/logging.rs +++ b/crates/router/src/servers/logging.rs @@ -28,6 +28,7 @@ pub enum LoggingRequest { RoleReadAll, RoleUpdate(Role), RoleDelete(RoleId), + RoleDeleteBlacklisted(RoleId), MemberCreate(Member), MemberRead(UserId), diff --git a/schema.prisma b/schema.prisma index 87d327ef..c712963f 100644 --- a/schema.prisma +++ b/schema.prisma @@ -159,8 +159,9 @@ model Interaction { custom_id String? } -model LoglessRoles { - id BigInt @id +model LoglessRole { + id BigInt @id + deleted Boolean @default(false) } model UserSettingsData {