631 lines
19 KiB
Rust
631 lines
19 KiB
Rust
use async_trait::async_trait;
|
|
use deadpool_redis::redis::AsyncTypedCommands;
|
|
use scc::HashSet;
|
|
use time::UtcDateTime;
|
|
use twilight_model::id::Id;
|
|
|
|
use crate::database::models::{Call, Channel, Participant};
|
|
use crate::discord::sanitizing::SanitizeDiscordMarkdown;
|
|
use crate::extras::Extras;
|
|
use crate::framework::commands::arguments::{Arg, Argument, ParseArguments};
|
|
use crate::framework::commands::context::CommandContext;
|
|
use crate::framework::commands::permissions::Permission;
|
|
use crate::framework::commands::{Command, CommandError};
|
|
use crate::groups::calls::masks::{get_mask_display_name, get_mask_emoji};
|
|
use crate::groups::calls::permissions::{
|
|
IsCallParticipantPermission, NonNewAccountPermission, NotBannedPermission,
|
|
};
|
|
use crate::groups::calls::strings::{
|
|
get_string_call_answered, get_string_call_hangup_other_party,
|
|
get_string_call_hangup_this_party, get_string_call_no_answer, get_string_call_searching,
|
|
get_string_tips,
|
|
};
|
|
use crate::state::State;
|
|
use crate::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<u64>,
|
|
}
|
|
|
|
impl CallCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
channel_lock: HashSet::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Command<State, Extras> for CallCommand {
|
|
fn name(&self) -> &'static str {
|
|
"call"
|
|
}
|
|
|
|
fn aliases(&self) -> &'static [&'static str] {
|
|
&["c"]
|
|
}
|
|
|
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
|
vec![
|
|
Box::new(NotBannedPermission),
|
|
Box::new(NonNewAccountPermission),
|
|
]
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
ctx: CommandContext<'_, State, Extras>,
|
|
_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:?}");
|
|
})?;
|
|
|
|
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?;
|
|
|
|
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
|
|
?;
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
self.channel_lock.insert(ctx.event.channel_id.get()).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());
|
|
})?;
|
|
|
|
self.channel_lock.remove(&ctx.event.channel_id.get());
|
|
|
|
if let Some(call) = call {
|
|
ctx.send()
|
|
.content(format!(
|
|
"**TIP:** {}\n☎️ {}\n☎️ {}",
|
|
get_string_tips(ctx.prefix),
|
|
get_string_call_searching(ctx.prefix),
|
|
get_string_call_answered(ctx.prefix)
|
|
))
|
|
.await
|
|
.bye();
|
|
|
|
ctx.rt
|
|
.http
|
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
|
.content(&format!("☎️ {}", get_string_call_answered(ctx.prefix)))
|
|
.await
|
|
.bye();
|
|
|
|
let mut redis = ctx.rt.state.redis.get().await?;
|
|
|
|
let timestamp = UtcDateTime::now().unix_timestamp();
|
|
|
|
redis
|
|
.zadd(
|
|
"active-calls",
|
|
format!("{}:{}", call.id, channel.id),
|
|
timestamp,
|
|
)
|
|
.await?;
|
|
redis
|
|
.zadd(
|
|
"active-calls",
|
|
format!("{}:{}", call.id, call.other_party_channel_id(channel.id)),
|
|
timestamp,
|
|
)
|
|
.await?;
|
|
} else {
|
|
ctx.send()
|
|
.content(format!(
|
|
"**TIP:** {}\n☎️ {}",
|
|
get_string_tips(ctx.prefix),
|
|
get_string_call_searching(ctx.prefix)
|
|
))
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SkipCommand;
|
|
|
|
#[async_trait]
|
|
impl Command<State, Extras> for SkipCommand {
|
|
fn name(&self) -> &'static str {
|
|
"skip"
|
|
}
|
|
|
|
fn aliases(&self) -> &'static [&'static str] {
|
|
&["s", "next"]
|
|
}
|
|
|
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
|
vec![
|
|
Box::new(NonNewAccountPermission),
|
|
Box::new(NotBannedPermission),
|
|
Box::new(IsCallParticipantPermission),
|
|
]
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
ctx: CommandContext<'_, State, Extras>,
|
|
_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:?}");
|
|
})?;
|
|
|
|
if channel.call_id.is_none() {
|
|
ctx.send()
|
|
.content(format!(
|
|
"❌ You're not on a call! Use `{}call` to start one.",
|
|
ctx.prefix
|
|
))
|
|
.await?;
|
|
|
|
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?;
|
|
|
|
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:?}"))?
|
|
.ok_or(CommandError::DatabaseInconsistentState)?;
|
|
|
|
call.hangup(&ctx.rt.state.db)
|
|
.await
|
|
.inspect_err(|e| log::error!("Failed to hang up call: {e:?}"))?;
|
|
|
|
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(&format!(
|
|
"☎️ {}",
|
|
get_string_call_hangup_other_party(ctx.prefix)
|
|
))
|
|
.await
|
|
.inspect_err(|e| {
|
|
log::error!("Failed to send message to other party: {e:?}");
|
|
})?;
|
|
|
|
let mut redis = ctx.rt.state.redis.get().await?;
|
|
|
|
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();
|
|
|
|
let call = Call::start(&channel, &ctx.rt.state.db)
|
|
.await
|
|
.inspect_err(|e| {
|
|
log::error!("Failed to start (skipped) call: {e:?}");
|
|
})?;
|
|
|
|
if let Some(call) = call {
|
|
ctx.send()
|
|
.content(format!(
|
|
"**TIP:** {}\n☎️ Skipped! Searching for a call...\n☎️ {}",
|
|
get_string_tips(ctx.prefix),
|
|
get_string_call_answered(ctx.prefix)
|
|
))
|
|
.await
|
|
.bye();
|
|
ctx.rt
|
|
.http
|
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
|
.content(&format!("☎️ {}", get_string_call_answered(ctx.prefix)))
|
|
.await
|
|
.bye();
|
|
|
|
let mut redis = ctx.rt.state.redis.get().await?;
|
|
|
|
let timestamp = UtcDateTime::now().unix_timestamp();
|
|
|
|
redis
|
|
.zadd(
|
|
"active-calls",
|
|
format!("{}:{}", call.id, channel.id),
|
|
timestamp,
|
|
)
|
|
.await?;
|
|
redis
|
|
.zadd(
|
|
"active-calls",
|
|
format!("{}:{}", call.id, call.other_party_channel_id(channel.id)),
|
|
timestamp,
|
|
)
|
|
.await?;
|
|
} else {
|
|
ctx.send()
|
|
.content(format!(
|
|
"**TIP:** {}\n☎️ Skipped! Searching for a call...",
|
|
get_string_tips(ctx.prefix)
|
|
))
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct HangupCommand;
|
|
|
|
#[async_trait]
|
|
impl Command<State, Extras> for HangupCommand {
|
|
fn name(&self) -> &'static str {
|
|
"hangup"
|
|
}
|
|
|
|
fn aliases(&self) -> &'static [&'static str] {
|
|
&["h", "hang"]
|
|
}
|
|
|
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
|
vec![
|
|
Box::new(NonNewAccountPermission),
|
|
Box::new(NotBannedPermission),
|
|
Box::new(IsCallParticipantPermission),
|
|
]
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
ctx: CommandContext<'_, State, Extras>,
|
|
_args: &str,
|
|
) -> Result<(), CommandError> {
|
|
let Some(mut 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:?}"))?
|
|
else {
|
|
ctx.send()
|
|
.content(format!(
|
|
"❌ You're not on a call! Use `{}call` to start one.",
|
|
ctx.prefix
|
|
))
|
|
.await?;
|
|
|
|
return Ok(());
|
|
};
|
|
|
|
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(format!("☎️ {}", get_string_call_no_answer(ctx.prefix)))
|
|
.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. If the issue persists, contact a moderator in the official server.").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:?}"))?;
|
|
|
|
ctx.send()
|
|
.content(format!(
|
|
"☎️ {}",
|
|
get_string_call_hangup_this_party(ctx.prefix)
|
|
))
|
|
.await
|
|
.bye();
|
|
|
|
ctx.rt
|
|
.http
|
|
.create_message(Id::new(call.other_party_channel_id(channel.id)))
|
|
.content(&format!(
|
|
"☎️ {}",
|
|
get_string_call_hangup_other_party(ctx.prefix)
|
|
))
|
|
.await?;
|
|
} else {
|
|
ctx.send()
|
|
.content(format!(
|
|
"❌ You're not on a call! Use `{}call` to start one.",
|
|
ctx.prefix
|
|
))
|
|
.await?;
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FriendRequestCommand;
|
|
|
|
#[async_trait]
|
|
impl Command<State, Extras> 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."
|
|
}
|
|
|
|
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
|
|
vec![
|
|
Box::new(NonNewAccountPermission),
|
|
Box::new(NotBannedPermission),
|
|
Box::new(IsCallParticipantPermission),
|
|
]
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
ctx: CommandContext<'_, State, Extras>,
|
|
_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:?}"))?;
|
|
|
|
let Some(channel) = channel else {
|
|
ctx.send()
|
|
.content(format!(
|
|
"❌ You're not on a call! Use `{}call` to start one.",
|
|
ctx.prefix
|
|
))
|
|
.await?;
|
|
|
|
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:?}"))?
|
|
} else {
|
|
ctx.send()
|
|
.content(format!(
|
|
"❌ You're not on a call! Use `{}call` to start one.",
|
|
ctx.prefix
|
|
))
|
|
.await?;
|
|
|
|
return Ok(());
|
|
};
|
|
|
|
let Some(call) = call else {
|
|
ctx.send()
|
|
.content("❌ Oops! We could not find that call.")
|
|
.await?;
|
|
|
|
return Err(CommandError::DatabaseInconsistentState);
|
|
};
|
|
|
|
let other_channel_id = call.other_party_channel_id(channel.id);
|
|
|
|
let Some(participant) = Participant::get_for_user_and_channel_in_call(
|
|
ctx.event.author.id.get(),
|
|
ctx.event.channel_id.get(),
|
|
call.id,
|
|
&ctx.rt.state.db,
|
|
)
|
|
.await?
|
|
else {
|
|
ctx.send()
|
|
.content("❌ You haven't even said hi! Who are you trying to add?")
|
|
.await?;
|
|
|
|
return Ok(());
|
|
};
|
|
|
|
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 `{}`.",
|
|
participant.display_name().sanitize(false),
|
|
ctx.event.author.name.sanitize(false)
|
|
)
|
|
)
|
|
.await
|
|
?;
|
|
|
|
ctx.send()
|
|
.content("📬 Asked the other party to send you a friend request!")
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct MaskCommand;
|
|
|
|
#[async_trait]
|
|
impl Command<State, Extras> for MaskCommand {
|
|
fn name(&self) -> &'static str {
|
|
"mask"
|
|
}
|
|
|
|
fn aliases(&self) -> &'static [&'static str] {
|
|
&["m"]
|
|
}
|
|
|
|
fn description(&self) -> &'static str {
|
|
"Hides your profile from the other party by replacing your profile with a random animal. This command's preferences persist across calls.\n\nNote: Using this command without `on/off` will display your current mask status, without making any changes to it."
|
|
}
|
|
|
|
fn arguments(&self) -> &'static [Argument] {
|
|
&[Argument {
|
|
name: "on/off",
|
|
is_optional: true,
|
|
is_greedy: false,
|
|
}]
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
mut ctx: CommandContext<'_, State, Extras>,
|
|
args: &str,
|
|
) -> Result<(), CommandError> {
|
|
let Arg(toggle) = match Arg::<Option<bool>>::parse(args, &ctx).await {
|
|
Ok(v) => v,
|
|
Err(_) => {
|
|
ctx.send()
|
|
.content(format!(
|
|
"❓ Are you sure that's correct? Use `{}mask on` and `{}mask off`, or just `{}mask` to toggle it.",
|
|
ctx.prefix, ctx.prefix, ctx.prefix
|
|
))
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
let mask_verb: &'static str;
|
|
|
|
// If toggle is Some, update user mask preference
|
|
if let Some(toggle) = toggle {
|
|
ctx.extras.user.is_masked = toggle;
|
|
ctx.extras.user.insert(&ctx.rt.state.db).await?;
|
|
|
|
mask_verb = "was";
|
|
} else {
|
|
mask_verb = "is";
|
|
}
|
|
|
|
let mask_status = if ctx.extras.user.is_masked {
|
|
"on"
|
|
} else {
|
|
"off"
|
|
};
|
|
|
|
let channel = Channel::get_by_id(ctx.event.channel_id.get(), &ctx.rt.state.db).await?;
|
|
let mut mask_message = format!(
|
|
"🎭 Your mask {mask_verb} set to `{}`. You can toggle your mask on or off using `{}mask on` and `{}mask off`.",
|
|
mask_status, ctx.prefix, ctx.prefix
|
|
);
|
|
|
|
if let Some(channel) = channel
|
|
&& let Some(call_id) = channel.call_id
|
|
{
|
|
if let Some(mut participant) = Participant::get_for_user_and_channel_in_call(
|
|
ctx.extras.user.id,
|
|
ctx.event.channel_id.get(),
|
|
call_id,
|
|
&ctx.rt.state.db,
|
|
)
|
|
.await?
|
|
{
|
|
participant.is_masked = ctx.extras.user.is_masked;
|
|
participant.upsert(&ctx.rt.state.db).await?;
|
|
|
|
if ctx.extras.user.is_masked {
|
|
mask_message = format!(
|
|
"🎭 Your mask {mask_verb} set to `on`. You're currently being displayed as **{} {}**. You can toggle your mask on or off using `{}mask on` and `{}mask off`.",
|
|
get_mask_emoji(&participant.user_mask).expect("User's mask did not exist"),
|
|
get_mask_display_name(&participant.user_mask),
|
|
ctx.prefix,
|
|
ctx.prefix
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
ctx.send().content(mask_message).await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|