diff --git a/Cargo.lock b/Cargo.lock index 8467f71..303f12e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2013,7 +2013,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "payphone" -version = "4.4.47" +version = "4.4.48" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index eef2db3..7678cd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "payphone" -version = "4.4.47" +version = "4.4.48" edition = "2024" default-run = "payphone" diff --git a/src/groups/moderation/arguments.rs b/src/groups/moderation/arguments.rs index e2e23f1..86d63e0 100644 --- a/src/groups/moderation/arguments.rs +++ b/src/groups/moderation/arguments.rs @@ -3,7 +3,7 @@ use std::time::Duration; use scylla::client::session::Session; -use crate::database::models::{Message, User}; +use crate::database::models::{Channel, Message, User}; use crate::extras::Extras; use crate::framework::commands::arguments::{ArgumentParsingError, FromArguments, NeedyString}; use crate::framework::commands::context::CommandContext; @@ -206,3 +206,63 @@ impl DerefMut for DurationArgument { &mut self.0 } } + +#[derive(Debug, Clone)] +pub struct ChannelArgument(pub Channel); + +impl FromArguments for ChannelArgument { + async fn from_args<'a>( + args: &'a str, + ctx: &'a CommandContext<'_, State, Extras>, + ) -> Result<(Self, &'a str), ArgumentParsingError> { + let (identifier, args) = NeedyString::from_args(args, ctx).await?; + let identifier = identifier.0.trim(); + + if identifier == "." || identifier == "this" || identifier == "here" { + let channel = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db).await?; + + if let Some(channel) = channel { + return Ok((Self(channel), args)); + } + } else if identifier.starts_with("<#") && identifier.ends_with(">") { + // The argument is a Discord mention. + + let channel_id: u64 = identifier[2..identifier.len() - 1] + .to_string() + .parse() + .map_err(|_| ArgumentParsingError::InvalidArgument)?; + + let channel = Channel::get_by_id(channel_id, &ctx.rt.state.db).await?; + + if let Some(channel) = channel { + return Ok((Self(channel), args)); + } + } else { + let channel_id: u64 = identifier + .parse() + .map_err(|_| ArgumentParsingError::InvalidArgument)?; + + let channel = Channel::get_by_id(channel_id, &ctx.rt.state.db).await?; + + if let Some(channel) = channel { + return Ok((Self(channel), args)); + } + } + + Err(ArgumentParsingError::MissingArgument) + } +} + +impl Deref for ChannelArgument { + type Target = Channel; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ChannelArgument { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/groups/moderation/commands.rs b/src/groups/moderation/commands.rs index 12ffc09..a98be54 100644 --- a/src/groups/moderation/commands.rs +++ b/src/groups/moderation/commands.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; +use deadpool_redis::redis::AsyncTypedCommands; use time::UtcDateTime; -use crate::database::models::{UserIncident, UserIncidentType}; +use crate::database::models::{Call, UserIncident, UserIncidentType}; use crate::extras::Extras; use crate::framework::commands::arguments::{ Arg, Argument, ArgumentParsingError, NeedyGreedyString, ParseArguments, @@ -10,11 +11,11 @@ use crate::framework::commands::context::CommandContext; use crate::framework::commands::permissions::Permission; use crate::framework::commands::{Command, CommandError}; use crate::framework::runtime::Runtime; -use crate::groups::moderation::arguments::{DurationArgument, UserArgument}; +use crate::groups::moderation::arguments::{ChannelArgument, DurationArgument, UserArgument}; use crate::groups::moderation::get_incident_page; use crate::groups::moderation::permissions::{BotModeratorPermission, BotOwnerPermission}; use crate::state::State; -use crate::utils::get_random_u64; +use crate::utils::{Bye, get_random_u64}; #[derive(Debug)] pub struct ModeratorCommand; @@ -246,3 +247,114 @@ impl Command for BanCommand { Ok(()) } } + +#[derive(Debug)] +pub struct RemoteHangupCommand; + +#[async_trait] +impl Command for RemoteHangupCommand { + fn name(&self) -> &'static str { + "remotehangup" + } + + fn aliases(&self) -> &'static [&'static str] { + &["rh"] + } + + fn permissions(&self) -> Vec + Send + Sync + 'static>> { + vec![Box::new(BotModeratorPermission)] + } + + fn show_in_help(&self, _rt: &Runtime, extras: &Extras) -> bool { + extras.user.is_moderator + } + + fn arguments(&self) -> &'static [Argument] { + &[Argument { + name: "channel", + is_optional: false, + is_greedy: false, + }] + } + + async fn execute( + &self, + ctx: CommandContext<'_, State, Extras>, + args: &str, + ) -> Result<(), CommandError> { + let Arg(mut channel) = match Arg::::parse(args, &ctx).await { + Ok(arg) => arg, + Err(e) => match e { + ArgumentParsingError::InvalidArgument => { + ctx.send().content("❌ Channel not found in DB.").await?; + return Ok(()); + } + ArgumentParsingError::DatabaseFailed(e) => { + return Err(CommandError::DatabaseFailed(e)); + } + e => { + unreachable!("Unexpected error: {e}"); + } + }, + }; + + if channel.is_searching { + channel + .cancel_search(&ctx.rt.state.db) + .await + .inspect_err(|e| { + log::error!("Failed to cancel search: {e:?}"); + })?; + + ctx.send() + .content("The channel was remotely made to stop searching for a call!") + .await?; + + return Ok(()); + } + + if let Some(call_id) = channel.call_id { + let mut redis = ctx.rt.state.redis.get().await?; + + let Some(call) = Call::get_by_id(call_id, &ctx.rt.state.db) + .await + .inspect_err(|e| log::error!("Failed to get call: {e:?}"))? + else { + ctx.send().content("❌ The call could not be found.\n-# This is not supposed to happen. The channel is pointing to an ID in the database that does not exist.").await?; + + channel.call_id = None; + channel.upsert(&ctx.rt.state.db).await.bye(); + + redis + .zrem("active-calls", format!("{}:{}", call_id, channel.id)) + .await + .bye(); + + return Err(CommandError::DatabaseInconsistentState); + }; + + let channel_keys = [ + format!("{call_id}:{}", call.channel_ids[0]), + format!("{call_id}:{}", call.channel_ids[1]), + ]; + + redis + .zrem("active-calls", &channel_keys) + .await + .inspect_err(|e| log::error!("Failed to remove inactive call from redis: {e}")) + .ok(); + + call.hangup(&ctx.rt.state.db) + .await + .inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?; + } else { + ctx.send() + .content("❌ This channel is not in a call.") + .await?; + + return Ok(()); + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 1c1f001..3384ede 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,7 +137,7 @@ use crate::groups::calls::events::{ use crate::groups::gateway::events::{ReadyHandler, ReconnectHandler}; use crate::groups::leveling::commands::LeaderboardCommand; use crate::groups::leveling::events::MessageLevelingHandler; -use crate::groups::moderation::commands::{BanCommand, IncidentsCommand, ModeratorCommand}; +use crate::groups::moderation::commands::{BanCommand, IncidentsCommand, ModeratorCommand, RemoteHangupCommand}; use crate::groups::servers::commands::PrefixCommand; use crate::groups::servers::events::{ ServerCreateHandler, ServerDeleteHandler, ServerUpdateHandler, @@ -298,6 +298,7 @@ async fn start( .command(ModeratorCommand) .command(IncidentsCommand) .command(BanCommand) + .command(RemoteHangupCommand) .component(HelpGoBackButton) .cache_resource_type(ResourceType::GUILD) .cache_resource_type(ResourceType::CHANNEL)