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, framework::{ events::traits::{ EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler, }, interactions::components::ActionRow, runtime::Runtime, }, groups::calls::{ components::{CodeOfConductButton, NeedHelpButton}, embeds, get_display_profile, moderation::{Action, Rule, escape_message}, }, state::State, utils::{ellipse, i64_to_u64}, }; async fn penalize_user( user: User, message: Message, broken_rules: Vec>, rt: &Runtime, ) -> Result<(), Box> { 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()), Some(user.id), &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 for CallMessageHandler { async fn on_message(&self, event: &MessageCreate, rt: Arc>) -> 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_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(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) { let user = User::get_by_id(event.author.id.get(), &rt.state.db).await?; let user = if let Some(user) = user { user } else { let user: User = event.author.clone().into(); user.insert(&rt.state.db).await?; user }; penalize_user(user, message, rules, &rt).await?; } Ok(()) } } #[derive(Debug)] pub struct CallMessageEditHandler; #[async_trait] impl OnMessageEditHandler for CallMessageEditHandler { async fn on_message_edit(&self, event: &MessageUpdate, rt: Arc>) -> 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_by_id(event.author.id.get(), &rt.state.db).await?; if let Some(user) = user { 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 for CallMessageDeleteHandler { async fn on_message_delete( &self, event: &MessageDelete, rt: Arc>, ) -> 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(()) } }