Compare commits

..

7 Commits

Author SHA1 Message Date
Paul Makles
49035f4817 feat: ratelimit user edit route and discriminator changes 2023-06-15 19:24:53 +01:00
Paul Makles
c0ebaa0bd3 chore: include discriminator in ban list response 2023-06-15 19:23:47 +01:00
Paul Makles
b98b244fc3 fix: enable staging for API spec generation 2023-06-15 15:21:00 +01:00
Paul Makles
c8d5128b0c chore: add additional sanitisation 2023-06-11 12:26:18 +01:00
Paul Makles
0578a05a05 feat: add remove "displayname" field 2023-06-11 12:05:08 +01:00
Paul Makles
9a412b3e08 fix: actually update the display name 2023-06-11 11:50:21 +01:00
Paul Makles
8deec1f80a fix: display name is optional 2023-06-11 10:56:03 +01:00
29 changed files with 281 additions and 40 deletions

View File

@@ -59,6 +59,9 @@ REVOLT_UNSAFE_NO_EMAIL=1
## Application Settings
##
# Whether to enable staging only features
REVOLT_IS_STAGING=1
# Whether to only allow users to sign up if they have an invite code
REVOLT_INVITE_ONLY=0

24
Cargo.lock generated
View File

@@ -806,6 +806,12 @@ dependencies = [
"uuid",
]
[[package]]
name = "decancer"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808127a7de612079ec37bfc1abc48ed77a6015a971a8bd7d4178d79147cbc839"
[[package]]
name = "derivative"
version = "2.2.0"
@@ -2837,7 +2843,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2854,7 +2860,7 @@ dependencies = [
[[package]]
name = "revolt-database"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"async-recursion",
"async-std",
@@ -2885,7 +2891,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"async-channel",
"async-std",
@@ -2925,7 +2931,7 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"revolt-permissions",
"revolt_optional_struct",
@@ -2936,7 +2942,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"async-std",
"async-trait",
@@ -2950,7 +2956,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"async-std",
"log",
@@ -2961,7 +2967,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"async-lock",
"async-recursion",
@@ -2974,6 +2980,7 @@ dependencies = [
"bson",
"dashmap",
"deadqueue",
"decancer",
"dotenv",
"futures",
"impl_ops",
@@ -2991,6 +2998,7 @@ dependencies = [
"redis-kiss",
"regex",
"reqwest",
"revolt-database",
"revolt-models",
"revolt-presence",
"revolt-result",
@@ -3012,7 +3020,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.6.0"
version = "0.6.3"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.6.0"
version = "0.6.3"
license = "AGPL-3.0-or-later"
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.6.0"
version = "0.6.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
@@ -22,10 +22,10 @@ default = [ "mongodb", "async-std-runtime" ]
[dependencies]
# Core
revolt-result = { version = "0.6.0", path = "../result" }
revolt-models = { version = "0.6.0", path = "../models" }
revolt-presence = { version = "0.6.0", path = "../presence" }
revolt-permissions = { version = "0.6.0", path = "../permissions", features = [ "serde", "bson" ] }
revolt-result = { version = "0.6.3", path = "../result" }
revolt-models = { version = "0.6.3", path = "../models" }
revolt-presence = { version = "0.6.3", path = "../presence" }
revolt-permissions = { version = "0.6.3", path = "../permissions", features = [ "serde", "bson" ] }
# Utility
log = "0.4"

View File

@@ -76,6 +76,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None)
.await
.expect("Failed to create ratelimit_events collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
@@ -209,5 +213,24 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to save migration info.");
db.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.await
.expect("Failed to create ratelimit_events index.");
info!("Created database.");
}

View File

@@ -18,7 +18,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 25;
pub const LATEST_REVISION: i32 = 26;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -947,8 +947,37 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create username index.");
}
if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db()
.create_collection("ratelimit_events", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.await
.expect("Failed to create ratelimit_events index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION
LATEST_REVISION.max(revision)
}

View File

@@ -3,6 +3,7 @@ mod bots;
mod channel_webhooks;
mod channels;
mod files;
mod ratelimit_events;
mod safety_strikes;
mod server_members;
mod servers;
@@ -14,6 +15,7 @@ pub use bots::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use files::*;
pub use ratelimit_events::*;
pub use safety_strikes::*;
pub use server_members::*;
pub use servers::*;
@@ -30,6 +32,7 @@ pub trait AbstractDatabase:
+ channels::AbstractChannels
+ channel_webhooks::AbstractWebhooks
+ files::AbstractAttachments
+ ratelimit_events::AbstractRatelimitEvents
+ safety_strikes::AbstractAccountStrikes
+ server_members::AbstractServerMembers
+ servers::AbstractServers

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,25 @@
use std::fmt;
auto_derived!(
/// Ratelimit Event
pub struct RatelimitEvent {
/// Id
#[serde(rename = "_id")]
pub id: String,
/// Relevant Object Id
pub target_id: String,
/// Type of event
pub event_type: RatelimitEventType,
}
/// Event type
pub enum RatelimitEventType {
DiscriminatorChange,
}
);
impl fmt::Display for RatelimitEventType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}

View File

@@ -0,0 +1,20 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractRatelimitEvents: Sync + Send {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>;
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool>;
}

View File

@@ -0,0 +1,40 @@
use std::time::{Duration, SystemTime};
use super::AbstractRatelimitEvents;
use crate::{MongoDb, RatelimitEvent, RatelimitEventType};
use revolt_result::Result;
use ulid::Ulid;
static COL: &str = "ratelimit_events";
#[async_trait]
impl AbstractRatelimitEvents for MongoDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
query!(self, insert_one, COL, &event).map(|_| ())
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool> {
self.col::<RatelimitEvent>(COL)
.count_documents(
doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
},
None,
)
.await
.map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL))
}
}

