474 lines
15 KiB
Rust
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(())
|
|
}
|
|
}
|