Restrict accounts to min 30 days to be able to call and start working on masks

This commit is contained in:
2025-08-23 19:56:50 -03:00
parent 134d4e8879
commit 2a2ffd8894
14 changed files with 368 additions and 20 deletions
Generated
+1 -1
View File
@@ -1437,7 +1437,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]] [[package]]
name = "payphone" name = "payphone"
version = "4.2.21" version = "4.2.24"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "payphone" name = "payphone"
version = "4.2.21" version = "4.2.24"
edition = "2024" edition = "2024"
default-run = "payphone" default-run = "payphone"
@@ -33,7 +33,7 @@ twilight-cache-inmemory = { version = "0.16.0", features = ["permission-calculat
twilight-gateway = "0.16.0" twilight-gateway = "0.16.0"
twilight-http = { version = "0.16.0", features = ["simd-json"] } twilight-http = { version = "0.16.0", features = ["simd-json"] }
twilight-model = "0.16.0" twilight-model = "0.16.0"
twilight-util = { version = "0.16.0", features = ["builder", "permission-calculator"] } twilight-util = { version = "0.16.0", features = ["builder", "permission-calculator", "snowflake"] }
twilight-validate = "0.16.0" twilight-validate = "0.16.0"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unidecode = "0.3.0" unidecode = "0.3.0"
+132 -1
View File
@@ -21,7 +21,7 @@ use twilight_model::{
use crate::{ use crate::{
constants::DEFAULT_PREFIX, constants::DEFAULT_PREFIX,
groups::calls::DisplayProfile, groups::calls::{DisplayProfile, masks::get_random_mask},
utils::{get_random_u64, i64_to_u64, u64_to_i64}, utils::{get_random_u64, i64_to_u64, u64_to_i64},
}; };
@@ -47,6 +47,9 @@ pub enum DatabaseError {
#[error("The resulting row could not be deserialized into an object")] #[error("The resulting row could not be deserialized into an object")]
DeserializationFailed, DeserializationFailed,
#[error("An unknown error occurred")]
Unknown(&'static str),
} }
trait Object<'a> { trait Object<'a> {
@@ -1744,3 +1747,131 @@ impl UserIncident {
incident.insert(session).await incident.insert(session).await
} }
} }
#[derive(Debug, Clone)]
pub struct Participant {
pub call_id: u64,
pub user_id: u64,
pub channel_id: u64,
pub user_name: String,
pub user_avatar_url: String,
pub user_mask: String,
pub is_masked: bool,
}
impl<'a> Object<'a> for Participant {
type Row = (i64, i64, i64, String, String, String, bool);
fn from_row(row: Self::Row) -> Self {
Self {
call_id: i64_to_u64(row.0),
user_id: i64_to_u64(row.1),
channel_id: i64_to_u64(row.2),
user_name: row.3,
user_avatar_url: row.4,
user_mask: row.5,
is_masked: row.6,
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.call_id),
u64_to_i64(value.user_id),
u64_to_i64(value.channel_id),
value.user_name,
value.user_avatar_url,
value.user_mask,
value.is_masked,
)
}
}
impl Participant {
pub async fn get_for_call(call_id: u64, session: &Session) -> Result<Vec<Self>, DatabaseError> {
let call_id = u64_to_i64(call_id);
let rows = session
.query_unpaged(
r#"
SELECT * FROM payphone.participants
WHERE call_id = ?;
"#,
(call_id,),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(rows
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?
.rows()
.map_err(|_| DatabaseError::ResultNotRows)?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DatabaseError::DeserializationFailed)?
.into_iter()
.map(Self::from_row)
.collect::<Vec<_>>())
}
pub async fn create(
user_id: u64,
channel_id: u64,
call_id: u64,
profile: DisplayProfile,
is_masked: bool,
session: &Session,
) -> Result<(), DatabaseError> {
let masks_in_use = Self::get_for_call(call_id, session)
.await?
.iter()
.filter_map(|p| {
if p.channel_id == channel_id {
Some(p.user_mask.clone())
} else {
None
}
})
.collect::<Vec<_>>();
let (mask, _) = get_random_mask(&masks_in_use).ok_or(DatabaseError::Unknown(
"No remaining masks available for the channel",
))?;
let participant = Participant {
call_id,
user_id,
channel_id,
user_name: profile.name,
user_avatar_url: profile.avatar_url,
user_mask: mask,
is_masked,
};
participant.insert(session).await
}
pub async fn insert(&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
) IF NOT EXISTS VALUES (?, ?, ?, ?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
}
+17 -6
View File
@@ -14,7 +14,7 @@ use crate::{
}, },
groups::calls::{ groups::calls::{
get_display_profile, get_display_profile,
permissions::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,
get_string_call_hangup_this_party, get_string_call_no_answer, get_string_call_hangup_this_party, get_string_call_no_answer,
@@ -53,7 +53,10 @@ impl Command<State, Extras> for CallCommand {
} }
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> { fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
vec![Box::new(NotBannedPermission)] vec![
Box::new(NotBannedPermission),
Box::new(NonNewAccountPermission),
]
} }
async fn execute( async fn execute(
@@ -146,7 +149,10 @@ impl Command<State, Extras> for SkipCommand {
} }
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> { fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
vec![Box::new(NotBannedPermission)] vec![
Box::new(NotBannedPermission),
Box::new(NonNewAccountPermission),
]
} }
async fn execute( async fn execute(
@@ -254,7 +260,10 @@ impl Command<State, Extras> for HangupCommand {
} }
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> { fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
vec![Box::new(NotBannedPermission)] vec![
Box::new(NotBannedPermission),
Box::new(NonNewAccountPermission),
]
} }
async fn execute( async fn execute(
@@ -354,7 +363,10 @@ impl Command<State, Extras> for FriendRequestCommand {
} }
fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> { fn permissions(&self) -> Vec<Box<dyn Permission<State, Extras> + Send + Sync + 'static>> {
vec![Box::new(NotBannedPermission)] vec![
Box::new(NotBannedPermission),
Box::new(NonNewAccountPermission),
]
} }
async fn execute( async fn execute(
@@ -494,7 +506,6 @@ impl Command<State, Extras> for MaskCommand {
}; };
if let Some(call_id) = channel.call_id { if let Some(call_id) = channel.call_id {
} else { } else {
ctx ctx
.send() .send()
+27 -5
View File
@@ -2,12 +2,16 @@ use std::{error::Error, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
use time::{Duration, UtcDateTime};
use twilight_http::{api_error::ApiError, error::ErrorType}; use twilight_http::{api_error::ApiError, error::ErrorType};
use twilight_model::{ use twilight_model::{
gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate}, gateway::payload::incoming::{MessageCreate, MessageDelete, MessageUpdate},
id::Id, id::Id,
}; };
use twilight_util::builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource}; use twilight_util::{
builder::embed::{EmbedAuthorBuilder, EmbedBuilder, ImageSource},
snowflake::Snowflake,
};
use crate::{ use crate::{
constants::COLOR_WHITE, constants::COLOR_WHITE,
@@ -15,14 +19,18 @@ use crate::{
discord::sanitizing::SanitizeDiscordMarkdown, discord::sanitizing::SanitizeDiscordMarkdown,
extras::Extras, extras::Extras,
framework::{ framework::{
commands::extras::ExtrasGetter, events::traits::{ commands::{extras::ExtrasGetter, permissions::Permission},
events::traits::{
EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler, EventResult, OnMessageDeleteHandler, OnMessageEditHandler, OnMessageHandler,
}, interactions::components::ActionRow, runtime::Runtime },
interactions::components::ActionRow,
runtime::Runtime,
}, },
groups::calls::{ groups::calls::{
components::{CodeOfConductButton, NeedHelpButton}, components::{CodeOfConductButton, NeedHelpButton},
embeds, get_display_profile, embeds, get_display_profile,
moderation::{escape_message, Action, Rule}, moderation::{Action, Rule, escape_message},
permissions::NonNewAccountPermission,
}, },
state::State, state::State,
utils::{ellipse, i64_to_u64}, utils::{ellipse, i64_to_u64},
@@ -96,6 +104,12 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
return Ok(()); return Ok(());
} }
if UtcDateTime::from_unix_timestamp(event.author.id.timestamp() / 1000).unwrap()
> UtcDateTime::now() - Duration::days(30)
{
return Ok(());
}
let prefixes = rt let prefixes = rt
.get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get())) .get_prefixes(event.guild_id.map(|i| i.get()), Some(event.author.id.get()))
.await; .await;
@@ -239,7 +253,11 @@ impl OnMessageHandler<State, Extras> for CallMessageHandler {
embeds.push( embeds.push(
EmbedBuilder::new() EmbedBuilder::new()
.author( .author(
EmbedAuthorBuilder::new(format!("Replying to {}", replying_to.data.author_name)).icon_url( EmbedAuthorBuilder::new(format!(
"Replying to {}",
replying_to.data.author_name
))
.icon_url(
ImageSource::url(replying_to.data.author_avatar_url).unwrap(), ImageSource::url(replying_to.data.author_avatar_url).unwrap(),
), ),
) )
@@ -323,6 +341,10 @@ impl OnMessageEditHandler<State, Extras> for CallMessageEditHandler {
event: &MessageUpdate, event: &MessageUpdate,
rt: Arc<Runtime<State, Extras>>, rt: Arc<Runtime<State, Extras>>,
) -> EventResult { ) -> EventResult {
if event.edited_timestamp.is_none() {
return Ok(());
}
log::trace!( log::trace!(
"Edited message was '{}' sent by {} (bot? {:?})", "Edited message was '{}' sent by {} (bot? {:?})",
event.content, event.content,
@@ -1,7 +1,5 @@
scum scum
idiot idiot
stupid
dumb
moron moron
fool fool
stfu stfu
+7
View File
@@ -0,0 +1,7 @@
{
"dinosaur": 1278255435652010014,
"shleepy cat": 1278255475066146816,
"cozy cat": 1278255420124696663,
"coquette cat": 1278255408254816300,
"panic cat": 1278255451993145345
}
+52
View File
@@ -0,0 +1,52 @@
{
"ferret": 1278176917123760139,
"octopus": 1278176904410697880,
"cat": 1278176891290783824,
"monkey": 1278176881195094049,
"panda": 1278176868767629455,
"sheep": 1278176856910073946,
"horse": 1278176842116894760,
"bee": 1278176823846506577,
"wild boar": 1278176806498996235,
"duck": 1278176794591232102,
"chicken": 1278176781790216233,
"elephant": 1278176771413512213,
"cow": 1278176761565413483,
"deer": 1278176744691597312,
"turkey": 1278176730619576321,
"dog": 1278176719932751903,
"beaver": 1278176709530882091,
"camel": 1278176699921465394,
"koala": 1278176687493746709,
"hippo": 1278176675846295603,
"ostrich": 1278176661317095494,
"rhino": 1278176650340864000,
"squirrel": 1278176637627924570,
"chamaleon": 1278176625392877648,
"frog": 1278176614512857088,
"skunk": 1278176604509442079,
"walrus": 1278176554723311678,
"hedgehog": 1278176541750067252,
"pig": 1278176531415564332,
"hen": 1278176518643646485,
"turtle": 1278176507428343891,
"mouse": 1278175957047578696,
"rabbit": 1278175944007487489,
"goat": 1278175933915856938,
"lion": 1278175922964664382,
"wolf": 1278175902047535165,
"snake": 1278175869331968061,
"llama": 1278175850008674325,
"crab": 1278175835634925620,
"owl": 1278175822947029152,
"bat": 1278175813572886550,
"zebra": 1278175799962505216,
"fox": 1278175788818104474,
"gorilla": 1278175775891259456,
"sloth": 1278175765661356175,
"puffer fish": 1278175753497743360,
"bear": 1278175741569142867,
"giraffe": 1278175732601983006,
"penguin": 1278175721956573236,
"parrot": 1278175706798489642
}
+32
View File
@@ -0,0 +1,32 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use rand::{SeedableRng, seq::IndexedRandom};
use rand_chacha::ChaCha20Rng;
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"))
} else {
("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"));
assert_ne!(masks.len(), 0, "No masks found in masks/{file_name}");
masks
});
pub fn get_random_mask(not_in: &[String]) -> Option<(String, u64)> {
let mask_names = MASKS_COMMON
.iter()
.filter(|(name, _)| !not_in.contains(name))
.map(|(name, value)| (name.clone(), *value))
.collect::<Vec<_>>();
let mut rng = ChaCha20Rng::from_os_rng();
mask_names.choose(&mut rng).cloned()
}
+7
View File
@@ -0,0 +1,7 @@
{
"dinosaur": 1278254725833297965,
"shleepy cat": 1278254779767984181,
"cozy cat": 1278254704324771893,
"coquette cat": 1278254684200636428,
"panic cat": 1278254760700674098
}
+52
View File
@@ -0,0 +1,52 @@
{
"ferret": 1278173356423909448,
"octopus": 1278173331421397072,
"cat": 1278173317034934282,
"monkey": 1278173297456185344,
"panda": 1278173272390762538,
"sheep": 1278173251914432563,
"horse": 1278173237582237708,
"bee": 1278173216652787826,
"wild boar": 1278173196658413649,
"duck": 1278173181483679918,
"chicken": 1278173167218589726,
"elephant": 1278173150063886346,
"cow": 1278173125024153733,
"deer": 1278173110105014406,
"turkey": 1278173091796877363,
"dog": 1278173070846201856,
"beaver": 1278173051485425705,
"camel": 1278173035018584086,
"koala": 1278173021345021952,
"hippo": 1278173007419932804,
"ostrich": 1278172994669248523,
"rhino": 1278172980848885832,
"squirrel": 1278172965590274088,
"chamaleon": 1278172953305022496,
"frog": 1278172937043710094,
"skunk": 1278172923495976981,
"walrus": 1278172912368619603,
"hedgehog": 1278172899911405678,
"pig": 1278172885239861338,
"hen": 1278172871302184994,
"turtle": 1278172858199314455,
"mouse": 1278172846442414081,
"rabbit": 1278172831921999945,
"goat": 1278172815421472808,
"lion": 1278172803534684235,
"wolf": 1278172791052570687,
"snake": 1278172776259260547,
"llama": 1278172764913799168,
"crab": 1278172752322232330,
"owl": 1278172738309193749,
"bat": 1278172726426865724,
"zebra": 1278172715114565753,
"fox": 1278172702955274292,
"gorilla": 1278172687797194824,
"sloth": 1278172676837343242,
"puffer fish": 1278172665093558294,
"bear": 1278172653076746301,
"giraffe": 1278172640296702073,
"penguin": 1278172627629899808,
"parrot": 1278172600702337074
}
+5 -1
View File
@@ -9,6 +9,7 @@ pub mod commands;
pub(super) mod components; pub(super) mod components;
pub(super) mod embeds; pub(super) mod embeds;
pub mod events; pub mod events;
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;
@@ -34,7 +35,10 @@ fn make_name_valid(name: String) -> String {
fn get_display_profile(event: &MessageCreate) -> DisplayProfile { fn get_display_profile(event: &MessageCreate) -> DisplayProfile {
let member = event.member.clone(); let member = event.member.clone();
let name = if let Some(nick) = &member.clone().and_then(|member| member.nick.map(make_name_valid)) { let name = if let Some(nick) = &member
.clone()
.and_then(|member| member.nick.map(make_name_valid))
{
if webhook_username(nick).is_err() { if webhook_username(nick).is_err() {
"Invalid Nickname".to_string() "Invalid Nickname".to_string()
} else { } else {
+31
View File
@@ -1,4 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use time::{Duration, UtcDateTime};
use twilight_util::snowflake::{self, Snowflake};
use crate::{ use crate::{
database::models::UserIncident, database::models::UserIncident,
@@ -41,3 +43,32 @@ impl Permission<State, Extras> for NotBannedPermission {
Ok(()) Ok(())
} }
} }
#[derive(Debug)]
pub struct NonNewAccountPermission;
#[async_trait]
impl Permission<State, Extras> for NonNewAccountPermission {
async fn check(&self, ctx: &CommandContext<State, Extras>) -> Result<bool, CommandError> {
Ok(
UtcDateTime::from_unix_timestamp(ctx.event.author.id.timestamp() / 1000).unwrap()
< UtcDateTime::now() - Duration::days(30),
)
}
async fn on_handle_denied(
&self,
ctx: &CommandContext<State, Extras>,
) -> Result<(), CommandError> {
ctx.send()
.content("❌ Your account must be at least 30 days old to use Payphone.")
.component(CodeOfConductButton, 1)
.component(NeedHelpButton, 1)
.await
.inspect_err(|e| {
log::error!("Failed to send message: {e}");
})?;
Ok(())
}
}
+3 -2
View File
@@ -137,7 +137,7 @@ use crate::{
groups::{ groups::{
about::{commands::HelpCommand, components::HelpGoBackButton}, about::{commands::HelpCommand, components::HelpGoBackButton},
calls::{ calls::{
commands::{CallCommand, FriendRequestCommand, HangupCommand, SkipCommand}, commands::{CallCommand, FriendRequestCommand, HangupCommand, MaskCommand, SkipCommand},
events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler}, events::{CallMessageDeleteHandler, CallMessageEditHandler, CallMessageHandler},
}, },
gateway::events::{ReadyHandler, ReconnectHandler}, gateway::events::{ReadyHandler, ReconnectHandler},
@@ -289,8 +289,9 @@ async fn start(
.command(SkipCommand) .command(SkipCommand)
.command(HangupCommand) .command(HangupCommand)
.command(FriendRequestCommand) .command(FriendRequestCommand)
.command(PrefixCommand) .command(MaskCommand)
.command(LeaderboardCommand) .command(LeaderboardCommand)
.command(PrefixCommand)
.command(ModeratorCommand) .command(ModeratorCommand)
.command(IncidentsCommand) .command(IncidentsCommand)
.command(StatsCommand) .command(StatsCommand)