use async_trait::async_trait; use scc::HashSet; use std::sync::Arc; use twilight_model::id::Id; use crate::{ database::models::{Call, Channel, Message, MessageRow, Report}, framework::commands::{Command, CommandError, context::CommandContext}, framework::interactions::{ InteractionError, context::InteractionContext, message_command::MessageCommand, }, state::State, utils::{Bye, get_random_u64}, }; #[derive(Debug, Default)] pub struct ReportMessageCommand; #[async_trait] impl MessageCommand for ReportMessageCommand { fn name(&self) -> &'static str { "Report Message" } fn description(&self) -> &'static str { "Report a message to the moderators." } async fn execute( &self, ctx: InteractionContext, target_message_id: u64, ) -> Result<(), InteractionError> { // Extract channel_id from resolved data or fallback to event.channel (Discord API v10+) let channel_id = ctx .event .channel .as_ref() .map(|c| c.id.get()) .or_else(|| ctx.event.channel.as_ref().map(|c| c.id.get())) .ok_or(InteractionError::InteractionNotFound)?; let db = &ctx.rt.state.db; let channel = Channel::get_by_id(channel_id, db) .await .map_err(|_| InteractionError::InteractionNotFound)?; let call_id = channel .and_then(|c| c.call_id) .ok_or(InteractionError::InteractionNotFound)?; // Fetch the message from the database let message_row = MessageRow::get_by_call_and_message_id(call_id, target_message_id, db) .await .map_err(|_| InteractionError::InteractionNotFound)? .ok_or(InteractionError::InteractionNotFound)?; // If the message is a reply, fetch the replied-to message and store in replying_to let replying_to = if let Some(replying_to_id) = message_row.data.replying_to_id { if let Ok(Some(reply_msg_row)) = MessageRow::get_by_call_and_message_id(call_id, replying_to_id as u64, db).await { Some(reply_msg_row.data) } else { None } } else { None }; // Build the report using the new flat message and replying_to let report = Report { report_id: get_random_u64(), message: message_row.data, replying_to, created_at: time::OffsetDateTime::now_utc(), reviewed_at: None, created_by_ids: vec![ctx.event.author_id().map(|id| id.get()).unwrap_or(0)], reviewed_by_id: None, resolution: None, }; // Insert the report report .insert(db) .await .map_err(InteractionError::DatabaseError)?; // Respond to the user Arc::new(ctx) .send() .content("Message has been reported to the moderators.") .await?; Ok(()) } } #[derive(Default, Debug)] pub struct CallCommand { /// A set of channel IDs that are currently searching for a call. This works as a lock for two /// users not to use p.call at the same time in the same channel, as it could create a race /// condition. IDs are removed from this set once the DB is properly updated with the channel's /// state, meaning that not all searching channels will be in this set at all times. channel_lock: HashSet, } impl CallCommand { pub fn new() -> Self { Self { channel_lock: HashSet::new(), } } } #[async_trait] impl Command for CallCommand { fn name(&self) -> &'static str { "call" } fn aliases(&self) -> &'static [&'static str] { &["c"] } async fn execute( &self, ctx: CommandContext<'_, State>, _args: &str, ) -> Result<(), CommandError> { let channel = Channel::get_or_create_by_id( ctx.event.channel_id.get(), &ctx.rt.state.db, &ctx.rt.http, ) .await .inspect_err(|e| { log::error!("Failed to get or create channel: {e:?}"); }) .map_err(|_| CommandError::DatabaseQueryFailed)?; if channel.is_searching || self.channel_lock.contains(&ctx.event.channel_id.get()) { ctx.send() .content(format!( "☎️ You are already searching for a call! Use `{}hangup` to hang up the phone.", ctx.prefix )) .await .map_err(|_| CommandError::MessageSendingFailed)?; return Ok(()); } if channel.call_id.is_some() { 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(); 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::DatabaseQueryFailed)?; self.channel_lock .remove_async(&ctx.event.channel_id.get()) .await; if let Some(call) = call { ctx.send().content("☎️ Call connected. Say hi!").await.bye(); ctx.rt .http .create_message(Id::new(call.other_party_channel_id(channel.id))) .content("☎️ Call connected. Say hi!") .await .bye(); } else { ctx.send() .content("☎️ Searching for a call...") .await .map_err(|_| CommandError::MessageSendingFailed)?; } Ok(()) } } #[derive(Debug)] pub struct HangupCommand; #[async_trait] impl Command for HangupCommand { fn name(&self) -> &'static str { "hangup" } fn aliases(&self) -> &'static [&'static str] { &["h", "hang"] } async fn execute( &self, ctx: CommandContext<'_, State>, _args: &str, ) -> 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::DatabaseQueryFailed)? else { ctx.send() .content(format!( "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) .await .map_err(|_| CommandError::MessageSendingFailed)?; return Ok(()); }; if channel.is_searching { channel .cancel_search(&ctx.rt.state.db) .await .inspect_err(|e| { log::error!("Failed to cancel search: {e:?}"); }) .map_err(|_| CommandError::DatabaseQueryFailed)?; ctx.send() .content("☎️ The phone was hung up.") .await .map_err(|_| CommandError::MessageSendingFailed)?; return Ok(()); } 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::DatabaseQueryFailed)? else { ctx.send() .content("❌ Call not found.") .await .map_err(|_| CommandError::MessageSendingFailed)?; 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::DatabaseQueryFailed)?; ctx.send().content("☎️ You hung up the phone.").await.bye(); ctx.rt .http .create_message(Id::new(call.other_party_channel_id(channel.id))) .content("☎️ The other party hung up the phone.") .await .map_err(|_| CommandError::MessageSendingFailed)?; } else { ctx.send() .content(format!( "❌ You're not on a call! Use `{}call` to start one.", ctx.prefix )) .await .map_err(|_| CommandError::MessageSendingFailed)?; return Ok(()); } Ok(()) } }