use std::{error::Error, sync::Arc}; use async_trait::async_trait; use deadpool_redis::redis::AsyncTypedCommands; use itertools::Itertools; use time::{Duration, UtcDateTime}; 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, EmbedFooterBuilder, ImageSource}, snowflake::Snowflake, }; use crate::{ constants::{COLOR_WHITE, COLOR_YELLOW}, database::models::{Call, Channel, Message, Participant, User, UserIncident}, discord::{images, 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, masks::{get_mask_avatar_url, get_mask_display_name}, 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()), 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 for CallMessageHandler { async fn on_message( &self, event: &MessageCreate, rt: Arc>, _extras: Arc>, ) -> EventResult { if event.author.bot || event.author.system.unwrap_or(false) { return Ok(()); } if UtcDateTime::from_unix_timestamp(event.author.id.timestamp() / 1000).unwrap() > UtcDateTime::now() - Duration::days(30) { return Ok(()); } if event.content.is_empty() { return Ok(()); } let Some(guild_id) = event.guild_id else { 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 mut user = User::create_or_update( event.author.clone(), &rt.state.db, &rt.state.color_queue_sender, ) .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(()); } if event.content.to_ascii_lowercase().starts_with("x ") { if !user.findings.contains("feature:hide-message") { user.findings.insert("feature:hide-message".into()); user.insert(&rt.state.db).await?; rt .http .create_message( event.channel_id ) .content( "✨ You found a hidden feature! Messages starting with `x` followed by a space will not be sent to the other party (nor moderated, i.e. you won't get banned for what you say in them)." ) .reply(event.id) .await?; } 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 (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); let participant = Participant::get_or_create( user.id, event.channel_id.get(), call.id, event .member .clone() .and_then(|member| member.nick) .or(event.author.global_name.clone()) .unwrap_or(event.author.name.clone()), images::get_avatar_url( event .member .clone() .and_then(|inner| inner.avatar) .or(event.author.avatar), event.author.id.get(), event.author.discriminator as u64, event .member .as_ref() .and_then(|member| member.avatar) .map(|_| guild_id.get()), ), user.is_masked, &rt.state.db, ) .await?; 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`.", participant.display_name().sanitize(false) )) .await?; message = Message::build() .call_id(call.id) .message(event.0.clone()) .participant(participant) .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 { if let Some((propagation_channel_id, propagation_message_id)) = replying_to .data .propagation_channel_id .zip(replying_to.data.propagation_message_id) { let (display_name, avatar_url) = if let Some(mask) = &replying_to.data.author_mask { ( if i64_to_u64(propagation_channel_id) != event.channel_id.get() { get_mask_display_name(mask) } else { format!( "{} ({})", get_mask_display_name(mask), replying_to.data.author_name ) }, get_mask_avatar_url(mask).expect("Failed to get mask avatar URL"), ) } else { ( replying_to.data.author_name.clone(), replying_to.data.author_avatar_url.clone(), ) }; let message_link = if i64_to_u64(propagation_channel_id) == channel.id { format!( "https://discord.com/channels/{}/{}/{}", opposite_channel.server_id, opposite_channel.id, replying_to.message_id ) } else { format!( "https://discord.com/channels/{}/{}/{}", channel.server_id, propagation_channel_id, propagation_message_id ) }; let replying_to_user = User::get_by_id(replying_to.author_id, &rt.state.db).await?; embeds.push( EmbedBuilder::new() .author( EmbedAuthorBuilder::new(format!("Replying to {display_name}",)) .icon_url(ImageSource::url(avatar_url).unwrap()) .url(message_link), ) .color(if replying_to.data.author_mask.is_some() { COLOR_WHITE } else { replying_to_user .and_then(|i| i.color.map(|c| c as u32)) .unwrap_or(COLOR_WHITE) }) .description(ellipse(replying_to.data.content, 100)) .build(), ); } } if participant.is_masked { let last_message = Message::get_last_by_author_id(call.id, event.author.id.get(), &rt.state.db) .await?; if last_message .map(|lm| lm.data.author_mask.is_none()) .unwrap_or(true) { embeds.push( EmbedBuilder::new() .color(COLOR_YELLOW) .description(format!( "🎭 **{}** is wearing a mask! Their real identity is hidden.", participant.display_name() )) .footer(EmbedFooterBuilder::new(format!( "You can set your own mask using {}mask on.", prefixes[0] ))) .build(), ); } } let mut redis = rt.state.redis.get().await?; redis .zadd( "active-calls", format!("{}:{}", call.id, channel.id), UtcDateTime::now().unix_timestamp(), ) .await?; let message_sent = match rt .http .execute_webhook(Id::new(webhook_id), &webhook_token) .allowed_mentions(None) .username(&participant.display_name()) .avatar_url(&participant.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()) .participant(participant) .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 for CallMessageEditHandler { async fn on_message_edit( &self, event: &MessageUpdate, rt: Arc>, ) -> EventResult { if event.edited_timestamp.is_none() { return Ok(()); } 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::create_or_update( event.author.clone(), &rt.state.db, &rt.state.color_queue_sender, ) .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 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(()) } }