use async_trait::async_trait; use scc::HashSet; use twilight_model::id::Id; use crate::{ database::models::{Call, Channel}, discord::sanitizing::SanitizeDiscordMarkdown, framework::commands::{Command, CommandError, context::CommandContext}, groups::calls::get_display_profile, state::State, utils::Bye, }; #[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 SkipCommand; #[async_trait] impl Command for SkipCommand { fn name(&self) -> &'static str { "skip" } fn aliases(&self) -> &'static [&'static str] { &["s", "next"] } 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.call_id.is_none() { 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 { ctx.send() .content(format!( "☎️ You're already searching for a call! Use `{}hangup` to cancel the search.", ctx.prefix )) .await .map_err(|_| CommandError::MessageSendingFailed)?; 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::DatabaseQueryFailed)? .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::DatabaseQueryFailed)?; let other_channel_id = call.other_party_channel_id(ctx.event.channel_id.get()); ctx.rt .http .create_message(Id::new(other_channel_id)) .content("☎️ The other party hung up the phone.") .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::DatabaseQueryFailed)?; 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("☎️ Skipped! 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(()) } } #[derive(Debug)] pub struct FriendRequestCommand; #[async_trait] impl Command for FriendRequestCommand { fn name(&self) -> &'static str { "friendreq" } fn aliases(&self) -> &'static [&'static str] { &["f", "fr", "friendrequest"] } fn description(&self) -> &'static str { "Shows your username to the other party and asks them to send you a friend request on Discord." } async fn execute( &self, ctx: CommandContext<'_, State>, _args: &str, ) -> 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::DatabaseQueryFailed)?; let Some(channel) = channel 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(()); }; 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::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(()); }; let Some(call) = call else { ctx.send() .content("❌ Oops! We could not find that call.") .await .map_err(|_| CommandError::MessageSendingFailed)?; return Err(CommandError::DatabaseInconsistentState); }; let other_channel_id = call.other_party_channel_id(channel.id); let profile = get_display_profile(ctx.event); ctx .rt .http .create_message( Id::new(other_channel_id) ) .content( &format!( "📬 **{}** wants you to send them a friend request on Discord! Their username is `{}`.", profile.name.sanitize(false), ctx.event.author.name.sanitize(false) ) ) .await .map_err(|_| CommandError::MessageSendingFailed)?; ctx.send() .content("📬 Asked the other party to send you a friend request!") .await .map_err(|_| CommandError::MessageSendingFailed)?; Ok(()) } }