289 lines
9.0 KiB
Rust
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(())
|
|
}
|
|
}
|