Add support for masks

This commit is contained in:
2025-08-24 02:02:25 -03:00
parent 2a2ffd8894
commit 325b4cbc16
8 changed files with 454 additions and 221 deletions
@@ -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);
+276 -111
View File
@@ -2,7 +2,10 @@ use scylla::{
DeserializeValue, SerializeValue, DeserializeValue, SerializeValue,
client::session::Session, client::session::Session,
deserialize::row::DeserializeRow, deserialize::row::DeserializeRow,
errors::{ExecutionError, SingleRowError}, errors::{
DeserializationError, ExecutionError, FirstRowError, IntoRowsResultError, RowsError,
SingleRowError,
},
response::query_result::QueryRowsResult, response::query_result::QueryRowsResult,
statement::{ statement::{
Statement, Statement,
@@ -21,7 +24,7 @@ use twilight_model::{
use crate::{ use crate::{
constants::DEFAULT_PREFIX, 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}, utils::{get_random_u64, i64_to_u64, u64_to_i64},
}; };
@@ -40,13 +43,22 @@ pub enum DatabaseOrDiscordError {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum DatabaseError { pub enum DatabaseError {
#[error("An error occurred when querying the database")] #[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")] #[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")] #[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")] #[error("An unknown error occurred")]
Unknown(&'static str), Unknown(&'static str),
@@ -72,10 +84,8 @@ async fn get_single_row_from_result<'a, T: Object<'a>>(
match result.single_row::<T::Row>() { match result.single_row::<T::Row>() {
Ok(row) => Ok(Some(T::from_row(row))), Ok(row) => Ok(Some(T::from_row(row))),
Err(error) => match error { Err(error) => match error {
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
Err(DatabaseError::DeserializationFailed)
}
SingleRowError::UnexpectedRowCount(_) => Ok(None), SingleRowError::UnexpectedRowCount(_) => Ok(None),
_ => Err(error.into()),
}, },
} }
} }
@@ -170,10 +180,8 @@ impl User {
"#, "#,
(id,), (id,),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await get_single_row_from_result(&result).await
} }
@@ -301,10 +309,8 @@ impl User {
"#, "#,
(), (),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result.rows_num()) Ok(result.rows_num())
} }
@@ -364,10 +370,8 @@ impl UserByName {
"#, "#,
(name,), (name,),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await get_single_row_from_result(&result).await
} }
@@ -423,20 +427,10 @@ impl Server {
"#, "#,
(id,), (id,),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
match result.single_row::<<Server as Object>::Row>() { get_single_row_from_result(&result).await
Ok(row) => Ok(Some(Self::from_row(row))),
Err(error) => match error {
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
Err(DatabaseError::DeserializationFailed)
}
SingleRowError::UnexpectedRowCount(_) => Ok(None),
},
}
} }
pub async fn get_or_create_by_id( pub async fn get_or_create_by_id(
@@ -643,10 +637,8 @@ impl Channel {
"#, "#,
(id,), (id,),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await get_single_row_from_result(&result).await
} }
@@ -789,16 +781,12 @@ impl SearchingChannel {
"#, "#,
(), (),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result Ok(result
.rows::<<SearchingChannel as Object>::Row>() .rows::<<SearchingChannel as Object>::Row>()?
.map_err(|_| DatabaseError::ResultNotRows)? .collect::<Result<Vec<_>, _>>()?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DatabaseError::DeserializationFailed)?
.into_iter() .into_iter()
.map(Self::from_row) .map(Self::from_row)
.collect::<Vec<_>>()) .collect::<Vec<_>>())
@@ -865,8 +853,7 @@ impl Call {
) )
.await .await
.map_err(DatabaseError::QueryError)? .map_err(DatabaseError::QueryError)?
.into_rows_result() .into_rows_result()?;
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await get_single_row_from_result(&result).await
} }
@@ -952,14 +939,11 @@ impl Call {
), ),
) )
.await .await
.inspect_err(|e| log::error!("Failed to add channel to the call! {e:?}")) .inspect_err(|e| log::error!("Failed to add channel to the call! {e:?}"))?
.map_err(DatabaseError::QueryError)?
.into_rows_result() .into_rows_result()
.inspect_err(|e| log::error!("Failed to get result of the query! {e:?}")) .inspect_err(|e| log::error!("Failed to get result of the query! {e:?}"))?
.map_err(|_| DatabaseError::ResultNotRows)?
.first_row::<(bool, Option<i64>)>() .first_row::<(bool, Option<i64>)>()
.inspect_err(|e| log::error!("Failed to get first row of the result! {e:?}")) .inspect_err(|e| log::error!("Failed to get first row of the result! {e:?}"))?;
.map_err(|_| DatabaseError::DeserializationFailed)?;
log::trace!("Is applied: {is_applied:?}"); log::trace!("Is applied: {is_applied:?}");
@@ -1075,10 +1059,8 @@ impl Call {
"#, "#,
(), (),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result.rows_num()) Ok(result.rows_num())
} }
@@ -1115,7 +1097,7 @@ pub struct MessageBuilderError;
#[derive(Default)] #[derive(Default)]
pub struct MessageBuilder { pub struct MessageBuilder {
message: Option<TwilightMessage>, message: Option<TwilightMessage>,
profile: Option<DisplayProfile>, participant: Option<Participant>,
call_id: Option<u64>, call_id: Option<u64>,
is_censored: bool, is_censored: bool,
propagation_message_id: Option<u64>, propagation_message_id: Option<u64>,
@@ -1127,7 +1109,7 @@ impl MessageBuilder {
fn new() -> Self { fn new() -> Self {
Self { Self {
message: None, message: None,
profile: None, participant: None,
call_id: None, call_id: None,
is_censored: false, is_censored: false,
propagation_message_id: None, propagation_message_id: None,
@@ -1141,8 +1123,8 @@ impl MessageBuilder {
self self
} }
pub fn profile(mut self, profile: DisplayProfile) -> Self { pub fn participant(mut self, participant: Participant) -> Self {
self.profile = Some(profile); self.participant = Some(participant);
self self
} }
@@ -1176,16 +1158,16 @@ impl MessageBuilder {
pub fn build(self) -> Result<Message, MessageBuilderError> { pub fn build(self) -> Result<Message, MessageBuilderError> {
let message = self.message.ok_or(MessageBuilderError)?; 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 call_id = self.call_id.ok_or(MessageBuilderError)?;
let data = MessageData { let data = MessageData {
id: u64_to_i64(message.id.get()), id: u64_to_i64(message.id.get()),
call_id: u64_to_i64(call_id), call_id: u64_to_i64(call_id),
author_id: u64_to_i64(profile.id), author_id: u64_to_i64(participant.user_id),
author_name: profile.name.clone(), author_name: participant.user_name,
author_avatar_url: profile.avatar_url.clone(), author_avatar_url: participant.user_avatar_url,
author_mask: profile.mask, author_mask: participant.is_masked.then_some(participant.user_mask),
content: message.content, content: message.content,
attachments: message.attachments.iter().map(|a| a.url.clone()).collect(), attachments: message.attachments.iter().map(|a| a.url.clone()).collect(),
sticker: message sticker: message
@@ -1206,7 +1188,7 @@ impl MessageBuilder {
Ok(Message { Ok(Message {
call_id, call_id,
message_id: message.id.get(), message_id: message.id.get(),
author_id: profile.id, author_id: participant.user_id,
propagation_message_id: self.propagation_message_id, propagation_message_id: self.propagation_message_id,
data, data,
}) })
@@ -1276,10 +1258,8 @@ impl Message {
"#, "#,
(call_id, message_id_i64), (call_id, message_id_i64),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
if result.rows_num() == 0 { if result.rows_num() == 0 {
Self::get_by_propagation_message_id(message_id, session).await 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), u64_to_i64(message_by_propagation_id.message_id),
), ),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?; 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<Option<Self>, 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; return get_single_row_from_result(&result).await;
} }
@@ -1356,10 +1372,8 @@ impl Message {
"#, "#,
(), (),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result.rows_num()) Ok(result.rows_num())
} }
@@ -1413,10 +1427,67 @@ impl MessageByPropagationId {
"#, "#,
(propagation_message_id,), (propagation_message_id,),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?; 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<Option<Self>, 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 get_single_row_from_result(&result).await
} }
@@ -1637,10 +1708,8 @@ impl UserIncident {
"#, "#,
(user_id, incident_id), (user_id, incident_id),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await get_single_row_from_result(&result).await
} }
@@ -1669,16 +1738,12 @@ impl UserIncident {
"#, "#,
(user_id,), (user_id,),
) )
.await .await?
.map_err(DatabaseError::QueryError)? .into_rows_result()?;
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result Ok(result
.rows::<<Self as Object>::Row>() .rows::<<Self as Object>::Row>()?
.map_err(|_| DatabaseError::ResultNotRows)? .collect::<Result<Vec<_>, _>>()?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DatabaseError::DeserializationFailed)?
.into_iter() .into_iter()
.map(Self::from_row) .map(Self::from_row)
.collect::<Vec<_>>()) .collect::<Vec<_>>())
@@ -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 { 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<Vec<Self>, DatabaseError> { pub async fn get_for_call(call_id: u64, session: &Session) -> Result<Vec<Self>, DatabaseError> {
let call_id = u64_to_i64(call_id); let call_id = u64_to_i64(call_id);
let rows = session let rows = session
.query_unpaged( .query_unpaged(
r#" 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 = ?; WHERE call_id = ?;
"#, "#,
(call_id,), (call_id,),
@@ -1805,27 +1904,69 @@ impl Participant {
.map_err(DatabaseError::QueryError)?; .map_err(DatabaseError::QueryError)?;
Ok(rows Ok(rows
.into_rows_result() .into_rows_result()?
.map_err(|_| DatabaseError::ResultNotRows)? .rows()?
.rows() .collect::<Result<Vec<_>, _>>()?
.map_err(|_| DatabaseError::ResultNotRows)?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DatabaseError::DeserializationFailed)?
.into_iter() .into_iter()
.map(Self::from_row) .map(Self::from_row)
.collect::<Vec<_>>()) .collect::<Vec<_>>())
} }
pub async fn create( pub async fn get_for_user_and_channel_in_call(
user_id: u64, user_id: u64,
channel_id: u64, channel_id: u64,
call_id: u64, call_id: u64,
profile: DisplayProfile, session: &Session,
) -> Result<Option<Self>, 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, is_masked: bool,
session: &Session, session: &Session,
) -> Result<(), DatabaseError> { ) -> Result<Self, DatabaseError> {
let masks_in_use = Self::get_for_call(call_id, session) let participants = Self::get_for_call(call_id, session).await?;
.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() .iter()
.filter_map(|p| { .filter_map(|p| {
if p.channel_id == channel_id { if p.channel_id == channel_id {
@@ -1844,13 +1985,15 @@ impl Participant {
call_id, call_id,
user_id, user_id,
channel_id, channel_id,
user_name: profile.name, user_name: display_name,
user_avatar_url: profile.avatar_url, user_avatar_url: display_avatar_url,
user_mask: mask, user_mask: mask,
is_masked, is_masked,
}; };
participant.insert(session).await participant.insert(session).await?;
Ok(participant)
} }
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> { pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
@@ -1865,7 +2008,29 @@ impl Participant {
user_avatar_url, user_avatar_url,
user_mask, user_mask,
is_masked 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()), Self::to_row(self.clone()),
) )
+1 -1
View File
@@ -171,7 +171,7 @@ impl<S, E> FromArguments<S, E> for bool {
} }
} }
match arg { match arg.to_lowercase().as_str() {
"y" | "yes" | "on" | "true" | "t" | "1" => Ok((true, rest)), "y" | "yes" | "on" | "true" | "t" | "1" => Ok((true, rest)),
"n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)), "n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)),
_ => Err(ArgumentParsingError::InvalidArgument), _ => Err(ArgumentParsingError::InvalidArgument),
+48 -6
View File
@@ -3,7 +3,7 @@ use scc::HashSet;
use twilight_model::id::Id; use twilight_model::id::Id;
use crate::{ use crate::{
database::models::{Call, Channel}, database::models::{Call, Channel, Participant},
discord::sanitizing::SanitizeDiscordMarkdown, discord::sanitizing::SanitizeDiscordMarkdown,
extras::Extras, extras::Extras,
framework::commands::{ framework::commands::{
@@ -13,7 +13,7 @@ use crate::{
permissions::Permission, permissions::Permission,
}, },
groups::calls::{ groups::calls::{
get_display_profile, masks::{get_mask_display_name, get_mask_emoji},
permissions::{NonNewAccountPermission, NotBannedPermission}, permissions::{NonNewAccountPermission, NotBannedPermission},
strings::{ strings::{
get_string_call_answered, get_string_call_hangup_other_party, get_string_call_answered, get_string_call_hangup_other_party,
@@ -414,7 +414,20 @@ impl Command<State, Extras> for FriendRequestCommand {
let other_channel_id = call.other_party_channel_id(channel.id); 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 ctx
.rt .rt
@@ -425,7 +438,7 @@ impl Command<State, Extras> for FriendRequestCommand {
.content( .content(
&format!( &format!(
"📬 **{}** wants you to send them a friend request on Discord! Their username is `{}`.", "📬 **{}** 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) ctx.event.author.name.sanitize(false)
) )
) )
@@ -505,8 +518,38 @@ impl Command<State, Extras> for MaskCommand {
return Ok(()); return Ok(());
}; };
log::trace!("User is masked, checking for call...");
if let Some(call_id) = channel.call_id { if let Some(call_id) = channel.call_id {
} else { 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?;
return Ok(());
}
}
}
ctx ctx
.send() .send()
.content( .content(
@@ -518,7 +561,6 @@ impl Command<State, Extras> for MaskCommand {
) )
) )
.await?; .await?;
}
Ok(()) Ok(())
} }
+86 -21
View File
@@ -9,17 +9,17 @@ use twilight_model::{
id::Id, id::Id,
}; };
use twilight_util::{ use twilight_util::{
builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource}, builder::embed::{EmbedAuthorBuilder, EmbedBuilder, EmbedFooterBuilder, ImageSource},
snowflake::Snowflake, snowflake::Snowflake,
}; };
use crate::{ use crate::{
constants::COLOR_WHITE, constants::{COLOR_WHITE, COLOR_YELLOW},
database::models::{Call, Channel, Message, User, UserIncident}, database::models::{Call, Channel, Message, Participant, User, UserIncident},
discord::sanitizing::SanitizeDiscordMarkdown, discord::{images, sanitizing::SanitizeDiscordMarkdown},
extras::Extras, extras::Extras,
framework::{ framework::{
commands::{extras::ExtrasGetter, permissions::Permission}, commands::extras::ExtrasGetter,
events::traits::{ events::traits::{
EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler, EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
}, },
@@ -28,9 +28,9 @@ use crate::{
}, },
groups::calls::{ groups::calls::{
components::{CodeOfConductButton, NeedHelpButton}, components::{CodeOfConductButton, NeedHelpButton},
embeds, get_display_profile, embeds,
masks::{get_mask_avatar_url, get_mask_display_name},
moderation::{Action, Rule, escape_message}, moderation::{Action, Rule, escape_message},
permissions::NonNewAccountPermission,
}, },
state::State, state::State,
utils::{ellipse, i64_to_u64}, utils::{ellipse, i64_to_u64},
@@ -195,8 +195,6 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
return Ok(()); return Ok(());
}; };
let profile = get_display_profile(event);
let (content, rules) = escape_message(event); let (content, rules) = escape_message(event);
let message: Message; let message: Message;
@@ -217,6 +215,30 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
}; };
let replying_to_id = replying_to.as_ref().map(|i| i.message_id); 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 if rules
.iter() .iter()
.any(|rule| rule.action() == &Action::BlockAndPenalty) .any(|rule| rule.action() == &Action::BlockAndPenalty)
@@ -232,14 +254,14 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
.create_message(Id::new(opposite_channel.id)) .create_message(Id::new(opposite_channel.id))
.content(&format!( .content(&format!(
"🚩 **{}** Sent a message that was blocked.\n-# If you see an inappropiate message, please report it using `Message > Apps > Report Message`.", "🚩 **{}** 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?; .await?;
message = Message::build() message = Message::build()
.call_id(call.id) .call_id(call.id)
.message(event.0.clone()) .message(event.0.clone())
.profile(profile.clone()) .participant(participant)
.is_censored(!rules.is_empty()) .is_censored(!rules.is_empty())
.replying_to_message_id(replying_to_id) .replying_to_message_id(replying_to_id)
.build() .build()
@@ -250,29 +272,72 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
let mut embeds = vec![]; let mut embeds = vec![];
if let Some(replying_to) = replying_to { if let Some(replying_to) = replying_to {
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( embeds.push(
EmbedBuilder::new() EmbedBuilder::new()
.author( .author(
EmbedAuthorBuilder::new(format!( EmbedAuthorBuilder::new(format!("Replying to {display_name}",))
"Replying to {}", .icon_url(ImageSource::url(avatar_url).unwrap()),
replying_to.data.author_name
))
.icon_url(
ImageSource::url(replying_to.data.author_avatar_url).unwrap(),
),
) )
.color(COLOR_WHITE) .color(COLOR_WHITE)
.description(ellipse(replying_to.data.content, 100)) .description(ellipse(replying_to.data.content, 100))
.build(), .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 message_sent = match rt let message_sent = match rt
.http .http
.execute_webhook(Id::new(webhook_id), &webhook_token) .execute_webhook(Id::new(webhook_id), &webhook_token)
.allowed_mentions(None) .allowed_mentions(None)
.username(&profile.name) .username(&participant.display_name())
.avatar_url(&profile.avatar_url) .avatar_url(&participant.avatar_url())
.content(&content.sanitize_mentions()) .content(&content.sanitize_mentions())
.embeds(&embeds) .embeds(&embeds)
.wait() .wait()
@@ -307,7 +372,7 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
message = Message::build() message = Message::build()
.call_id(call.id) .call_id(call.id)
.message(event.0.clone()) .message(event.0.clone())
.profile(profile.clone()) .participant(participant)
.is_censored(!rules.is_empty()) .is_censored(!rules.is_empty())
.replying_to_message_id(replying_to_id) .replying_to_message_id(replying_to_id)
.propagation_message_id(message_sent.id.get()) .propagation_message_id(message_sent.id.get())
+23 -2
View File
@@ -4,6 +4,8 @@ use once_cell::sync::Lazy;
use rand::{SeedableRng, seq::IndexedRandom}; use rand::{SeedableRng, seq::IndexedRandom};
use rand_chacha::ChaCha20Rng; use rand_chacha::ChaCha20Rng;
use crate::utils::ToTitleCase;
static MASKS_COMMON: Lazy<HashMap<String, u64>> = Lazy::new(|| { static MASKS_COMMON: Lazy<HashMap<String, u64>> = Lazy::new(|| {
let (file_name, json) = if cfg!(debug_assertions) { let (file_name, json) = if cfg!(debug_assertions) {
("dev/common.json", include_str!("dev/common.json")) ("dev/common.json", include_str!("dev/common.json"))
@@ -11,8 +13,8 @@ static MASKS_COMMON: Lazy<HashMap<String, u64>> = Lazy::new(|| {
("prod/common.json", include_str!("prod/common.json")) ("prod/common.json", include_str!("prod/common.json"))
}; };
let masks: HashMap<String, u64> = let masks: HashMap<String, u64> = serde_json::from_str(json)
serde_json::from_str(json).expect(&format!("Failed to parse masks/{file_name} masks")); .unwrap_or_else(|_| panic!("Failed to parse masks/{file_name} masks"));
assert_ne!(masks.len(), 0, "No masks found in masks/{file_name}"); 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() 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<String> {
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<String> {
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"
))
}
-64
View File
@@ -1,10 +1,5 @@
//! Call commands and events. //! 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 mod commands;
pub(super) mod components; pub(super) mod components;
pub(super) mod embeds; pub(super) mod embeds;
@@ -13,62 +8,3 @@ pub mod masks;
pub(super) mod moderation; pub(super) mod moderation;
pub(super) mod permissions; pub(super) mod permissions;
pub(super) mod strings; pub(super) mod strings;
#[derive(Debug, Clone)]
pub struct DisplayProfile {
pub id: u64,
pub name: String,
pub avatar_url: String,
pub mask: Option<String>,
}
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,
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use time::{Duration, UtcDateTime}; use time::{Duration, UtcDateTime};
use twilight_util::snowflake::{self, Snowflake}; use twilight_util::snowflake::Snowflake;
use crate::{ use crate::{
database::models::UserIncident, database::models::UserIncident,