View File

@@ -0,0 +1,28 @@
use std::time::Duration;
use super::AbstractRatelimitEvents;
use crate::RatelimitEvent;
use crate::RatelimitEventType;
use crate::ReferenceDb;
use revolt_result::Result;
#[async_trait]
impl AbstractRatelimitEvents for ReferenceDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, _event: &RatelimitEvent) -> Result<()> {
// TODO: implement
unimplemented!()
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
_target_id: &str,
_event_type: RatelimitEventType,
_period: Duration,
_count: usize,
) -> Result<bool> {
// TODO: implement
unimplemented!()
}
}

View File

@@ -16,7 +16,8 @@ auto_derived_partial!(
/// Discriminator
pub discriminator: String,
/// Display name
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment
pub avatar: Option<File>,

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.6.0"
version = "0.6.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
@@ -18,7 +18,7 @@ default = [ "serde", "partials" ]
[dependencies]
# Core
revolt-permissions = { version = "0.6.0", path = "../permissions" }
revolt-permissions = { version = "0.6.3", path = "../permissions" }
# Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true }

View File

@@ -11,7 +11,8 @@ auto_derived!(
/// Discriminator
pub discriminator: String,
/// Display name
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment
pub avatar: Option<File>,

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.6.0"
version = "0.6.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.6.0"
version = "0.6.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.6.0"
version = "0.6.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.6.0"
version = "0.6.3"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"

View File

@@ -14,6 +14,8 @@ struct BannedUser {
pub id: String,
/// Username of the banned user
pub username: String,
/// Discriminator of the banned user
pub discriminator: String,
/// Avatar of the banned user
pub avatar: Option<File>,
}
@@ -32,6 +34,7 @@ impl From<User> for BannedUser {
BannedUser {
id: user.id,
username: user.username,
discriminator: user.discriminator,
avatar: user.avatar,
}
}

View File

@@ -27,7 +27,6 @@ pub struct UserProfileData {
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditUser {
/// New display name
#[serde(rename = "displayName")]
#[validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")]
display_name: Option<String>,
/// Attachment Id for avatar
@@ -123,6 +122,7 @@ pub async fn req(
}
let mut partial: PartialUser = PartialUser {
display_name: data.display_name,
badges: data.badges,
flags: data.flags,
..Default::default()

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-quark"
version = "0.6.0"
version = "0.6.3"
edition = "2021"
license = "AGPL-3.0-or-later"
@@ -64,6 +64,7 @@ nanoid = "0.4.0"
linkify = "0.8.1"
dotenv = "0.15.0"
indexmap = "1.9.1"
decancer = "1.6.2"
impl_ops = "0.1.1"
num_enum = "0.5.6"
reqwest = "0.11.10"
@@ -93,4 +94,5 @@ sentry = "0.25.0"
# Core
revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] }
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
revolt-database = { path = "../core/database" }
revolt-models = { path = "../core/models" }

View File

@@ -71,3 +71,14 @@ impl From<Database> for authifier::Database {
}
}
}
impl From<Database> for revolt_database::Database {
fn from(val: Database) -> Self {
match val {
Database::Dummy(_) => revolt_database::Database::Reference(Default::default()),
Database::MongoDb(MongoDb(client)) => revolt_database::Database::MongoDb(
revolt_database::MongoDb(client, "revolt".to_string()),
),
}
}
}

View File

@@ -10,9 +10,11 @@ use futures::try_join;
use impl_ops::impl_op_ex_commutative;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_database::RatelimitEventType;
use revolt_presence::filter_online;
use std::collections::HashSet;
use std::ops;
use std::time::Duration;
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
@@ -68,6 +70,7 @@ impl User {
x.background = None;
}
}
FieldsUser::DisplayName => self.display_name = None,
}
}
@@ -176,6 +179,11 @@ impl User {
// Copy the username for validation
let username_lowercase = username.to_lowercase();
// Block homoglyphs
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
return Err(Error::InvalidUsername);
}
// Ensure the username itself isn't blocked
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
@@ -201,7 +209,7 @@ impl User {
pub async fn find_discriminator(
db: &Database,
username: &str,
preferred: Option<String>,
preferred: Option<(String, String)>,
) -> Result<String> {
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE_QUARK;
let used_discriminators: HashSet<String> = db
@@ -217,9 +225,31 @@ impl User {
return Err(Error::UsernameTaken);
}
if let Some(preferred) = preferred {
if let Some((preferred, target_id)) = preferred {
if available_discriminators.contains(&&preferred) {
return Ok(preferred);
} else {
let rvdb: revolt_database::Database = db.clone().into();
if rvdb
.has_ratelimited(
&target_id,
RatelimitEventType::DiscriminatorChange,
Duration::from_secs(60 * 60 * 24),
1,
)
.await
.map_err(Error::from_core)?
{
return Err(Error::DiscriminatorChangeRatelimited);
}
rvdb.insert_ratelimit_event(&revolt_database::RatelimitEvent {
id: ulid::Ulid::new().to_string(),
target_id,
event_type: RatelimitEventType::DiscriminatorChange,
})
.await
.map_err(Error::from_core)?;
}
}
@@ -251,7 +281,7 @@ impl User {
User::find_discriminator(
db,
&username,
Some(self.discriminator.to_string()),
Some((self.discriminator.to_string(), self.id.clone())),
)
.await?,
),

View File

@@ -337,6 +337,7 @@ impl IntoDocumentPath for FieldsUser {
FieldsUser::ProfileContent => "profile.content",
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
FieldsUser::DisplayName => "display_name",
})
}
}

