Add support for masks
This commit is contained in:
@@ -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<State, Extras> 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<State, Extras> 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<State, Extras> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
+93
-28
@@ -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<State, Extras> 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<State, Extras> 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<State, Extras> 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<State, Extras> 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<State, Extras> 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())
|
||||
|
||||
@@ -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<HashMap<String, u64>> = 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<HashMap<String, u64>> = Lazy::new(|| {
|
||||
("prod/common.json", include_str!("prod/common.json"))
|
||||
};
|
||||
|
||||
let masks: HashMap<String, u64> =
|
||||
serde_json::from_str(json).expect(&format!("Failed to parse masks/{file_name} masks"));
|
||||
let masks: HashMap<String, u64> = 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<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"
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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<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,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,
|
||||
|
||||
Reference in New Issue
Block a user