diff --git a/Cargo.lock b/Cargo.lock index 3d97f8d..f941a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1437,7 +1437,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "payphone" -version = "4.1.1" +version = "4.2.3" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 0d41cd3..1fcf241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "payphone" -version = "4.1.1" +version = "4.2.3" edition = "2024" default-run = "payphone" diff --git a/playbooks/09-payphone.yml b/playbooks/09-payphone.yml index 89be672..94c4f7c 100644 --- a/playbooks/09-payphone.yml +++ b/playbooks/09-payphone.yml @@ -103,7 +103,9 @@ "--database-password", "cassandra", "--redis-urls", - "redis://redis-cluster.redis" + "redis://redis-cluster.redis", + "--bot-owner-id", + "1357199907512914051" ] envFrom: - secretRef: diff --git a/src/database/models.rs b/src/database/models.rs index 46876e2..609488c 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1614,4 +1614,26 @@ impl UserIncident { Ok(()) } + + pub async fn delete_by_user_and_incident_id( + user_id: u64, + incident_id: u64, + session: &Session, + ) -> Result<(), DatabaseError> { + let user_id = u64_to_i64(user_id); + let incident_id = u64_to_i64(incident_id); + + session + .query_unpaged( + r#" + DELETE FROM payphone.user_incidents + WHERE user_id = ? AND incident_id = ?; + "#, + (user_id, incident_id), + ) + .await + .map_err(DatabaseError::QueryError)?; + + Ok(()) + } } diff --git a/src/framework/commands/arguments.rs b/src/framework/commands/arguments.rs index 3cb2277..fac6c9d 100644 --- a/src/framework/commands/arguments.rs +++ b/src/framework/commands/arguments.rs @@ -5,7 +5,7 @@ use std::{ use thiserror::Error; -use crate::framework::commands::context::CommandContext; +use crate::{database::models::DatabaseError, framework::commands::context::CommandContext}; #[derive(Debug, Error)] pub enum ArgumentParsingError { @@ -16,7 +16,7 @@ pub enum ArgumentParsingError { InvalidArgument, #[error("A database operation failed")] - DatabaseFailed, + DatabaseFailed(#[from] DatabaseError), } pub trait FromArguments: Sized { @@ -218,7 +218,10 @@ impl ParseArguments for Arg where T: FromArguments, { - async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result { + async fn parse( + args: &str, + ctx: &CommandContext<'_, S, E>, + ) -> Result { let (value, _) = T::from_args(args, ctx).await?; Ok(Arg(value)) @@ -229,7 +232,10 @@ impl ParseArguments for (A,) where A: FromArguments, { - async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result { + async fn parse( + args: &str, + ctx: &CommandContext<'_, S, E>, + ) -> Result { let (a, _) = A::from_args(args, ctx).await?; Ok((a,)) @@ -241,7 +247,10 @@ where A: FromArguments, B: FromArguments, { - async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result { + async fn parse( + args: &str, + ctx: &CommandContext<'_, S, E>, + ) -> Result { let (a, args) = A::from_args(args, ctx).await?; let (b, _) = B::from_args(args, ctx).await?; @@ -255,7 +264,10 @@ where B: FromArguments, C: FromArguments, { - async fn parse(args: &str, ctx: &CommandContext<'_, S, E>) -> Result { + async fn parse( + args: &str, + ctx: &CommandContext<'_, S, E>, + ) -> Result { let (a, args) = A::from_args(args, ctx).await?; let (b, args) = B::from_args(args, ctx).await?; let (c, _) = C::from_args(args, ctx).await?; diff --git a/src/framework/commands/context.rs b/src/framework/commands/context.rs index 20fe4ad..609ce04 100644 --- a/src/framework/commands/context.rs +++ b/src/framework/commands/context.rs @@ -1,8 +1,15 @@ use std::sync::Arc; -use twilight_model::gateway::payload::incoming::MessageCreate; +use twilight_http::{Error, Response}; +use twilight_model::{ + channel::{Message, message::Component}, + gateway::payload::incoming::MessageCreate, +}; -use crate::framework::{commands::messages::MessageSender, runtime::Runtime}; +use crate::framework::{ + commands::messages::MessageSender, interactions::components::ActionRow, + messages::MessageBuilder, runtime::Runtime, +}; pub struct CommandContext<'a, T, E> { pub event: &'a MessageCreate, @@ -33,4 +40,59 @@ where pub fn send(&self) -> MessageSender { MessageSender::new(self.event.channel_id, self.rt.clone()) } + + pub async fn send_from_builder( + &self, + builder: MessageBuilder, + ) -> Result, Error> { + let mut sender = self.rt.http.create_message(self.event.channel_id); + + if let Some(content) = &builder.content { + sender = sender.content(content); + } + + let mut current_row = ActionRow::new(&[]); + let mut current_index = 0; + + let mut rows: Vec = vec![]; + let mut custom_ids: Vec = vec![]; + + for (component, index) in builder.components { + if index != current_index { + if !current_row.is_empty() { + rows.push(current_row.build()); + current_row = ActionRow::new(&[]); + } + + current_index += 1; + } + + let custom_id = component.custom_id(); + + if let Some(custom_id) = &custom_id { + custom_ids.push(custom_id.clone()); + + self.rt.registry.register(custom_id, component.clone()); + } + + current_row.add(custom_id.clone(), component.clone()); + } + + if !current_row.is_empty() { + rows.push(current_row.build()); + } + + sender = sender.components(&rows); + sender = sender.embeds(&builder.embeds); + + let result = sender.await; + + if result.is_err() { + for custom_id in custom_ids { + self.rt.registry.unregister(custom_id); + } + } + + result + } } diff --git a/src/framework/commands/mod.rs b/src/framework/commands/mod.rs index d902a31..414b505 100644 --- a/src/framework/commands/mod.rs +++ b/src/framework/commands/mod.rs @@ -5,9 +5,9 @@ use deadpool_redis::{cluster::PoolError, redis::RedisError}; use thiserror::Error; use twilight_http::response::DeserializeBodyError; -use crate::framework::{commands::{ +use crate::{database::models::{DatabaseError, DatabaseOrDiscordError}, framework::{commands::{ arguments::Argument, context::CommandContext, permissions::Permission, -}, runtime::Runtime}; +}, runtime::Runtime}}; pub mod arguments; pub mod context; @@ -18,11 +18,8 @@ pub mod runner; #[derive(Debug, Error)] pub enum CommandError { - #[error("A message was attempted to be sent but it failed")] - MessageSendingFailed, - #[error("A database query failed")] - DatabaseFailed, + DatabaseFailed(#[from] DatabaseError), #[error("A database operation failed due to an inconsistent state")] DatabaseInconsistentState, @@ -41,6 +38,9 @@ pub enum CommandError { #[error(transparent)] RedisFailed(#[from] RedisError), + + #[error(transparent)] + DatabaseOrDiscordError(#[from] DatabaseOrDiscordError) } #[async_trait] diff --git a/src/framework/interactions/components.rs b/src/framework/interactions/components.rs index c588f99..2264e76 100644 --- a/src/framework/interactions/components.rs +++ b/src/framework/interactions/components.rs @@ -64,7 +64,7 @@ where fn build(&self, id: Option) -> TwilightComponent { TwilightComponent::Button(TwilightButton { custom_id: id, - disabled: false, + disabled: self.disabled(), emoji: None, label: self.label().map(|i| i.to_string()), style: if self.url().is_none() { @@ -95,16 +95,23 @@ where None } } + fn style(&self) -> ButtonStyle { ButtonStyle::Secondary } - fn label(&self) -> Option<&'static str> { + + fn label(&self) -> Option { None } - fn url(&self) -> Option<&'static str> { + + fn url(&self) -> Option { None } + fn disabled(&self) -> bool { + false + } + async fn on_press(&self, ctx: InteractionContext) -> Result<(), InteractionError> { ctx.defer().await } diff --git a/src/framework/interactions/context.rs b/src/framework/interactions/context.rs index b11d001..871a86a 100644 --- a/src/framework/interactions/context.rs +++ b/src/framework/interactions/context.rs @@ -1,13 +1,16 @@ use std::sync::Arc; +use twilight_http::{Error, Response, response::marker::EmptyBody}; use twilight_model::{ gateway::payload::incoming::InteractionCreate, http::interaction::{InteractionResponse, InteractionResponseType}, - id::{Id, marker::InteractionMarker}, }; +use twilight_util::builder::InteractionResponseDataBuilder; use crate::framework::{ - commands::messages::OriginalMessageUpdater, interactions::InteractionError, runtime::Runtime, + interactions::{InteractionError, components::ActionRow}, + messages::MessageEditBuilder, + runtime::Runtime, }; pub struct InteractionContext { @@ -17,7 +20,11 @@ pub struct InteractionContext { pub extras: E, } -impl InteractionContext { +impl InteractionContext +where + T: Send + Sync + 'static, + E: Send + Sync + 'static, +{ pub fn new( event: InteractionCreate, rt: Arc>, @@ -44,37 +51,85 @@ impl InteractionContext { kind: InteractionResponseType::DeferredUpdateMessage, }, ) - .await - .map_err(|_| InteractionError::DeferringFailed)?; + .await?; Ok(()) } - pub fn original_message(&self) -> Option> { - self.event.message.as_ref().map(|_| OriginalMessage { - interaction_id: self.event.id, - interaction_token: self.event.token.clone(), - rt: self.rt.clone(), - }) - } -} - -pub struct OriginalMessage { - interaction_id: Id, - interaction_token: String, - rt: Arc>, -} - -impl OriginalMessage -where - T: Send + Sync + 'static, - E: Send + Sync + 'static, -{ - pub fn edit(&self) -> OriginalMessageUpdater { - OriginalMessageUpdater::new( - self.interaction_id, - self.interaction_token.clone(), - self.rt.clone(), - ) + pub async fn edit_original_message( + &self, + builder: MessageEditBuilder, + ) -> Result, Error> { + let mut sender = InteractionResponseDataBuilder::new(); + + if let Some(content) = &builder.content { + if let Some(content) = content { + sender = sender.content(content); + } else { + sender = sender.content(""); + } + } + + let mut current_row = ActionRow::new(&[]); + let mut current_index = 0; + + let mut custom_ids: Vec = vec![]; + + if let Some(components) = builder.components { + let mut rows = vec![]; + + for (component, index) in components { + if index != current_index { + if !current_row.is_empty() { + rows.push(current_row.build()); + current_row = ActionRow::new(&[]); + } + + current_index += 1; + } + + let custom_id = component.custom_id(); + + if let Some(custom_id) = &custom_id { + custom_ids.push(custom_id.clone()); + + self.rt.registry.register(custom_id, component.clone()); + } + + current_row.add(custom_id.clone(), component.clone()); + } + + if !current_row.is_empty() { + rows.push(current_row.build()); + } + + sender = sender.components(rows); + } + + if let Some(embeds) = builder.embeds { + sender = sender.embeds(embeds); + } + + let result = self + .rt + .http + .interaction(self.rt.app.id) + .create_response( + self.event.id, + &self.event.token, + &InteractionResponse { + data: Some(sender.build()), + kind: InteractionResponseType::UpdateMessage, + }, + ) + .await; + + if result.is_err() { + for custom_id in custom_ids { + self.rt.registry.unregister(custom_id); + } + } + + result } } diff --git a/src/framework/interactions/mod.rs b/src/framework/interactions/mod.rs index bd406d1..dc048f4 100644 --- a/src/framework/interactions/mod.rs +++ b/src/framework/interactions/mod.rs @@ -1,5 +1,7 @@ use thiserror::Error; +use crate::database::models::DatabaseError; + pub mod components; pub mod context; pub mod registry; @@ -12,9 +14,6 @@ pub enum InteractionError { )] InteractionNotFound, - #[error("The interaction was attempted to be deferred, but it failed to be done")] - DeferringFailed, - #[error("Discord responded with an interaction type Payphone does not use")] InteractionTypeUnsent, @@ -26,4 +25,10 @@ pub enum InteractionError { #[error("The extras getter failed to retrieve extras")] ExtrasGetterFailed, + + #[error(transparent)] + DatabaseError(#[from] DatabaseError), + + #[error(transparent)] + DiscordAPIError(#[from] twilight_http::Error), } diff --git a/src/framework/interactions/registry.rs b/src/framework/interactions/registry.rs index d09045c..c401c06 100644 --- a/src/framework/interactions/registry.rs +++ b/src/framework/interactions/registry.rs @@ -47,7 +47,10 @@ where if let Some(data) = &event.data { match data { InteractionData::MessageComponent(data) => { - if let Some(component) = self.inner.get_async(&data.custom_id).await { + if let Some(guarded_component) = self.inner.get(&data.custom_id) { + let component = guarded_component.clone(); + drop(guarded_component); + let prefixes = rt .get_prefixes( event.guild_id.map(|id| id.get()), @@ -60,7 +63,7 @@ where let extras = extras .get_extra( - &event.author().expect("Component interaction had no author"), + event.author().expect("Component interaction had no author"), &rt, ) .await @@ -77,6 +80,10 @@ where extras, )) .await + .inspect(|_| log::debug!("Executed component interaction")) + .inspect_err(|e| { + log::error!("Interaction execution failed: {e:?}") + }) } else { Err(InteractionError::InteractionNotFound) } diff --git a/src/framework/messages.rs b/src/framework/messages.rs new file mode 100644 index 0000000..cf83d15 --- /dev/null +++ b/src/framework/messages.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; + +use twilight_model::channel::message::Embed; + +use crate::framework::interactions::components::Component; + +type ComponentWithRowIndex = (Arc>, usize); + +pub struct MessageBuilder { + pub content: Option, + pub embeds: Vec, + pub components: Vec>, +} + +impl MessageBuilder { + pub fn new() -> Self { + Self { + content: None, + embeds: Vec::new(), + components: Vec::new(), + } + } + + pub fn content(mut self, content: impl Into) -> Self { + self.content = Some(content.into()); + + self + } + + pub fn embed(mut self, embed: Embed) -> Self { + self.embeds.push(embed); + + self + } + + pub fn component(mut self, component: impl Component + 'static, row: usize) -> Self { + self.components.push((Arc::new(component), row)); + + self + } +} + +pub struct MessageEditBuilder { + pub content: Option>, + pub embeds: Option>, + pub components: Option>>, +} + +impl MessageEditBuilder { + pub fn new() -> Self { + Self { + content: None, + embeds: None, + components: None, + } + } + + pub fn content(mut self, content: impl Into) -> Self { + self.content = Some(Some(content.into())); + + self + } + + pub fn embed(mut self, embed: Embed) -> Self { + if self.embeds.is_none() { + self.embeds = Some(Vec::new()); + } + + self.embeds.as_mut().unwrap().push(embed); + + self + } + + pub fn component(mut self, component: impl Component + 'static, row: usize) -> Self { + if self.components.is_none() { + self.components = Some(Vec::new()); + } + + self.components + .as_mut() + .unwrap() + .push((Arc::new(component), row)); + + self + } +} + +impl From> for MessageEditBuilder { + fn from(builder: MessageBuilder) -> Self { + Self { + content: builder.content.map(Some), + embeds: Some(builder.embeds), + components: Some(builder.components), + } + } +} diff --git a/src/framework/mod.rs b/src/framework/mod.rs index 3823c8e..1b19238 100644 --- a/src/framework/mod.rs +++ b/src/framework/mod.rs @@ -76,8 +76,7 @@ //! // Your command logic here. //! ctx.send() //! .content("Hello, world!") -//! .await -//! .map_err(|_| CommandError::MessageSendingFailed)?; +//! .await?; //! //! Ok(()) //! } @@ -149,8 +148,7 @@ //! //! ctx.send() //! .content(format!("Hello, {}!", name.0)) -//! .await -//! .map_err(|_| CommandError::MessageSendingFailed)?; +//! .await?; //! //! Ok(()) //! } @@ -161,4 +159,5 @@ pub mod bot; pub mod commands; pub mod events; pub mod interactions; +pub mod messages; pub mod runtime; diff --git a/src/groups/about/commands.rs b/src/groups/about/commands.rs index 291917a..84c87e1 100644 --- a/src/groups/about/commands.rs +++ b/src/groups/about/commands.rs @@ -51,15 +51,13 @@ impl Command for HelpCommand { .component(HelpGoBackButton, 1) .component(HelpSupportLinkButton, 1) .await - .inspect_err(|e| log::error!("Failed to send message! {e}")) - .map_err(|_| CommandError::MessageSendingFailed)?; + .inspect_err(|e| log::error!("Failed to send message! {e}"))?; } else { ctx.send() .embed(get_command_not_found_embed(&ctx.rt, command_name.0)) .component(HelpGoBackButton, 1) .component(HelpSupportLinkButton, 1) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; } } else { ctx.send() @@ -67,8 +65,7 @@ impl Command for HelpCommand { .component(HelpWebsiteLinkButton, 1) .component(HelpSupportLinkButton, 1) .component(HelpDonateLinkButton, 1) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; } Ok(()) diff --git a/src/groups/about/components.rs b/src/groups/about/components.rs index 458b986..7d79801 100644 --- a/src/groups/about/components.rs +++ b/src/groups/about/components.rs @@ -2,7 +2,10 @@ use async_trait::async_trait; use crate::{ extras::Extras, - framework::interactions::{InteractionError, components::Button, context::InteractionContext}, + framework::{ + interactions::{InteractionError, components::Button, context::InteractionContext}, + messages::MessageEditBuilder, + }, groups::about::embeds::get_all_command_help_embed, state::State, }; @@ -12,8 +15,12 @@ pub struct HelpGoBackButton; #[async_trait] impl Button for HelpGoBackButton { - fn label(&self) -> Option<&'static str> { - Some("← Go back to commands list") + fn id(&self) -> Option { + Some("help:go-back".into()) + } + + fn label(&self) -> Option { + Some("← Go back to commands list".into()) } async fn on_press( @@ -21,16 +28,15 @@ impl Button for HelpGoBackButton { ctx: InteractionContext, ) -> Result<(), InteractionError> { ctx - .original_message() - .expect( - "Buttons always have the original message in the interaction context, this one didn't (got None)" + .edit_original_message( + MessageEditBuilder::new().embed( + get_all_command_help_embed(&ctx.prefix, &ctx.rt, &ctx.extras) + ) + .component(HelpWebsiteLinkButton, 1) + .component(HelpSupportLinkButton, 1) + .component(HelpDonateLinkButton, 1) + .component(CodeOfConductLinkButton, 1) ) - .edit() - .embed(get_all_command_help_embed(&ctx.prefix, &ctx.rt, &ctx.extras)) - .component(HelpWebsiteLinkButton, 1) - .component(HelpSupportLinkButton, 1) - .component(HelpDonateLinkButton, 1) - .component(CodeOfConductLinkButton, 1) .await .inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}")) .map_err( @@ -46,12 +52,12 @@ pub struct HelpSupportLinkButton; #[async_trait] impl Button for HelpSupportLinkButton { - fn label(&self) -> Option<&'static str> { - Some("Get help") + fn label(&self) -> Option { + Some("Get help".into()) } - fn url(&self) -> Option<&'static str> { - Some("https://discord.gg/9daSsrfTmE") + fn url(&self) -> Option { + Some("https://discord.gg/9daSsrfTmE".into()) } } @@ -60,12 +66,12 @@ pub struct HelpWebsiteLinkButton; #[async_trait] impl Button for HelpWebsiteLinkButton { - fn label(&self) -> Option<&'static str> { - Some("Website") + fn label(&self) -> Option { + Some("Website".into()) } - fn url(&self) -> Option<&'static str> { - Some("https://payphone.lat/") + fn url(&self) -> Option { + Some("https://payphone.lat/".into()) } } @@ -74,12 +80,12 @@ pub struct HelpDonateLinkButton; #[async_trait] impl Button for HelpDonateLinkButton { - fn label(&self) -> Option<&'static str> { - Some("Donate") + fn label(&self) -> Option { + Some("Donate".into()) } - fn url(&self) -> Option<&'static str> { - Some("https://ko-fi.com/payphonedev") + fn url(&self) -> Option { + Some("https://ko-fi.com/payphonedev".into()) } } @@ -88,11 +94,11 @@ pub struct CodeOfConductLinkButton; #[async_trait] impl Button for CodeOfConductLinkButton { - fn label(&self) -> Option<&'static str> { - Some("Code of Conduct") + fn label(&self) -> Option { + Some("Code of Conduct".into()) } - fn url(&self) -> Option<&'static str> { - Some("https://payphone.lat/code-of-conduct") + fn url(&self) -> Option { + Some("https://payphone.lat/code-of-conduct".into()) } } diff --git a/src/groups/calls/commands.rs b/src/groups/calls/commands.rs index 3713c47..7fe214d 100644 --- a/src/groups/calls/commands.rs +++ b/src/groups/calls/commands.rs @@ -66,8 +66,7 @@ impl Command for CallCommand { .await .inspect_err(|e| { log::error!("Failed to get or create channel: {e:?}"); - }) - .map_err(|_| CommandError::DatabaseFailed)?; + })?; if channel.is_searching || self.channel_lock.contains(&ctx.event.channel_id.get()) { ctx.send() @@ -75,8 +74,7 @@ impl Command for CallCommand { "☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); } @@ -85,27 +83,21 @@ impl Command for CallCommand { ctx.send() .content(format!("☎️ You are already on a call! Use `{}hangup` to hang up the phone, or `{}skip` to skip to the next call.", ctx.prefix, ctx.prefix)) .await - .map_err(|_| CommandError::MessageSendingFailed)?; + ?; return Ok(()); } - self.channel_lock - .insert_async(ctx.event.channel_id.get()) - .await - .bye(); + self.channel_lock.insert(ctx.event.channel_id.get()).bye(); let call = Call::start(&channel, &ctx.rt.state.db) .await .inspect_err(|e| { log::error!("Failed to start call: {e:?}"); self.channel_lock.remove(&ctx.event.channel_id.get()); - }) - .map_err(|_| CommandError::DatabaseFailed)?; + })?; - self.channel_lock - .remove_async(&ctx.event.channel_id.get()) - .await; + self.channel_lock.remove(&ctx.event.channel_id.get()); if let Some(call) = call { ctx.send() @@ -130,8 +122,7 @@ impl Command for CallCommand { get_string_tips(ctx.prefix), get_string_call_searching(ctx.prefix) )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; } Ok(()) @@ -168,8 +159,7 @@ impl Command for SkipCommand { .await .inspect_err(|e| { log::error!("Failed to get or create channel: {e:?}"); - }) - .map_err(|_| CommandError::DatabaseFailed)?; + })?; if channel.call_id.is_none() { ctx.send() @@ -177,8 +167,7 @@ impl Command for SkipCommand { "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); } @@ -189,22 +178,19 @@ impl Command for SkipCommand { "☎️ You're already searching for a call! Use `{}hangup` to cancel the search.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); } let call = Call::get_by_id(channel.call_id.unwrap(), &ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to get call: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)? + .inspect_err(|e| log::error!("Failed to get call: {e:?}"))? .ok_or(CommandError::DatabaseInconsistentState)?; call.hangup(&ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to hang up call: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)?; + .inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?; let other_channel_id = call.other_party_channel_id(ctx.event.channel_id.get()); @@ -218,15 +204,13 @@ impl Command for SkipCommand { .await .inspect_err(|e| { log::error!("Failed to send message to other party: {e:?}"); - }) - .map_err(|_| CommandError::MessageSendingFailed)?; + })?; let call = Call::start(&channel, &ctx.rt.state.db) .await .inspect_err(|e| { log::error!("Failed to start (skipped) call: {e:?}"); - }) - .map_err(|_| CommandError::DatabaseFailed)?; + })?; if let Some(call) = call { ctx.send() @@ -248,8 +232,7 @@ impl Command for SkipCommand { } else { ctx.send() .content("☎️ Skipped! Searching for a call...") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; } Ok(()) @@ -280,16 +263,14 @@ impl Command for HangupCommand { ) -> Result<(), CommandError> { let Some(channel) = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to get channel: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)? + .inspect_err(|e| log::error!("Failed to get channel: {e:?}"))? else { ctx.send() .content(format!( "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); }; @@ -300,13 +281,11 @@ impl Command for HangupCommand { .await .inspect_err(|e| { log::error!("Failed to cancel search: {e:?}"); - }) - .map_err(|_| CommandError::DatabaseFailed)?; + })?; ctx.send() .content(format!("☎️ {}", get_string_call_no_answer(ctx.prefix))) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); } @@ -314,21 +293,16 @@ impl Command for HangupCommand { if let Some(call_id) = channel.call_id { let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to get call: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)? + .inspect_err(|e| log::error!("Failed to get call: {e:?}"))? else { - ctx.send() - .content("❌ Call not found.") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + ctx.send().content("❌ Call not found.").await?; return Err(CommandError::DatabaseInconsistentState); }; call.hangup(&ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to hang up call: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)?; + .inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?; ctx.send() .content(format!( @@ -345,16 +319,14 @@ impl Command for HangupCommand { "☎️ {}", get_string_call_hangup_other_party(ctx.prefix) )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; } else { ctx.send() .content(format!( "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); } @@ -391,8 +363,7 @@ impl Command for FriendRequestCommand { ) -> Result<(), CommandError> { let channel = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to get channel: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)?; + .inspect_err(|e| log::error!("Failed to get channel: {e:?}"))?; let Some(channel) = channel else { ctx.send() @@ -400,8 +371,7 @@ impl Command for FriendRequestCommand { "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); }; @@ -409,16 +379,14 @@ impl Command for FriendRequestCommand { let call = if let Some(call_id) = channel.call_id { Call::get_by_id(call_id, &ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to get call: {e:?}")) - .map_err(|_| CommandError::DatabaseFailed)? + .inspect_err(|e| log::error!("Failed to get call: {e:?}"))? } else { ctx.send() .content(format!( "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Ok(()); }; @@ -426,8 +394,7 @@ impl Command for FriendRequestCommand { let Some(call) = call else { ctx.send() .content("❌ Oops! We could not find that call.") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; return Err(CommandError::DatabaseInconsistentState); }; @@ -450,12 +417,11 @@ impl Command for FriendRequestCommand { ) ) .await - .map_err(|_| CommandError::MessageSendingFailed)?; + ?; ctx.send() .content("📬 Asked the other party to send you a friend request!") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; Ok(()) } diff --git a/src/groups/calls/components.rs b/src/groups/calls/components.rs index 2b24a7b..7326be8 100644 --- a/src/groups/calls/components.rs +++ b/src/groups/calls/components.rs @@ -4,12 +4,12 @@ use crate::{extras::Extras, framework::interactions::components::Button, state:: pub struct CodeOfConductButton; impl Button for CodeOfConductButton { - fn label(&self) -> Option<&'static str> { - Some("Code of Conduct") + fn label(&self) -> Option { + Some("Code of Conduct".into()) } - fn url(&self) -> Option<&'static str> { - Some("https://payphone.lat/code-of-conduct") + fn url(&self) -> Option { + Some("https://payphone.lat/code-of-conduct".into()) } } @@ -17,11 +17,11 @@ impl Button for CodeOfConductButton { pub struct NeedHelpButton; impl Button for NeedHelpButton { - fn label(&self) -> Option<&'static str> { - Some("Need Help?") + fn label(&self) -> Option { + Some("Need Help?".into()) } - fn url(&self) -> Option<&'static str> { - Some("https://discord.gg/9daSsrfTmE") + fn url(&self) -> Option { + Some("https://discord.gg/9daSsrfTmE".into()) } } diff --git a/src/groups/calls/permissions.rs b/src/groups/calls/permissions.rs index 2435d77..485c539 100644 --- a/src/groups/calls/permissions.rs +++ b/src/groups/calls/permissions.rs @@ -16,8 +16,7 @@ impl Permission for NotBannedPermission { async fn check(&self, ctx: &CommandContext) -> Result { let incidents = UserIncident::get_for_user(ctx.event.author.id.get(), &ctx.rt.state.db) .await - .inspect_err(|e| log::error!("Failed to get user incidents: {e}")) - .map_err(|_| CommandError::DatabaseFailed)?; + .inspect_err(|e| log::error!("Failed to get user incidents: {e}"))?; if incidents.iter().any(|i| i.is_active()) { Ok(false) @@ -37,8 +36,7 @@ impl Permission for NotBannedPermission { .await .inspect_err(|e| { log::error!("Failed to send message: {e}"); - }) - .map_err(|_| CommandError::MessageSendingFailed)?; + })?; Ok(()) } diff --git a/src/groups/leveling/commands.rs b/src/groups/leveling/commands.rs index 6c7648f..5803009 100644 --- a/src/groups/leveling/commands.rs +++ b/src/groups/leveling/commands.rs @@ -6,7 +6,13 @@ use twilight_util::builder::embed::{ }; use crate::{ - constants::COLOR_WHITE, database::models::User, discord::{images, sanitizing::SanitizeDiscordMarkdown}, extras::Extras, framework::commands::{context::CommandContext, Command, CommandError}, groups::leveling::constants::KEY_LEADERBOARD_MONTHLY, state::State + constants::COLOR_WHITE, + database::models::User, + discord::{images, sanitizing::SanitizeDiscordMarkdown}, + extras::Extras, + framework::commands::{Command, CommandError, context::CommandContext}, + groups::leveling::constants::KEY_LEADERBOARD_MONTHLY, + state::State, }; #[derive(Debug)] @@ -31,33 +37,22 @@ impl Command for LeaderboardCommand { ctx: CommandContext<'_, State, Extras>, _args: &str, ) -> Result<(), CommandError> { - let mut redis = ctx - .rt - .state - .redis - .get() - .await - .map_err(CommandError::RedisPoolFailed)?; + let mut redis = ctx.rt.state.redis.get().await?; let top: Vec<(u64, f64)> = redis .zrevrange_withscores(KEY_LEADERBOARD_MONTHLY, 0, 9) - .await - .map_err(CommandError::RedisFailed)?; + .await?; log::trace!("Top users for leaderboard: {top:?}"); let current_user_rank: Option = redis .zrevrank(KEY_LEADERBOARD_MONTHLY, ctx.event.author.id.get()) - .await - .map_err(CommandError::RedisFailed)?; + .await?; let mut users = Vec::new(); for (user_id, score) in top { - let Some(user) = User::get_by_id(user_id, &ctx.rt.state.db) - .await - .map_err(|_| CommandError::DatabaseFailed)? - else { + let Some(user) = User::get_by_id(user_id, &ctx.rt.state.db).await? else { log::trace!("User ID {user_id} not found for leaderboard"); continue; }; @@ -130,8 +125,7 @@ impl Command for LeaderboardCommand { ctx.send() .embed(embed.build()) .await - .inspect_err(|e| log::error!("Failed to send leaderboard embed: {e}")) - .map_err(|_| CommandError::MessageSendingFailed)?; + .inspect_err(|e| log::error!("Failed to send leaderboard embed: {e}"))?; Ok(()) } diff --git a/src/groups/moderation/arguments.rs b/src/groups/moderation/arguments.rs index a78f7bf..e5141b2 100644 --- a/src/groups/moderation/arguments.rs +++ b/src/groups/moderation/arguments.rs @@ -22,10 +22,7 @@ use crate::{ /// [`Result`] - The user from the DB, or an [`ArgumentParsingError`] ready /// to be returned by the parser. async fn get_user_by_id(id: u64, db: &Session) -> Result { - let Some(user) = User::get_by_id(id, db) - .await - .map_err(|_| ArgumentParsingError::DatabaseFailed)? - else { + let Some(user) = User::get_by_id(id, db).await? else { return Err(ArgumentParsingError::InvalidArgument); }; Ok(user) @@ -49,8 +46,7 @@ async fn user_from_reply_or_command_executor( if referenced.author.bot { if let Some(message) = Message::get_by_propagation_message_id(referenced.id.get(), &ctx.rt.state.db) - .await - .map_err(|_| ArgumentParsingError::DatabaseFailed)? + .await? { return get_user_by_id(message.author_id, &ctx.rt.state.db).await; } @@ -112,10 +108,7 @@ impl FromArguments for UserArgument { } else { // The argument is (probably) a username. - let Some(db_user) = User::get_by_name(identifier.0, &ctx.rt.state.db) - .await - .map_err(|_| ArgumentParsingError::DatabaseFailed)? - else { + let Some(db_user) = User::get_by_name(identifier.0, &ctx.rt.state.db).await? else { // Check in the message's reply, and if not use the command executor. user = user_from_reply_or_command_executor(ctx).await?; diff --git a/src/groups/moderation/commands.rs b/src/groups/moderation/commands.rs index 240bbde..1bb65e2 100644 --- a/src/groups/moderation/commands.rs +++ b/src/groups/moderation/commands.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; use crate::{ - database::models::UserIncident, extras::Extras, framework::{ commands::{ @@ -14,10 +13,10 @@ use crate::{ }, groups::moderation::{ arguments::UserArgument, + get_incident_page, permissions::{BotModeratorPermission, BotOwnerPermission}, }, state::State, - utils::Bye, }; #[derive(Debug)] @@ -58,14 +57,11 @@ impl Command for ModeratorCommand { Ok(arg) => arg, Err(e) => match e { ArgumentParsingError::InvalidArgument => { - ctx.send() - .content("❌ User not found in DB.") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + ctx.send().content("❌ User not found in DB.").await?; return Ok(()); } - ArgumentParsingError::DatabaseFailed => { - return Err(CommandError::DatabaseFailed); + ArgumentParsingError::DatabaseFailed(e) => { + return Err(CommandError::DatabaseFailed(e)); } e => { unreachable!("Unexpected error: {e}"); @@ -75,9 +71,7 @@ impl Command for ModeratorCommand { user.is_moderator = !user.is_moderator; - user.insert(&ctx.rt.state.db) - .await - .map_err(|_| CommandError::DatabaseFailed)?; + user.insert(&ctx.rt.state.db).await?; ctx.send() .content(format!( @@ -89,8 +83,7 @@ impl Command for ModeratorCommand { "**is no longer** a moderator" } )) - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; Ok(()) } @@ -126,14 +119,11 @@ impl Command for IncidentsCommand { Ok(Arg(user)) => user, Err(e) => match e { ArgumentParsingError::InvalidArgument => { - ctx.send() - .content("❌ User not found in DB.") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + ctx.send().content("❌ User not found in DB.").await?; return Ok(()); } - ArgumentParsingError::DatabaseFailed => { - return Err(CommandError::DatabaseFailed); + ArgumentParsingError::DatabaseFailed(e) => { + return Err(CommandError::DatabaseFailed(e)); } e => { unreachable!("Unexpected error: {e}"); @@ -141,17 +131,9 @@ impl Command for IncidentsCommand { }, }; - let incidents = UserIncident::get_for_user(user.id, &ctx.rt.state.db) - .await - .map_err(|_| CommandError::DatabaseFailed)?; + let message = get_incident_page(user.id, &ctx.rt.state.db, 0).await?; - ctx.send() - .content(format!( - "Great! The user fetched from the DB is {}", - user.name - )) - .await - .bye(); + ctx.send_from_builder(message).await?; Ok(()) } diff --git a/src/groups/moderation/components.rs b/src/groups/moderation/components.rs new file mode 100644 index 0000000..2a8bddc --- /dev/null +++ b/src/groups/moderation/components.rs @@ -0,0 +1,135 @@ +use async_trait::async_trait; +use twilight_model::channel::message::component::ButtonStyle; + +use crate::{ + database::models::UserIncident, + extras::Extras, + framework::interactions::{InteractionError, components::Button, context::InteractionContext}, + groups::moderation::get_incident_page, + state::State, + utils::get_random_u64, +}; + +#[derive(Debug)] +pub struct ArrowIncidentButton { + pub arrow: char, + pub user_id: u64, + pub go_to: Option, +} + +#[async_trait] +impl Button for ArrowIncidentButton { + fn id(&self) -> Option { + Some(format!("incidents:arrow:{}", get_random_u64())) + } + + fn label(&self) -> Option { + Some(format!("{}", self.arrow)) + } + + fn disabled(&self) -> bool { + self.go_to.is_none() + } + + async fn on_press( + &self, + ctx: InteractionContext, + ) -> Result<(), InteractionError> { + if !ctx.extras.user.is_moderator { + ctx.defer().await?; + return Ok(()); + } + + if let Some(go_to) = self.go_to { + ctx.edit_original_message( + get_incident_page(self.user_id, &ctx.rt.state.db, go_to) + .await + .inspect_err(|e| log::error!("Failed to get incident page: {e:?}"))? + .into(), + ) + .await + .inspect_err(|e| log::error!("Failed to edit original message: {e:?}"))?; + } else { + ctx.defer().await?; + } + + Ok(()) + } +} + +#[derive(Debug)] +pub struct CounterButton { + pub current: usize, + pub total: usize, +} + +#[async_trait] +impl Button for CounterButton { + fn id(&self) -> Option { + Some("incidents:counter".into()) + } + + fn label(&self) -> Option { + Some(format!("{}/{}", self.current, self.total)) + } + + fn disabled(&self) -> bool { + true + } +} + +#[derive(Debug)] +pub struct DeleteIncidentButton { + pub user_id: u64, + pub incident_id: u64, + pub incident_index: usize, +} + +#[async_trait] +impl Button for DeleteIncidentButton { + fn id(&self) -> Option { + Some(format!("incidents:delete:{}", get_random_u64())) + } + + fn label(&self) -> Option { + Some("Delete Incident".into()) + } + + fn style(&self) -> ButtonStyle { + ButtonStyle::Danger + } + + fn disabled(&self) -> bool { + false + } + + async fn on_press( + &self, + ctx: InteractionContext, + ) -> Result<(), InteractionError> { + if !ctx.extras.user.is_moderator { + ctx.defer().await?; + return Ok(()); + } + + UserIncident::delete_by_user_and_incident_id( + self.user_id, + self.incident_id, + &ctx.rt.state.db, + ) + .await?; + + let new_page_index = self.incident_index.saturating_sub(1); + + ctx.edit_original_message( + get_incident_page(self.user_id, &ctx.rt.state.db, new_page_index) + .await + .inspect_err(|e| log::error!("Failed to get incident page: {e:?}"))? + .into(), + ) + .await + .inspect_err(|e| log::error!("Failed to edit original message: {e:?}"))?; + + Ok(()) + } +} diff --git a/src/groups/moderation/embeds.rs b/src/groups/moderation/embeds.rs new file mode 100644 index 0000000..3986e55 --- /dev/null +++ b/src/groups/moderation/embeds.rs @@ -0,0 +1,102 @@ +use time::UtcDateTime; +use twilight_model::channel::message::Embed; +use twilight_util::builder::embed::{ + EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, ImageSource, +}; + +use crate::{ + constants::{COLOR_RED, COLOR_YELLOW}, + database::models::{UserIncident, UserIncidentType}, + utils::ellipse, +}; + +fn format_timestamp(timestamp: UtcDateTime) -> String { + format!( + " ()", + timestamp.unix_timestamp(), + timestamp.unix_timestamp() + ) +} + +pub fn incident_embed(incident: &UserIncident) -> Embed { + if let Some(message) = &incident.message { + let mut builder = EmbedBuilder::new() + .color(match incident.r#type { + UserIncidentType::Ban => COLOR_RED, + UserIncidentType::Warning => COLOR_YELLOW, + }) + .author( + EmbedAuthorBuilder::new(&message.author_name) + .icon_url(ImageSource::url(&message.author_avatar_url).unwrap()), + ) + .title("Message Incident") + .thumbnail(ImageSource::url(message.author_avatar_url.clone()).unwrap()) + .field(EmbedFieldBuilder::new("Message", ellipse(message.content.clone(), 100)).build()) + .field(EmbedFieldBuilder::new("Author Name", message.author_name.clone()).build()) + .field( + EmbedFieldBuilder::new( + "Author Avatar", + format!("[Click to open avatar](<{}>)", &message.author_avatar_url), + ) + .build(), + ) + .field( + EmbedFieldBuilder::new("Created At", format_timestamp(incident.created_at)).build(), + ) + .field(EmbedFieldBuilder::new( + "Created By", + incident + .created_by_id + .map(|id| format!("<@{id}>")) + .unwrap_or("Automod".into()), + )); + + if incident.r#type == UserIncidentType::Ban { + builder = builder.field( + EmbedFieldBuilder::new( + "Expires At", + incident + .expires_at + .map(format_timestamp) + .unwrap_or("*Never*".into()), + ) + .build(), + ); + } + + builder.build() + } else { + let mut builder = EmbedBuilder::new() + .color(match incident.r#type { + UserIncidentType::Ban => COLOR_RED, + UserIncidentType::Warning => COLOR_YELLOW, + }) + .title("Manual Incident") + .field(EmbedFieldBuilder::new("Reason", incident.reason.clone())) + .field( + EmbedFieldBuilder::new("Created At", format_timestamp(incident.created_at)).build(), + ) + .field(EmbedFieldBuilder::new( + "Created By", + incident + .created_by_id + .map(|id| format!("<@{id}>")) + .unwrap_or("Automod".into()), + )); + + if incident.r#type == UserIncidentType::Ban { + builder = builder.field( + EmbedFieldBuilder::new( + "Expires At", + incident + .expires_at + .map(format_timestamp) + .unwrap_or("*Never*".into()), + ) + .build(), + ); + } + + builder.build() + } +} diff --git a/src/groups/moderation/mod.rs b/src/groups/moderation/mod.rs index a9e43f7..c498e60 100644 --- a/src/groups/moderation/mod.rs +++ b/src/groups/moderation/mod.rs @@ -1,3 +1,68 @@ +use scylla::client::session::Session; + +use crate::{ + database::models::{DatabaseError, UserIncident}, + extras::Extras, + framework::messages::MessageBuilder, + groups::moderation::{ + components::{ArrowIncidentButton, CounterButton, DeleteIncidentButton}, + embeds::incident_embed, + }, + state::State, +}; + pub mod arguments; pub mod commands; +pub mod components; +pub(super) mod embeds; pub mod permissions; + +pub(super) async fn get_incident_page( + user_id: u64, + session: &Session, + page_index: usize, +) -> Result, DatabaseError> { + let incidents = UserIncident::get_for_user(user_id, session).await?; + + if let Some(incident) = incidents.get(page_index) { + Ok(MessageBuilder::new() + .embed(incident_embed(incident)) + .component( + ArrowIncidentButton { + arrow: '<', + user_id, + go_to: if page_index > 0 { + Some(page_index - 1) + } else { + None + }, + }, + 1, + ) + .component( + CounterButton { + current: page_index + 1, + total: incidents.len(), + }, + 1, + ) + .component( + ArrowIncidentButton { + arrow: '>', + user_id, + go_to: incidents.get(page_index + 1).map(|_| page_index + 1), + }, + 1, + ) + .component( + DeleteIncidentButton { + incident_id: incident.incident_id, + incident_index: page_index, + user_id, + }, + 1, + )) + } else { + Ok(MessageBuilder::new().content("No incidents found for this user.")) + } +} diff --git a/src/groups/moderation/permissions.rs b/src/groups/moderation/permissions.rs index 090c591..d46a276 100644 --- a/src/groups/moderation/permissions.rs +++ b/src/groups/moderation/permissions.rs @@ -21,14 +21,12 @@ impl Permission for BotOwnerPermission { ) -> Result<(), CommandError> { ctx.send() .content("⛔ Not enough aura to run this command.") - .await - .map_err(|_| CommandError::MessageSendingFailed)?; + .await?; Ok(()) } } - #[derive(Debug)] pub struct BotModeratorPermission; @@ -45,7 +43,7 @@ impl Permission for BotModeratorPermission { ctx.send() .content("⛔ You're not a Payphone moderator.\n-# If you need to check your incidents, contact a Payphone moderator in the official server.") .await - .map_err(|_| CommandError::MessageSendingFailed)?; + ?; Ok(()) } diff --git a/src/groups/servers/commands.rs b/src/groups/servers/commands.rs index 206a065..b98c3be 100644 --- a/src/groups/servers/commands.rs +++ b/src/groups/servers/commands.rs @@ -1,9 +1,18 @@ use async_trait::async_trait; use crate::{ - constants::DEFAULT_PREFIX, database::models::Server, discord::sanitizing::SanitizeDiscordMarkdown, extras::Extras, framework::commands::{ - arguments::{Arg, Argument, GreedyString, ParseArguments}, context::CommandContext, permissions::Permission, Command, CommandError - }, groups::servers::permissions::{GuildOnlyPermission, ServerModeratorPermission}, state::State + constants::DEFAULT_PREFIX, + database::models::Server, + discord::sanitizing::SanitizeDiscordMarkdown, + extras::Extras, + framework::commands::{ + Command, CommandError, + arguments::{Arg, Argument, GreedyString, ParseArguments}, + context::CommandContext, + permissions::Permission, + }, + groups::servers::permissions::{GuildOnlyPermission, ServerModeratorPermission}, + state::State, }; #[derive(Debug)] @@ -55,18 +64,14 @@ impl Command for PrefixCommand { &ctx.rt.http, ) .await - .inspect_err(|e| log::error!("Failed to get or create server: {e}")) - .map_err(|_| CommandError::DatabaseFailed)?; + .inspect_err(|e| log::error!("Failed to get or create server: {e}"))?; if prefix.is_empty() { prefix = DEFAULT_PREFIX; } server.prefix = prefix.to_string(); - server - .upsert(&ctx.rt.state.db) - .await - .map_err(|_| CommandError::DatabaseFailed)?; + server.upsert(&ctx.rt.state.db).await?; ctx.send() .content(format!( @@ -74,10 +79,7 @@ impl Command for PrefixCommand { prefix.sanitize(false) )) .await - .map_err(|e| { - log::error!("Failed to send prefix update message: {e}"); - CommandError::MessageSendingFailed - })?; + .inspect_err(|e| log::error!("Failed to send prefix update message: {e}"))?; Ok(()) } diff --git a/src/groups/servers/permissions.rs b/src/groups/servers/permissions.rs index e9db92a..90baf05 100644 --- a/src/groups/servers/permissions.rs +++ b/src/groups/servers/permissions.rs @@ -49,11 +49,9 @@ impl Permission for ServerModeratorPermission { .rt .http .guild_member(guild_id, ctx.event.author.id) - .await - .map_err(CommandError::DiscordAPIFailed)? + .await? .model() - .await - .map_err(CommandError::DiscordDeserializationFailed)?; + .await?; let Some(everyone_role) = ctx.rt.cache.role(guild_id.cast()) else { log::warn!("Could not get @everyone role for guild ID {guild_id}!");