View File

@@ -131,7 +131,8 @@ pub struct User {
/// Discriminator
pub discriminator: String,
/// Display name
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment
pub avatar: Option<File>,
@@ -176,6 +177,7 @@ pub enum FieldsUser {
StatusPresence,
ProfileContent,
ProfileBackground,
DisplayName,
}
/// Enumeration providing a hint to the type of user we are handling

View File

@@ -8,6 +8,6 @@ pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_j
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
v.into_iter()
.filter(|(_k, v)| !v.is_null())
.map(|(k, v)| (prefix.to_owned() + &k, v))
.map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
.collect()
}

View File

@@ -31,6 +31,7 @@ pub enum Error {
// ? User related errors
UsernameTaken,
InvalidUsername,
DiscriminatorChangeRatelimited,
UnknownUser,
AlreadyFriends,
AlreadySentRequest,
@@ -165,6 +166,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::UnknownUser => Status::NotFound,
Error::InvalidUsername => Status::BadRequest,
Error::DiscriminatorChangeRatelimited => Status::TooManyRequests,
Error::UsernameTaken => Status::Conflict,
Error::AlreadyFriends => Status::Conflict,
Error::AlreadySentRequest => Status::Conflict,

View File

@@ -101,16 +101,19 @@ pub struct Ratelimiter {
fn resolve_bucket<'r>(request: &'r rocket::Request<'_>) -> (&'r str, Option<&'r str>) {
if let Some(segment) = request.routed_segment(0) {
let resource = request.routed_segment(1);
match (segment, resource) {
("users", _) => {
let method = request.method();
match (segment, resource, method) {
("users", target, Method::Patch) => ("user_edit", target),
("users", _, _) => {
if let Some("default_avatar") = request.routed_segment(2) {
return ("default_avatar", None);
}
("users", None)
}
("bots", _) => ("bots", None),
("channels", Some(id)) => {
("bots", _, _) => ("bots", None),
("channels", Some(id), _) => {
if request.method() == Method::Post {
if let Some("messages") = request.routed_segment(2) {
return ("messaging", Some(id));
@@ -119,17 +122,17 @@ fn resolve_bucket<'r>(request: &'r rocket::Request<'_>) -> (&'r str, Option<&'r
("channels", Some(id))
}
("servers", Some(id)) => ("servers", Some(id)),
("auth", _) => {
("servers", Some(id), _) => ("servers", Some(id)),
("auth", _, _) => {
if request.method() == Method::Delete {
("auth_delete", None)
} else {
("auth", None)
}
}
("swagger", _) => ("swagger", None),
("safety", Some("report")) => ("safety_report", Some("report")),
("safety", _) => ("safety", None),
("swagger", _, _) => ("swagger", None),
("safety", Some("report"), _) => ("safety_report", Some("report")),
("safety", _, _) => ("safety", None),
_ => ("any", None),
}
} else {
@@ -140,6 +143,7 @@ fn resolve_bucket<'r>(request: &'r rocket::Request<'_>) -> (&'r str, Option<&'r
/// Resolve per-bucket limits
fn resolve_bucket_limit(bucket: &str) -> u8 {
match bucket {
"user_edit" => 2,
"users" => 20,
"bots" => 10,
"messaging" => 10,