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

474 lines
15 KiB
Rust

use std::{error::Error, sync::Arc};
use async_trait::async_trait;
use itertools::Itertools;
use twilight_http::{api_error::ApiError, error::ErrorType};
use twilight_model::{
gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate},
id::Id,
};
use twilight_util::builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource};
use crate::{
constants::COLOR_WHITE,
database::models::{Call, Channel, Message, User, UserIncident},
discord::sanitizing::SanitizeDiscordMarkdown,
extras::Extras,
framework::{
commands::extras::ExtrasGetter, events::traits::{
EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
}, interactions::components::ActionRow, runtime::Runtime
},
groups::calls::{
components::{CodeOfConductButton, NeedHelpButton},
embeds, get_display_profile,
moderation::{escape_message, Action, Rule},
},
state::State,
utils::{ellipse, i64_to_u64},
};
async fn penalize_user(
user: User,
message: Message,
broken_rules: Vec<Arc<dyn Rule>>,
rt: &Runtime<State, Extras>,
) -> Result<(), Box<dyn Error>> {
let reason = format!(
"You broke the following rules during a call:\n{}",
broken_rules
.iter()
.map(|rule| format!("- {}", rule.name()))
.join("\n")
);
let new_incident = user
.incident(
reason.clone(),
Some(message.data.clone()),
None,
&rt.state.db,
)
.await?;
let dm_channel = rt
.http
.create_private_channel(Id::new(user.id))
.await?
.model()
.await?;
let embed = if new_incident.is_first_incident {
embeds::first_warning_embed(&rt.app, &message, broken_rules)
} else if new_incident.counting_incidents == 0 {
embeds::repeat_warning_embed(&rt.app, &message, broken_rules)
} else if new_incident.counting_incidents == 1 {
embeds::first_ban_embed(&rt.app, &message, broken_rules)
} else {
embeds::final_ban_embed(&rt.app, &message, broken_rules)
};
rt.http
.create_message(dm_channel.id)
.embeds(&[embed])
.components(&[ActionRow::new(&[
(None, Arc::new(CodeOfConductButton)),
(None, Arc::new(NeedHelpButton)),
])
.build()])
.await?;
Ok(())
}
#[derive(Debug)]
pub struct CallMessageHandler;
#[async_trait]
impl OnMessageHandler<State, Extras> for CallMessageHandler {
async fn on_message(
&self,
event: &MessageCreate,
rt: Arc<Runtime<State, Extras>>,
_extras: Arc<dyn ExtrasGetter<State, Extras = Extras>>,
) -> EventResult {
if event.author.bot || event.author.system.unwrap_or(false) {
return Ok(());
}
let prefixes = rt
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
.await;
if prefixes.iter().any(|prefix| {
event
.content
.to_ascii_lowercase()
.starts_with(&prefix.to_ascii_lowercase())
}) {
return Ok(());
}
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db)
.await
.inspect_err(|e| {
log::error!(
"Failed to get channel from DB when processing message create event! {e:?}"
);
})?
else {
return Ok(());
};
let call = if let Some(call_id) = channel.call_id {
if let Some(call) = Call::get_by_id(call_id, &rt.state.db)
.await
.inspect_err(|e| {
log::error!(
"Failed to get call from DB when processing message create event! {e:?}"
);
})?
{
if !call.is_active() {
return Ok(());
}
call
} else {
log::error!(
"Call ID {} in channel {} does not correspond to an active call.",
call_id,
channel.id
);
return Ok(());
}
} else {
return Ok(());
};
let user =
User::get_or_create_and_set_last_seen(event.author.clone(), &rt.state.db).await?;
let user_incidents = UserIncident::get_for_user(event.author.id.get(), &rt.state.db)
.await?
.iter()
.filter(|incident| incident.is_active())
.count();
if user_incidents > 0 {
return Ok(());
}
let Some(mut opposite_channel) = Channel::get_by_id(
call.other_party_channel_id(event.channel_id.get()),
&rt.state.db,
)
.await
.inspect_err(|e| {
log::error!(
"Failed to get opposite channel from DB when processing message create event! {e:?}"
);
})?
else {
return Ok(());
};
let Some((webhook_id, webhook_token)) = opposite_channel
.webhook_id
.zip(opposite_channel.webhook_token.clone())
else {
// Handle DM calls in the future.
return Ok(());
};
let profile = get_display_profile(event);
let (content, rules) = escape_message(event);
let message: Message;
let replying_to = if let Some(reference) = &event.reference {
if let Some(replying_to_message_id) = reference.message_id {
Message::get_by_call_and_message_id(
call.id,
replying_to_message_id.get(),
&rt.state.db,
)
.await?
} else {
None
}
} else {
None
};
let replying_to_id = replying_to.as_ref().map(|i| i.message_id);
if rules
.iter()
.any(|rule| rule.action() == &Action::BlockAndPenalty)
{
rt.http
.create_message(event.channel_id)
.content(
"🚩 Your message has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
)
.await?;
rt.http
.create_message(Id::new(opposite_channel.id))
.content(&format!(
"🚩 **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.",
profile.name.sanitize(false)
))
.await?;
message = Message::build()
.call_id(call.id)
.message(event.0.clone())
.profile(profile.clone())
.is_censored(!rules.is_empty())
.replying_to_message_id(replying_to_id)
.build()
.expect(
"The message builder was missing either the call ID, the message, or the profile",
);
} else {
let mut embeds = vec![];
if let Some(replying_to) = replying_to {
embeds.push(
EmbedBuilder::new()
.author(
EmbedAuthorBuilder::new(format!("Replying to {}", replying_to.data.author_name)).icon_url(
ImageSource::url(replying_to.data.author_avatar_url).unwrap(),
),
)
.color(COLOR_WHITE)
.description(ellipse(replying_to.data.content, 100))
.build(),
);
}
let message_sent = match rt
.http
.execute_webhook(Id::new(webhook_id), &webhook_token)
.allowed_mentions(None)
.username(&profile.name)
.avatar_url(&profile.avatar_url)
.content(&content.sanitize_mentions())
.embeds(&embeds)
.wait()
.await
.inspect_err(|e| {
log::error!(
"Failed to send message to webhook for call {} in channel {}: {e:?}",
call.id,
opposite_channel.id
);
}) {
Ok(message) => message,
Err(e) => {
if let ErrorType::Response {
error: ApiError::General(error),
..
} = e.kind()
{
if error.code == 10015 {
log::debug!("Webhook not found, updating...");
opposite_channel
.update_webhook(&rt.state.db, &rt.http)
.await?;
}
}
return Ok(());
}
}
.model()
.await?;
message = Message::build()
.call_id(call.id)
.message(event.0.clone())
.profile(profile.clone())
.is_censored(!rules.is_empty())
.replying_to_message_id(replying_to_id)
.propagation_message_id(message_sent.id.get())
.propagation_channel_id(opposite_channel.id)
.build()
.expect(
"The message builder was missing either the call ID, the message, or the profile",
);
}
message.insert(&rt.state.db).await?;
if rules
.iter()
.any(|rule| rule.action() == &Action::BlockAndPenalty)
{
penalize_user(user, message, rules, &rt).await?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct CallMessageEditHandler;
#[async_trait]
impl OnMessageEditHandler<State, Extras> for CallMessageEditHandler {
async fn on_message_edit(
&self,
event: &MessageUpdate,
rt: Arc<Runtime<State, Extras>>,
) -> EventResult {
log::trace!(
"Edited message was '{}' sent by {} (bot? {:?})",
event.content,
event.author.id.get(),
event.author.bot
);
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else {
return Ok(());
};
let call = if let Some(call_id) = channel.call_id {
if let Some(call) = Call::get_by_id(call_id, &rt.state.db).await? {
if !call.is_active() {
return Ok(());
}
call
} else {
return Ok(());
}
} else {
return Ok(());
};
let Some(mut message) =
Message::get_by_call_and_message_id(call.id, event.id.get(), &rt.state.db).await?
else {
return Ok(());
};
let opposite_channel_id = call.other_party_channel_id(event.channel_id.get());
let Some(opposite_channel) = Channel::get_by_id(opposite_channel_id, &rt.state.db).await?
else {
return Ok(());
};
if let Some(propagation_message_id) = message.data.propagation_message_id {
let (content, rules) = escape_message(event);
if rules
.iter()
.any(|rule| rule.action() == &Action::BlockAndPenalty)
{
rt.http
.create_message(event.channel_id)
.content(
"🚩 Your message's edit has been blocked because it broke Payphone's code of conduct. Check your DMs for more information.",
)
.reply(event.id)
.await?;
let user =
User::get_or_create_and_set_last_seen(event.author.clone(), &rt.state.db)
.await?;
penalize_user(user, message, rules, &rt).await?;
return Ok(());
}
if let Some((webhook_id, webhook_token)) = opposite_channel
.webhook_id
.zip(opposite_channel.webhook_token)
{
rt.http
.update_webhook_message(
Id::new(webhook_id),
&webhook_token,
Id::new(i64_to_u64(propagation_message_id)),
)
.content(Some(&content))
.await?;
message.data.content = content;
message.data.is_edited = true;
message.data.is_censored = !rules.is_empty();
message.insert(&rt.state.db).await?;
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct CallMessageDeleteHandler;
#[async_trait]
impl OnMessageDeleteHandler<State, Extras> for CallMessageDeleteHandler {
async fn on_message_delete(
&self,
event: &MessageDelete,
rt: Arc<Runtime<State, Extras>>,
) -> EventResult {
let Some(channel) = Channel::get_by_id(event.channel_id.get(), &rt.state.db).await? else {
return Ok(());
};
let call = if let Some(call_id) = channel.call_id {
if let Some(call) = Call::get_by_id(call_id, &rt.state.db).await? {
if !call.is_active() {
return Ok(());
}
call
} else {
return Ok(());
}
} else {
return Ok(());
};
let Some(mut message) =
Message::get_by_call_and_message_id(call.id, event.id.get(), &rt.state.db).await?
else {
return Ok(());
};
let opposite_channel_id = call.other_party_channel_id(event.channel_id.get());
let Some(opposite_channel) = Channel::get_by_id(opposite_channel_id, &rt.state.db).await?
else {
return Ok(());
};
if let Some(propagation_message_id) = message.data.propagation_message_id {
if let Some((webhook_id, webhook_token)) = opposite_channel
.webhook_id
.zip(opposite_channel.webhook_token)
{
rt.http
.delete_webhook_message(
Id::new(webhook_id),
&webhook_token,
Id::new(i64_to_u64(propagation_message_id)),
)
.await?;
message.data.is_deleted = true;
message.insert(&rt.state.db).await?;
}
}
Ok(())
}
}