From 325b4cbc16df8e59f2c6a2274b1e84d4ca99ef50 Mon Sep 17 00:00:00 2001 From: Rafael Bradley Date: Sun, 24 Aug 2025 02:02:25 -0300 Subject: [PATCH] Add support for masks --- .../0005_call_messages_by_author_id.cql | 4 + src/database/models.rs | 387 +++++++++++++----- src/framework/commands/arguments.rs | 2 +- src/groups/calls/commands.rs | 70 +++- src/groups/calls/events.rs | 121 ++++-- src/groups/calls/masks/mod.rs | 25 +- src/groups/calls/mod.rs | 64 --- src/groups/calls/permissions.rs | 2 +- 8 files changed, 454 insertions(+), 221 deletions(-) create mode 100644 src/database/migrations/0005_call_messages_by_author_id.cql diff --git a/src/database/migrations/0005_call_messages_by_author_id.cql b/src/database/migrations/0005_call_messages_by_author_id.cql new file mode 100644 index 0000000..81b602e --- /dev/null +++ b/src/database/migrations/0005_call_messages_by_author_id.cql @@ -0,0 +1,4 @@ +CREATE MATERIALIZED VIEW IF NOT EXISTS payphone.messages_by_author_id AS + SELECT call_id, message_id, author_id FROM payphone.messages + WHERE call_id IS NOT NULL AND message_id IS NOT NULL AND author_id IS NOT NULL + PRIMARY KEY (call_id, author_id, message_id); diff --git a/src/database/models.rs b/src/database/models.rs index 333b535..30d70e0 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -2,7 +2,10 @@ use scylla::{ DeserializeValue, SerializeValue, client::session::Session, deserialize::row::DeserializeRow, - errors::{ExecutionError, SingleRowError}, + errors::{ + DeserializationError, ExecutionError, FirstRowError, IntoRowsResultError, RowsError, + SingleRowError, + }, response::query_result::QueryRowsResult, statement::{ Statement, @@ -21,7 +24,7 @@ use twilight_model::{ use crate::{ constants::DEFAULT_PREFIX, - groups::calls::{DisplayProfile, masks::get_random_mask}, + groups::calls::masks::{get_mask_avatar_url, get_mask_display_name, get_random_mask}, utils::{get_random_u64, i64_to_u64, u64_to_i64}, }; @@ -40,13 +43,22 @@ pub enum DatabaseOrDiscordError { #[derive(Debug, Error)] pub enum DatabaseError { #[error("An error occurred when querying the database")] - QueryError(ExecutionError), + QueryError(#[from] ExecutionError), #[error("The database responded but the result of the query weren't rows")] - ResultNotRows, + ResultNotRows(#[from] IntoRowsResultError), #[error("The resulting row could not be deserialized into an object")] - DeserializationFailed, + DeserializationFailed(#[from] DeserializationError), + + #[error("The database responded but the result of the query weren't rows")] + RowsError(#[from] RowsError), + + #[error("The resulting row could not be deserialized into an object")] + SingleRowFailed(#[from] SingleRowError), + + #[error("The database responded but the first row raised an error")] + FirstRowError(#[from] FirstRowError), #[error("An unknown error occurred")] Unknown(&'static str), @@ -72,10 +84,8 @@ async fn get_single_row_from_result<'a, T: Object<'a>>( match result.single_row::() { Ok(row) => Ok(Some(T::from_row(row))), Err(error) => match error { - SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => { - Err(DatabaseError::DeserializationFailed) - } SingleRowError::UnexpectedRowCount(_) => Ok(None), + _ => Err(error.into()), }, } } @@ -170,10 +180,8 @@ impl User { "#, (id,), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; get_single_row_from_result(&result).await } @@ -301,10 +309,8 @@ impl User { "#, (), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; Ok(result.rows_num()) } @@ -364,10 +370,8 @@ impl UserByName { "#, (name,), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; get_single_row_from_result(&result).await } @@ -423,20 +427,10 @@ impl Server { "#, (id,), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; - match result.single_row::<::Row>() { - Ok(row) => Ok(Some(Self::from_row(row))), - Err(error) => match error { - SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => { - Err(DatabaseError::DeserializationFailed) - } - SingleRowError::UnexpectedRowCount(_) => Ok(None), - }, - } + get_single_row_from_result(&result).await } pub async fn get_or_create_by_id( @@ -643,10 +637,8 @@ impl Channel { "#, (id,), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; get_single_row_from_result(&result).await } @@ -789,16 +781,12 @@ impl SearchingChannel { "#, (), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; Ok(result - .rows::<::Row>() - .map_err(|_| DatabaseError::ResultNotRows)? - .collect::, _>>() - .map_err(|_| DatabaseError::DeserializationFailed)? + .rows::<::Row>()? + .collect::, _>>()? .into_iter() .map(Self::from_row) .collect::>()) @@ -865,8 +853,7 @@ impl Call { ) .await .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .into_rows_result()?; get_single_row_from_result(&result).await } @@ -952,14 +939,11 @@ impl Call { ), ) .await - .inspect_err(|e| log::error!("Failed to add channel to the call! {e:?}")) - .map_err(DatabaseError::QueryError)? + .inspect_err(|e| log::error!("Failed to add channel to the call! {e:?}"))? .into_rows_result() - .inspect_err(|e| log::error!("Failed to get result of the query! {e:?}")) - .map_err(|_| DatabaseError::ResultNotRows)? + .inspect_err(|e| log::error!("Failed to get result of the query! {e:?}"))? .first_row::<(bool, Option)>() - .inspect_err(|e| log::error!("Failed to get first row of the result! {e:?}")) - .map_err(|_| DatabaseError::DeserializationFailed)?; + .inspect_err(|e| log::error!("Failed to get first row of the result! {e:?}"))?; log::trace!("Is applied: {is_applied:?}"); @@ -1075,10 +1059,8 @@ impl Call { "#, (), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; Ok(result.rows_num()) } @@ -1115,7 +1097,7 @@ pub struct MessageBuilderError; #[derive(Default)] pub struct MessageBuilder { message: Option, - profile: Option, + participant: Option, call_id: Option, is_censored: bool, propagation_message_id: Option, @@ -1127,7 +1109,7 @@ impl MessageBuilder { fn new() -> Self { Self { message: None, - profile: None, + participant: None, call_id: None, is_censored: false, propagation_message_id: None, @@ -1141,8 +1123,8 @@ impl MessageBuilder { self } - pub fn profile(mut self, profile: DisplayProfile) -> Self { - self.profile = Some(profile); + pub fn participant(mut self, participant: Participant) -> Self { + self.participant = Some(participant); self } @@ -1176,16 +1158,16 @@ impl MessageBuilder { pub fn build(self) -> Result { let message = self.message.ok_or(MessageBuilderError)?; - let profile = self.profile.ok_or(MessageBuilderError)?; + let participant = self.participant.ok_or(MessageBuilderError)?; let call_id = self.call_id.ok_or(MessageBuilderError)?; let data = MessageData { id: u64_to_i64(message.id.get()), call_id: u64_to_i64(call_id), - author_id: u64_to_i64(profile.id), - author_name: profile.name.clone(), - author_avatar_url: profile.avatar_url.clone(), - author_mask: profile.mask, + author_id: u64_to_i64(participant.user_id), + author_name: participant.user_name, + author_avatar_url: participant.user_avatar_url, + author_mask: participant.is_masked.then_some(participant.user_mask), content: message.content, attachments: message.attachments.iter().map(|a| a.url.clone()).collect(), sticker: message @@ -1206,7 +1188,7 @@ impl MessageBuilder { Ok(Message { call_id, message_id: message.id.get(), - author_id: profile.id, + author_id: participant.user_id, propagation_message_id: self.propagation_message_id, data, }) @@ -1276,10 +1258,8 @@ impl Message { "#, (call_id, message_id_i64), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; if result.rows_num() == 0 { Self::get_by_propagation_message_id(message_id, session).await @@ -1317,10 +1297,46 @@ impl Message { u64_to_i64(message_by_propagation_id.message_id), ), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; + + return get_single_row_from_result(&result).await; + } + + Ok(None) + } + + pub async fn get_last_by_author_id( + call_id: u64, + author_id: u64, + session: &Session, + ) -> Result, DatabaseError> { + let message_by_author_id = + MessageByAuthorId::get_last_by_author_id(call_id, author_id, session).await?; + + if let Some(message_by_author_id) = message_by_author_id { + let result = session + .query_unpaged( + r#" + SELECT + call_id, + message_id, + author_id, + propagation_message_id, + data + FROM + payphone.messages + WHERE + call_id = ? AND message_id = ? + LIMIT 1; + "#, + ( + u64_to_i64(message_by_author_id.call_id), + u64_to_i64(message_by_author_id.message_id), + ), + ) + .await? + .into_rows_result()?; return get_single_row_from_result(&result).await; } @@ -1356,10 +1372,8 @@ impl Message { "#, (), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; Ok(result.rows_num()) } @@ -1413,10 +1427,67 @@ impl MessageByPropagationId { "#, (propagation_message_id,), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; + + get_single_row_from_result(&result).await + } +} + +pub struct MessageByAuthorId { + pub call_id: u64, + pub message_id: u64, + pub author_id: u64, +} + +impl<'a> Object<'a> for MessageByAuthorId { + type Row = (i64, i64, i64); + + fn from_row(row: Self::Row) -> Self { + Self { + call_id: i64_to_u64(row.0), + message_id: i64_to_u64(row.1), + author_id: i64_to_u64(row.2), + } + } + + fn to_row(value: Self) -> Self::Row { + ( + u64_to_i64(value.call_id), + u64_to_i64(value.message_id), + u64_to_i64(value.author_id), + ) + } +} + +impl MessageByAuthorId { + pub async fn get_last_by_author_id( + call_id: u64, + author_id: u64, + session: &Session, + ) -> Result, DatabaseError> { + let call_id = u64_to_i64(call_id); + let author_id = u64_to_i64(author_id); + + let result = session + .query_unpaged( + r#" + SELECT + call_id, + message_id, + author_id + FROM + payphone.messages_by_author_id + WHERE + call_id = ? AND + author_id = ? + ORDER BY message_id DESC + LIMIT 1; + "#, + (call_id, author_id), + ) + .await? + .into_rows_result()?; get_single_row_from_result(&result).await } @@ -1637,10 +1708,8 @@ impl UserIncident { "#, (user_id, incident_id), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; get_single_row_from_result(&result).await } @@ -1669,16 +1738,12 @@ impl UserIncident { "#, (user_id,), ) - .await - .map_err(DatabaseError::QueryError)? - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)?; + .await? + .into_rows_result()?; Ok(result - .rows::<::Row>() - .map_err(|_| DatabaseError::ResultNotRows)? - .collect::, _>>() - .map_err(|_| DatabaseError::DeserializationFailed)? + .rows::<::Row>()? + .collect::, _>>()? .into_iter() .map(Self::from_row) .collect::>()) @@ -1789,14 +1854,48 @@ impl<'a> Object<'a> for Participant { } } +fn make_name_valid(name: String) -> String { + if name.len() == 1 { + format!("{name}\u{2800}") + } else if name.len() > 80 { + format!("{name:.80}") + } else { + name + } +} + impl Participant { + pub fn display_name(&self) -> String { + if self.is_masked { + get_mask_display_name(self.user_mask.clone()) + } else { + make_name_valid(self.user_name.clone()) + } + } + + pub fn avatar_url(&self) -> String { + if self.is_masked { + get_mask_avatar_url(self.user_mask.clone()).expect("Failed to get mask avatar URL") + } else { + self.user_avatar_url.clone() + } + } + pub async fn get_for_call(call_id: u64, session: &Session) -> Result, DatabaseError> { let call_id = u64_to_i64(call_id); let rows = session .query_unpaged( r#" - SELECT * FROM payphone.participants + SELECT + call_id, + user_id, + channel_id, + user_name, + user_avatar_url, + user_mask, + is_masked + FROM payphone.participants WHERE call_id = ?; "#, (call_id,), @@ -1805,27 +1904,69 @@ impl Participant { .map_err(DatabaseError::QueryError)?; Ok(rows - .into_rows_result() - .map_err(|_| DatabaseError::ResultNotRows)? - .rows() - .map_err(|_| DatabaseError::ResultNotRows)? - .collect::, _>>() - .map_err(|_| DatabaseError::DeserializationFailed)? + .into_rows_result()? + .rows()? + .collect::, _>>()? .into_iter() .map(Self::from_row) .collect::>()) } - pub async fn create( + pub async fn get_for_user_and_channel_in_call( user_id: u64, channel_id: u64, call_id: u64, - profile: DisplayProfile, + session: &Session, + ) -> Result, DatabaseError> { + let user_id = u64_to_i64(user_id); + let channel_id = u64_to_i64(channel_id); + let call_id = u64_to_i64(call_id); + + let result = session + .query_unpaged( + r#" + SELECT + call_id, + user_id, + channel_id, + user_name, + user_avatar_url, + user_mask, + is_masked + FROM + payphone.participants + WHERE + call_id = ? AND user_id = ? AND channel_id = ? + LIMIT 1; + "#, + (call_id, user_id, channel_id), + ) + .await? + .into_rows_result()?; + + get_single_row_from_result(&result).await + } + + pub async fn get_or_create( + user_id: u64, + channel_id: u64, + call_id: u64, + display_name: String, + display_avatar_url: String, is_masked: bool, session: &Session, - ) -> Result<(), DatabaseError> { - let masks_in_use = Self::get_for_call(call_id, session) - .await? + ) -> Result { + let participants = Self::get_for_call(call_id, session).await?; + + if let Some(participant) = participants + .iter() + .find(|p| p.user_id == user_id && p.channel_id == channel_id) + { + // User is already a participant in the call + return Ok(participant.clone()); + } + + let masks_in_use = participants .iter() .filter_map(|p| { if p.channel_id == channel_id { @@ -1844,13 +1985,15 @@ impl Participant { call_id, user_id, channel_id, - user_name: profile.name, - user_avatar_url: profile.avatar_url, + user_name: display_name, + user_avatar_url: display_avatar_url, user_mask: mask, is_masked, }; - participant.insert(session).await + participant.insert(session).await?; + + Ok(participant) } pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> { @@ -1865,7 +2008,29 @@ impl Participant { user_avatar_url, user_mask, is_masked - ) IF NOT EXISTS VALUES (?, ?, ?, ?, ?, ?, ?); + ) VALUES (?, ?, ?, ?, ?, ?, ?) IF NOT EXISTS; + "#, + Self::to_row(self.clone()), + ) + .await + .map_err(DatabaseError::QueryError)?; + + Ok(()) + } + + pub async fn upsert(&self, session: &Session) -> Result<(), DatabaseError> { + session + .query_unpaged( + r#" + INSERT INTO payphone.participants ( + call_id, + user_id, + channel_id, + user_name, + user_avatar_url, + user_mask, + is_masked + ) VALUES (?, ?, ?, ?, ?, ?, ?); "#, Self::to_row(self.clone()), ) diff --git a/src/framework/commands/arguments.rs b/src/framework/commands/arguments.rs index fac6c9d..9e86102 100644 --- a/src/framework/commands/arguments.rs +++ b/src/framework/commands/arguments.rs @@ -171,7 +171,7 @@ impl FromArguments for bool { } } - match arg { + match arg.to_lowercase().as_str() { "y" | "yes" | "on" | "true" | "t" | "1" => Ok((true, rest)), "n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)), _ => Err(ArgumentParsingError::InvalidArgument), diff --git a/src/groups/calls/commands.rs b/src/groups/calls/commands.rs index 2925de9..62e94d0 100644 --- a/src/groups/calls/commands.rs +++ b/src/groups/calls/commands.rs @@ -3,7 +3,7 @@ use scc::HashSet; use twilight_model::id::Id; use crate::{ - database::models::{Call, Channel}, + database::models::{Call, Channel, Participant}, discord::sanitizing::SanitizeDiscordMarkdown, extras::Extras, framework::commands::{ @@ -13,7 +13,7 @@ use crate::{ permissions::Permission, }, groups::calls::{ - get_display_profile, + masks::{get_mask_display_name, get_mask_emoji}, permissions::{NonNewAccountPermission, NotBannedPermission}, strings::{ get_string_call_answered, get_string_call_hangup_other_party, @@ -414,7 +414,20 @@ impl Command for FriendRequestCommand { let other_channel_id = call.other_party_channel_id(channel.id); - let profile = get_display_profile(ctx.event); + 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 @@ -425,7 +438,7 @@ impl Command for FriendRequestCommand { .content( &format!( "📬 **{}** wants you to send them a friend request on Discord! Their username is `{}`.", - profile.name.sanitize(false), + participant.display_name().sanitize(false), ctx.event.author.name.sanitize(false) ) ) @@ -505,21 +518,50 @@ impl Command for MaskCommand { return Ok(()); }; + log::trace!("User is masked, checking for call..."); if let Some(call_id) = channel.call_id { - } else { - ctx - .send() - .content( - format!( - "🎭 Your mask was set to `{}`. You can toggle your mask on or off using `{}mask on` and `{}mask off`.", - if ctx.extras.user.is_masked { "on" } else { "off" }, + log::trace!("Found call ID: {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? + { + log::trace!( + "Participant's is_masked is being set to {}", + ctx.extras.user.is_masked + ); + + participant.is_masked = ctx.extras.user.is_masked; + participant.upsert(&ctx.rt.state.db).await?; + + if ctx.extras.user.is_masked { + ctx.send().content(format!("🎭 Your mask was 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 - ) - ) - .await?; + )).await?; + + return Ok(()); + } + } } + ctx + .send() + .content( + format!( + "🎭 Your mask was set to `{}`. You can toggle your mask on or off using `{}mask on` and `{}mask off`.", + if ctx.extras.user.is_masked { "on" } else { "off" }, + ctx.prefix, + ctx.prefix + ) + ) + .await?; + Ok(()) } } diff --git a/src/groups/calls/events.rs b/src/groups/calls/events.rs index f1c0897..82c47e2 100644 --- a/src/groups/calls/events.rs +++ b/src/groups/calls/events.rs @@ -9,17 +9,17 @@ use twilight_model::{ id::Id, }; use twilight_util::{ - builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource}, + builder::embed::{EmbedAuthorBuilder, EmbedBuilder, EmbedFooterBuilder, ImageSource}, snowflake::Snowflake, }; use crate::{ - constants::COLOR_WHITE, - database::models::{Call, Channel, Message, User, UserIncident}, - discord::sanitizing::SanitizeDiscordMarkdown, + constants::{COLOR_WHITE, COLOR_YELLOW}, + database::models::{Call, Channel, Message, Participant, User, UserIncident}, + discord::{images, sanitizing::SanitizeDiscordMarkdown}, extras::Extras, framework::{ - commands::{extras::ExtrasGetter, permissions::Permission}, + commands::extras::ExtrasGetter, events::traits::{ EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler, }, @@ -28,9 +28,9 @@ use crate::{ }, groups::calls::{ components::{CodeOfConductButton, NeedHelpButton}, - embeds, get_display_profile, + embeds, + masks::{get_mask_avatar_url, get_mask_display_name}, moderation::{Action, Rule, escape_message}, - permissions::NonNewAccountPermission, }, state::State, utils::{ellipse, i64_to_u64}, @@ -195,8 +195,6 @@ impl OnMessageHandler for CallMessageHandler { return Ok(()); }; - let profile = get_display_profile(event); - let (content, rules) = escape_message(event); let message: Message; @@ -217,6 +215,30 @@ impl OnMessageHandler for CallMessageHandler { }; 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, + ), + user.is_masked, + &rt.state.db, + ) + .await?; + if rules .iter() .any(|rule| rule.action() == &Action::BlockAndPenalty) @@ -232,14 +254,14 @@ impl OnMessageHandler for CallMessageHandler { .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) + participant.display_name().sanitize(false) )) .await?; message = Message::build() .call_id(call.id) .message(event.0.clone()) - .profile(profile.clone()) + .participant(participant) .is_censored(!rules.is_empty()) .replying_to_message_id(replying_to_id) .build() @@ -250,29 +272,72 @@ impl OnMessageHandler for CallMessageHandler { 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 + if let Some(propagation_channel_id) = replying_to.data.propagation_channel_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(), + ) + }; + + embeds.push( + EmbedBuilder::new() + .author( + EmbedAuthorBuilder::new(format!("Replying to {display_name}",)) + .icon_url(ImageSource::url(avatar_url).unwrap()), + ) + .color(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() )) - .icon_url( - ImageSource::url(replying_to.data.author_avatar_url).unwrap(), - ), - ) - .color(COLOR_WHITE) - .description(ellipse(replying_to.data.content, 100)) - .build(), - ); + .footer(EmbedFooterBuilder::new(format!( + "You can set your own mask using {}mask on.", + prefixes[0] + ))) + .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) + .username(&participant.display_name()) + .avatar_url(&participant.avatar_url()) .content(&content.sanitize_mentions()) .embeds(&embeds) .wait() @@ -307,7 +372,7 @@ impl OnMessageHandler for CallMessageHandler { message = Message::build() .call_id(call.id) .message(event.0.clone()) - .profile(profile.clone()) + .participant(participant) .is_censored(!rules.is_empty()) .replying_to_message_id(replying_to_id) .propagation_message_id(message_sent.id.get()) diff --git a/src/groups/calls/masks/mod.rs b/src/groups/calls/masks/mod.rs index a81f4da..2448529 100644 --- a/src/groups/calls/masks/mod.rs +++ b/src/groups/calls/masks/mod.rs @@ -4,6 +4,8 @@ use once_cell::sync::Lazy; use rand::{SeedableRng, seq::IndexedRandom}; use rand_chacha::ChaCha20Rng; +use crate::utils::ToTitleCase; + static MASKS_COMMON: Lazy> = Lazy::new(|| { let (file_name, json) = if cfg!(debug_assertions) { ("dev/common.json", include_str!("dev/common.json")) @@ -11,8 +13,8 @@ static MASKS_COMMON: Lazy> = Lazy::new(|| { ("prod/common.json", include_str!("prod/common.json")) }; - let masks: HashMap = - serde_json::from_str(json).expect(&format!("Failed to parse masks/{file_name} masks")); + let masks: HashMap = serde_json::from_str(json) + .unwrap_or_else(|_| panic!("Failed to parse masks/{file_name} masks")); assert_ne!(masks.len(), 0, "No masks found in masks/{file_name}"); @@ -30,3 +32,22 @@ pub fn get_random_mask(not_in: &[String]) -> Option<(String, u64)> { mask_names.choose(&mut rng).cloned() } + +pub fn get_mask_display_name(mask: impl ToString) -> String { + mask.to_title_case() +} + +pub fn get_mask_emoji(mask: impl ToString) -> Option { + let emoji_name = mask.to_string().replace(" ", ""); + let emoji_id = MASKS_COMMON.get(&mask.to_string())?; + + Some(format!("<:{emoji_name}:{emoji_id}>")) +} + +pub fn get_mask_avatar_url(mask: impl ToString) -> Option { + let mask_emoji_id = MASKS_COMMON.get(&mask.to_string())?; + + Some(format!( + "https://cdn.discordapp.com/emojis/{mask_emoji_id}.png?size=128&quality=lossless" + )) +} diff --git a/src/groups/calls/mod.rs b/src/groups/calls/mod.rs index 3c81fd7..425d88b 100644 --- a/src/groups/calls/mod.rs +++ b/src/groups/calls/mod.rs @@ -1,10 +1,5 @@ //! Call commands and events. -use twilight_model::gateway::payload::incoming::MessageCreate; -use twilight_validate::request::webhook_username; - -use crate::discord::images; - pub mod commands; pub(super) mod components; pub(super) mod embeds; @@ -13,62 +8,3 @@ pub mod masks; pub(super) mod moderation; pub(super) mod permissions; pub(super) mod strings; - -#[derive(Debug, Clone)] -pub struct DisplayProfile { - pub id: u64, - pub name: String, - pub avatar_url: String, - pub mask: Option, -} - -fn make_name_valid(name: String) -> String { - if name.len() == 1 { - format!("{name}\u{2800}") - } else if name.len() > 80 { - format!("{name:.80}") - } else { - name - } -} - -fn get_display_profile(event: &MessageCreate) -> DisplayProfile { - let member = event.member.clone(); - - let name = if let Some(nick) = &member - .clone() - .and_then(|member| member.nick.map(make_name_valid)) - { - if webhook_username(nick).is_err() { - "Invalid Nickname".to_string() - } else { - nick.clone() - } - } else if let Some(name) = event.author.global_name.clone().map(make_name_valid) { - if webhook_username(&name).is_err() { - "Invalid Global Name".to_string() - } else { - name - } - } else if webhook_username(make_name_valid(event.author.name.clone())).is_err() { - "Invalid Username".to_string() - } else { - make_name_valid(event.author.name.clone()) - }; - - let avatar_hash = member - .and_then(|inner| inner.avatar) - .or(event.author.avatar); - let avatar_url = images::get_avatar_url( - avatar_hash, - event.author.id.get(), - event.author.discriminator as u64, - ); - - DisplayProfile { - id: event.author.id.get(), - name, - avatar_url, - mask: None, - } -} diff --git a/src/groups/calls/permissions.rs b/src/groups/calls/permissions.rs index 8f313e3..a52908c 100644 --- a/src/groups/calls/permissions.rs +++ b/src/groups/calls/permissions.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use time::{Duration, UtcDateTime}; -use twilight_util::snowflake::{self, Snowflake}; +use twilight_util::snowflake::Snowflake; use crate::{ database::models::UserIncident,