Files
payphone/src/groups/calls/commands.rs
T

289 lines
9.0 KiB
Rust

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<State> 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<State>,
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<u64>,
}
impl CallCommand {
pub fn new() -> Self {
Self {
channel_lock: HashSet::new(),
}
}
}
#[async_trait]
impl Command<State> 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<State> 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(())
}
}