Compare commits

...
20 Commits
Author SHA1 Message Date
Paul Makles 815c3fe99e chore: cut a new release (0.6.4) 2023-07-02 08:45:39 +01:00
Zomatree 54878e8e8d feat: add webhook permissions 2023-07-02 08:41:38 +01:00
TheBobBobs 6e4798f1d4 fix(bonfire): populate cache for calculating permissions (#259) 2023-07-02 08:40:15 +01:00
TheBobBobs 55bd6fb087 fix: incorrect owner rank 2023-07-02 08:35:49 +01:00
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
Paul Makles 26afbeed84 chore: extend discriminator block list 2023-06-11 10:49:51 +01:00
Paul Makles 9975de01bc chore: remove webhooks from production 2023-06-11 10:09:50 +01:00
Paul Makles ac525466b8 chore: cut a new release (0.6.0) 2023-06-11 10:04:15 +01:00
Paul Makles c7a04e4559 fix: conflict resolution in migration
fix: write new username and display name
chore: restrict discriminator search space
2023-06-11 09:53:41 +01:00
Paul Makles 5bbe30edbc feat(core/database): migrate to discriminators 2023-06-11 09:16:24 +01:00
Paul Makles 31c7dc0577 feat: add discriminator and display name fields 2023-06-09 16:34:18 +01:00
Paul Makles aba5c7d8af fix: shouldn't prefix dep: 2023-06-04 19:45:47 +01:00
Paul Makles 2f4ea4cabb refactor(core/models): transitively apply feature flags 2023-06-04 19:45:31 +01:00
Paul Makles 7e801a31bd fix: make sure feature flags are respected 2023-06-04 19:43:56 +01:00
54 changed files with 969 additions and 205 deletions
+3
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
Generated
+25 -8
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-rc.2"
version = "0.6.4"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2854,7 +2860,7 @@ dependencies = [
[[package]]
name = "revolt-database"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"async-recursion",
"async-std",
@@ -2867,7 +2873,9 @@ dependencies = [
"mongodb",
"nanoid",
"once_cell",
"rand 0.8.5",
"redis-kiss",
"regex",
"revolt-models",
"revolt-permissions",
"revolt-presence",
@@ -2878,11 +2886,12 @@ dependencies = [
"serde",
"serde_json",
"ulid 1.0.0",
"unicode-segmentation",
]
[[package]]
name = "revolt-delta"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"async-channel",
"async-std",
@@ -2922,7 +2931,7 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"revolt-permissions",
"revolt_optional_struct",
@@ -2933,7 +2942,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"async-std",
"async-trait",
@@ -2947,7 +2956,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"async-std",
"log",
@@ -2958,7 +2967,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"async-lock",
"async-recursion",
@@ -2971,6 +2980,7 @@ dependencies = [
"bson",
"dashmap",
"deadqueue",
"decancer",
"dotenv",
"futures",
"impl_ops",
@@ -2988,6 +2998,7 @@ dependencies = [
"redis-kiss",
"regex",
"reqwest",
"revolt-database",
"revolt-models",
"revolt-presence",
"revolt-result",
@@ -3009,7 +3020,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.6.0-rc.2"
version = "0.6.4"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",
@@ -4327,6 +4338,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-xid"
version = "0.0.4"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.6.0-rc.2"
version = "0.6.4"
license = "AGPL-3.0-or-later"
edition = "2021"
+10 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.6.0-rc.2"
version = "0.6.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
@@ -22,13 +22,14 @@ default = [ "mongodb", "async-std-runtime" ]
[dependencies]
# Core
revolt-result = { version = "0.6.0-rc.2", path = "../result" }
revolt-models = { version = "0.6.0-rc.2", path = "../models" }
revolt-presence = { version = "0.6.0-rc.2", path = "../presence" }
revolt-permissions = { version = "0.6.0-rc.2", path = "../permissions", features = [ "serde", "bson" ] }
revolt-result = { version = "0.6.4", path = "../result" }
revolt-models = { version = "0.6.4", path = "../models" }
revolt-presence = { version = "0.6.4", path = "../presence" }
revolt-permissions = { version = "0.6.4", path = "../permissions", features = [ "serde", "bson" ] }
# Utility
log = "0.4"
rand = "0.8.5"
ulid = "1.0.0"
nanoid = "0.4.0"
once_cell = "1.17"
@@ -46,6 +47,10 @@ redis-kiss = { version = "0.1.4" }
bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false }
# Database Migration
unicode-segmentation = "1.10.1"
regex = "1"
# Async Language Features
futures = "0.3.19"
async-trait = "0.1.51"
@@ -44,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None)
.await
.expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None)
.await
.expect("Failed to create migrations collection.");
@@ -72,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()
@@ -91,6 +99,18 @@ pub async fn create_database(db: &MongoDb) {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
@@ -193,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.");
}
@@ -1,14 +1,17 @@
use std::{ops::BitXor, time::Duration};
use std::{collections::HashSet, ops::BitXor, time::Duration};
use crate::{
mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
MongoDb,
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
};
use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Serialize, Deserialize)]
struct MigrationInfo {
@@ -16,7 +19,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 23;
pub const LATEST_REVISION: i32 = 26;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -768,8 +771,231 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update server members.");
}
if revision <= 23 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db()
.run_command(
doc! {
"dropIndexes": "users",
"index": "username"
},
None,
)
.await
.expect("Failed to drop existing username index.");
#[derive(Serialize, Deserialize)]
struct UserInformation {
#[serde(rename = "_id")]
id: String,
username: String,
}
let re_username = regex::Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap();
let users: Vec<UserInformation> = db
.col::<UserInformation>("users")
.find(doc! {}, None)
.await
.unwrap()
.map(|doc| doc.expect("id and username"))
.collect()
.await;
let search_space: Vec<String> = DISCRIMINATOR_SEARCH_SPACE.iter().cloned().collect();
let mut claimed: HashSet<String> = HashSet::new();
for i in 0..users.len() {
let info = &users[i];
let mut discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
if re_username.is_match(&info.username) {
while claimed.contains(&format!("{}#{}", info.username, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!(
"Re-rolled {} to {new_discriminator} from {discriminator}",
info.username
);
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", info.username, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - compliant",
i + 1,
users.len(),
info.username,
discriminator
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"discriminator": discriminator
}
},
None,
)
.await
.unwrap();
} else {
let mut sanitised = info
.username
.graphemes(true)
.filter(|s| re_username.is_match(s))
.collect::<String>();
while sanitised.len() < 2 {
sanitised += "_";
}
while claimed.contains(&format!("{}#{}", sanitised, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!("Re-rolled {sanitised} to {new_discriminator} from {discriminator}");
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", sanitised, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - sanitised: \"{}\"",
i + 1,
users.len(),
info.username,
discriminator,
sanitised
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"username": sanitised,
"discriminator": discriminator,
"display_name": &info.username
}
},
None,
)
.await
.unwrap();
}
}
}
if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
},
None,
)
.await
.expect("Failed to create username index.");
};
if revision <= 25 {
info!("Running migration [revision 25 / 11-06-2023]: Add permissions to webhooks.");
db.col::<Document>("webhooks")
.update_many(
doc! {},
doc! {
"$set": {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
}
},
None,
)
.await
.expect("Failed to update webhooks.");
}
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)
}
@@ -20,6 +20,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions of the webhook
pub permissions: u64,
/// The private token for the webhook
pub token: Option<String>,
},
+3
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
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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)
}
}
@@ -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>;
}
@@ -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))
}
}
@@ -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!()
}
}
@@ -1,5 +1,8 @@
use std::collections::HashSet;
use crate::{Database, File};
use once_cell::sync::Lazy;
use revolt_result::{Error, ErrorType, Result};
auto_derived_partial!(
@@ -10,6 +13,11 @@ auto_derived_partial!(
pub id: String,
/// Username
pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[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>,
@@ -195,3 +203,17 @@ impl User {
.await
}
}
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
] {
set.remove(&format!("{:0>4}", discrim));
}
set.into_iter().collect()
});
@@ -52,6 +52,7 @@ impl From<crate::Webhook> for Webhook {
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions
}
}
}
@@ -64,6 +65,7 @@ impl From<crate::PartialWebhook> for PartialWebhook {
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions
}
}
}
@@ -257,6 +259,8 @@ impl crate::User {
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.6.0-rc.2"
version = "0.6.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
@@ -9,8 +9,8 @@ description = "Revolt Backend: API Models"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
serde = [ "dep:serde" ]
schemas = [ "dep:schemars" ]
serde = [ "dep:serde", "revolt-permissions/serde" ]
schemas = [ "dep:schemars", "revolt-permissions/schemas" ]
validator = [ "dep:validator" ]
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
@@ -18,7 +18,7 @@ default = [ "serde", "partials" ]
[dependencies]
# Core
revolt-permissions = { version = "0.6.0-rc.2", path = "../permissions", features = [ "serde" ] }
revolt-permissions = { version = "0.6.4", path = "../permissions" }
# Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true }
@@ -16,6 +16,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64,
/// The private token for the webhook
pub token: Option<String>,
},
@@ -43,6 +46,9 @@ auto_derived!(
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>,
/// Webhook permissions
pub permissions: Option<u64>,
/// Fields to remove from webhook
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsWebhook>,
@@ -61,6 +67,9 @@ auto_derived!(
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64
}
/// Optional fields on webhook object
@@ -85,6 +94,7 @@ impl From<Webhook> for ResponseWebhook {
name: value.name,
avatar: value.avatar.map(|file| file.id),
channel_id: value.channel_id,
permissions: value.permissions
}
}
}
+5
View File
@@ -8,6 +8,11 @@ auto_derived!(
pub id: String,
/// Username
pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[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>,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.6.0-rc.2"
version = "0.6.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
@@ -135,3 +135,5 @@ pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
+ ChannelPermission::ChangeAvatar,
)
});
pub static DEFAULT_WEBHOOK_PERMISSIONS: Lazy<u64> = Lazy::new(|| ChannelPermission::SendMessage + ChannelPermission::SendEmbeds + ChannelPermission::Masquerade + ChannelPermission::React);
+3 -1
View File
@@ -32,7 +32,7 @@ pub struct DataPermissionsValue {
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[serde(untagged)]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum DataPermissionPoly {
Value {
/// Permission values to set for members in a `Group`
@@ -88,6 +88,8 @@ impl From<OverrideField> for Override {
#[cfg(feature = "bson")]
use bson::Bson;
#[cfg(feature = "bson")]
impl From<OverrideField> for Bson {
fn from(v: OverrideField) -> Self {
Self::Document(bson::to_document(&v).unwrap())
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.6.0-rc.2"
version = "0.6.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.6.0-rc.2"
version = "0.6.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.6.0-rc.2"
version = "0.6.4"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
+3 -5
View File
@@ -38,14 +38,12 @@ pub async fn create_bot(db: &Db, user: User, info: Json<DataCreateBot>) -> Resul
return Err(Error::ReachedMaximumBots);
}
if db.is_username_taken(&info.name).await? {
return Err(Error::UsernameTaken);
}
let id = Ulid::new().to_string();
let username = User::validate_username(info.name)?;
let bot_user = User {
id: id.clone(),
username: info.name.trim().to_string(),
discriminator: User::find_discriminator(db, &username, None).await?,
username,
bot: Some(BotInformation {
owner: user.id.clone(),
}),
-4
View File
@@ -58,10 +58,6 @@ pub async fn edit_bot(
}
if let Some(name) = data.name {
if db.is_username_taken(&name).await? {
return Err(Error::UsernameTaken);
}
let mut user = db.fetch_user(&bot.id).await?;
user.update_username(db, name).await?;
}
@@ -2,6 +2,7 @@ use revolt_database::{Database, Webhook};
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Permission, Ref, Result,
DEFAULT_WEBHOOK_PERMISSIONS,
};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
@@ -60,6 +61,7 @@ pub async fn req(
name: data.name,
avatar,
channel_id: channel.id().to_string(),
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
token: Some(nanoid::nanoid!(64)),
};
+45 -24
View File
@@ -1,3 +1,4 @@
use revolt_quark::variables::delta::IS_STAGING;
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
pub use rocket::http::Status;
pub use rocket::response::Redirect;
@@ -20,26 +21,48 @@ mod webhooks;
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
let settings = OpenApiSettings::default();
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root, root::ping],
"/admin" => admin::routes(),
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
"/webhooks" => webhooks::routes()
};
if *IS_STAGING {
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root, root::ping],
"/admin" => admin::routes(),
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
"/webhooks" => webhooks::routes()
};
} else {
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root, root::ping],
"/admin" => admin::routes(),
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/safety" => safety::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes()
};
}
rocket
}
@@ -298,11 +321,9 @@ fn custom_openapi_spec() -> OpenApi {
},
Tag {
name: "Webhooks".to_owned(),
description: Some(
"Send messages from 3rd party services".to_owned(),
),
description: Some("Send messages from 3rd party services".to_owned()),
..Default::default()
}
},
],
..Default::default()
}
+2 -1
View File
@@ -34,9 +34,10 @@ pub async fn req(
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let username = User::validate_username(db, data.username).await?;
let username = User::validate_username(data.username)?;
let user = User {
id: session.user_id,
discriminator: User::find_discriminator(db, &username, None).await?,
username,
..Default::default()
};
@@ -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,
}
}
@@ -13,7 +13,7 @@ pub async fn req(db: &Db, user: User, target: Ref, role_id: String) -> Result<Em
.throw_permission(db, Permission::ManageRole)
.await?;
let member_rank = permissions.get_member_rank().unwrap_or(0);
let member_rank = permissions.get_member_rank().unwrap_or(i64::MIN);
if let Some(role) = server.roles.remove(&role_id) {
if role.rank <= member_rank {
+8 -1
View File
@@ -8,6 +8,8 @@ use rocket::State;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::util::regex::RE_DISPLAY_NAME;
/// # Profile Data
#[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)]
pub struct UserProfileData {
@@ -24,6 +26,9 @@ pub struct UserProfileData {
/// # User Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditUser {
/// New display name
#[validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")]
display_name: Option<String>,
/// Attachment Id for avatar
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
@@ -84,7 +89,8 @@ pub async fn req(
}
// Exit out early if nothing is changed
if data.status.is_none()
if data.display_name.is_none()
&& data.status.is_none()
&& data.profile.is_none()
&& data.avatar.is_none()
&& data.badges.is_none()
@@ -116,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()
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
/// # User Lookup Information
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct DataSendFriendRequest {
/// Username and discriminator combo separated by #
username: String,
}
@@ -21,12 +22,16 @@ pub async fn req(
user: User,
data: Json<DataSendFriendRequest>,
) -> Result<Json<User>> {
let mut target = db.fetch_user_by_username(&data.username).await?;
if let Some((username, discriminator)) = data.username.split_once('#') {
let mut target = db.fetch_user_by_username(username, discriminator).await?;
if user.bot.is_some() || target.bot.is_some() {
return Err(Error::IsBot);
if user.bot.is_some() || target.bot.is_some() {
return Err(Error::IsBot);
}
user.add_friend(db, &mut target).await?;
Ok(Json(target.with_auto_perspective(db, &user).await))
} else {
Err(Error::InvalidProperty)
}
user.add_friend(db, &mut target).await?;
Ok(Json(target.with_auto_perspective(db, &user).await))
}
@@ -35,11 +35,13 @@ pub async fn webhook_edit(
let DataEditWebhook {
name,
avatar,
permissions,
remove,
} = data;
let mut partial = PartialWebhook {
name,
permissions,
..Default::default()
};
@@ -33,11 +33,13 @@ pub async fn webhook_edit_token(
let DataEditWebhook {
name,
avatar,
remove,
permissions,
remove
} = data;
let mut partial = PartialWebhook {
name,
permissions,
..Default::default()
};
@@ -29,8 +29,7 @@ pub async fn webhook_execute(
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
webhook.assert_token(&token).map_err(Error::from_core)?;
// TODO: webhooks can currently always send masquerades, files, embeds, reactions (interactions)
// TODO: they can also mention anyone
data.validate_webhook_permissions(webhook.permissions)?;
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
let message = channel
+7 -2
View File
@@ -1,12 +1,17 @@
use once_cell::sync::Lazy;
use regex::Regex;
/// Regex for valid display names
///
/// Block zero width space
/// Block newline and carriage return
pub static RE_DISPLAY_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap());
/// Regex for valid usernames
///
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω@#:\n\r\[\]]+$").unwrap());
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
/// Regex for valid emoji names
///
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-quark"
version = "0.6.0-rc.2"
version = "0.6.4"
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" }
+11
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()),
),
}
}
}
+19 -10
View File
@@ -23,8 +23,7 @@ impl Cache {
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
match &channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self.members.values().find(|x| &x.id.server == server);
let member = self.members.get(server);
let server = self.servers.get(server);
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
@@ -107,9 +106,15 @@ impl State {
// Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?;
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
// Collect channel ids from servers.
let mut channel_ids = vec![];
@@ -164,17 +169,11 @@ impl State {
self.cache
.users
.insert(self.cache.user_id.clone(), user.clone());
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache.channels = channels
.iter()
.cloned()
.map(|x| (x.id().to_string(), x))
.collect();
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
// Make all users appear from our perspective.
let mut users: Vec<User> = users
@@ -353,7 +352,7 @@ impl State {
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
self.cache.can_view_channel(db, channel).await
} else {
true
false
};
if let Some(channel) = self.cache.channels.get_mut(id) {
@@ -364,6 +363,12 @@ impl State {
channel.apply_options(data.clone());
}
if !self.cache.channels.contains_key(id) {
if let Ok(channel) = db.fetch_channel(id).await {
self.cache.channels.insert(id.clone(), channel);
}
}
if let Some(channel) = self.cache.channels.get(id) {
let can_view = self.cache.can_view_channel(db, channel).await;
if could_view != can_view {
@@ -398,7 +403,9 @@ impl State {
channels,
} => {
self.insert_subscription(id.clone());
self.cache.servers.insert(id.to_string(), server.clone());
self.cache.servers.insert(id.clone(), server.clone());
let member = Member::new(id.clone(), self.cache.user_id.clone());
self.cache.members.insert(id.clone(), member);
for channel in channels {
self.cache
@@ -436,6 +443,7 @@ impl State {
self.cache.channels.remove(channel);
}
}
self.cache.members.remove(id);
}
}
EventV1::ServerDelete { id } => {
@@ -447,6 +455,7 @@ impl State {
self.cache.channels.remove(channel);
}
}
self.cache.members.remove(id);
}
EventV1::ServerMemberUpdate { id, data, clear } => {
if id.user == self.cache.user_id {
@@ -3,22 +3,10 @@ use crate::{AbstractServerMember, Result};
use super::super::DummyDb;
use iso8601_timestamp::Timestamp;
#[async_trait]
impl AbstractServerMember for DummyDb {
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
Ok(Member {
id: MemberCompositeKey {
server: server.into(),
user: user.into(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
})
Ok(Member::new(server.into(), user.into()))
}
async fn insert_member(&self, member: &Member) -> Result<()> {
+4 -3
View File
@@ -9,11 +9,12 @@ impl AbstractUser for DummyDb {
Ok(User {
id: id.into(),
username: "username".into(),
discriminator: "0000".into(),
..Default::default()
})
}
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
async fn fetch_user_by_username(&self, username: &str, _discriminator: &str) -> Result<User> {
self.fetch_user(username).await
}
@@ -45,8 +46,8 @@ impl AbstractUser for DummyDb {
Ok(vec![self.fetch_user("id").await.unwrap()])
}
async fn is_username_taken(&self, _username: &str) -> Result<bool> {
Ok(false)
async fn fetch_discriminators_in_use(&self, _username: &str) -> Result<Vec<String>> {
Ok(vec![])
}
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
@@ -10,7 +10,7 @@ use crate::{
models::{
message::{
AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
SystemMessage,
SystemMessage, DataMessageSend,
},
Channel, Emoji, Message, User,
},
@@ -451,3 +451,39 @@ impl Interactions {
!self.restrict_reactions && self.reactions.is_none()
}
}
fn throw_permission(permissions: u64, permission: Permission) -> Result<()> {
if (permission as u64) & permissions == (permission as u64) {
Ok(())
} else {
Err(Error::MissingPermission { permission })
}
}
impl DataMessageSend {
pub fn validate_webhook_permissions(
&self,
permissions: u64,
) -> Result<()> {
throw_permission(permissions, Permission::SendMessage)?;
if self.attachments.as_ref().map_or(false, |v| !v.is_empty()) {
throw_permission(permissions, Permission::UploadFiles)?;
};
if self.embeds.as_ref().map_or(false, |v| !v.is_empty()) {
throw_permission(permissions, Permission::SendEmbeds)?;
};
if self.masquerade.is_some() {
throw_permission(permissions, Permission::Masquerade)?;
};
if self.interactions.is_some() {
throw_permission(permissions, Permission::React)?;
};
Ok(())
}
}
@@ -1,6 +1,5 @@
use std::collections::HashSet;
use iso8601_timestamp::Timestamp;
use ulid::Ulid;
use crate::{
@@ -186,18 +185,7 @@ impl Server {
return Err(Error::Banned);
}
let member = Member {
id: MemberCompositeKey {
server: self.id.clone(),
user: user.id.clone(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
let member = Member::new(self.id.clone(), user.id.clone());
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
@@ -3,13 +3,27 @@ use iso8601_timestamp::Timestamp;
use crate::{
events::client::EventV1,
models::{
server_member::{FieldsMember, PartialMember},
server_member::{FieldsMember, MemberCompositeKey, PartialMember},
Member, Server,
},
Database, Result,
};
impl Member {
pub fn new(server_id: String, user_id: String) -> Self {
Self {
id: MemberCompositeKey {
server: server_id,
user: user_id,
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
}
}
/// Update member data
pub async fn update<'a>(
&mut self,
+109 -21
View File
@@ -8,8 +8,13 @@ use crate::{perms, Database, Error, Result};
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 });
@@ -65,6 +70,7 @@ impl User {
x.background = None;
}
}
FieldsUser::DisplayName => self.display_name = None,
}
}
@@ -169,18 +175,15 @@ impl User {
}
/// Sanitise and validate a username can be used
pub async fn validate_username(db: &Database, username: String) -> Result<String> {
// Trim surrounding spaces
let username = username.trim().to_string();
// Make sure username is still at least 3 characters
if username.len() < 2 {
return Err(Error::InvalidUsername);
}
pub fn validate_username(username: String) -> Result<String> {
// 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"];
@@ -199,25 +202,96 @@ impl User {
}
}
// Make sure the username isn't taken
if db.is_username_taken(&username).await? {
Ok(username)
}
// Find a free discriminator for a given username
pub async fn find_discriminator(
db: &Database,
username: &str,
preferred: Option<(String, String)>,
) -> Result<String> {
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE_QUARK;
let used_discriminators: HashSet<String> = db
.fetch_discriminators_in_use(username)
.await?
.into_iter()
.collect();
let available_discriminators: Vec<&String> =
search_space.difference(&used_discriminators).collect();
if available_discriminators.is_empty() {
return Err(Error::UsernameTaken);
}
Ok(username)
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)?;
}
}
let mut rng = rand::thread_rng();
Ok(available_discriminators
.choose(&mut rng)
.expect("we can assert this has an element")
.to_string())
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
self.update(
db,
PartialUser {
username: Some(User::validate_username(db, username).await?),
..Default::default()
},
vec![],
)
.await
let username = User::validate_username(username)?;
if self.username.to_lowercase() == username.to_lowercase() {
self.update(
db,
PartialUser {
username: Some(username),
..Default::default()
},
vec![],
)
.await
} else {
self.update(
db,
PartialUser {
discriminator: Some(
User::find_discriminator(
db,
&username,
Some((self.discriminator.to_string(), self.id.clone())),
)
.await?,
),
username: Some(username),
..Default::default()
},
vec![],
)
.await
}
}
/// Apply a certain relationship between two users
@@ -407,3 +481,17 @@ impl User {
}
}
}
pub static DISCRIMINATOR_SEARCH_SPACE_QUARK: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
] {
set.remove(&format!("{:0>4}", discrim));
}
set.into_iter().collect()
});
+42 -17
View File
@@ -9,14 +9,16 @@ use crate::{AbstractUser, Error, Result};
use super::super::MongoDb;
static FIND_USERNAME_OPTIONS: Lazy<FindOneOptions> = Lazy::new(|| FindOneOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build()
)
.build());
static FIND_USERNAME_OPTIONS: Lazy<FindOneOptions> = Lazy::new(|| {
FindOneOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.build()
});
static COL: &str = "users";
@@ -26,11 +28,12 @@ impl AbstractUser for MongoDb {
self.find_one_by_id(COL, id).await
}
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
self.find_one_with_options(
COL,
doc! {
"username": username
"username": username,
"discriminator": discriminator
},
FIND_USERNAME_OPTIONS.clone(),
)
@@ -106,13 +109,34 @@ impl AbstractUser for MongoDb {
Ok(users)
}
async fn is_username_taken(&self, username: &str) -> Result<bool> {
// ! FIXME: move this up to generic
match self.fetch_user_by_username(username).await {
Ok(_) => Ok(true),
Err(Error::NotFound) => Ok(false),
Err(error) => Err(error),
}
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
Ok(self
.col::<Document>(COL)
.find(
doc! {
"username": username
},
FindOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.projection(doc! { "_id": 0, "discriminator": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("discriminator").ok().map(|x| x.to_string()))
.collect::<Vec<String>>())
}
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
@@ -313,6 +337,7 @@ impl IntoDocumentPath for FieldsUser {
FieldsUser::ProfileContent => "profile.content",
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
FieldsUser::DisplayName => "display_name",
})
}
}
+6
View File
@@ -128,6 +128,11 @@ pub struct User {
pub id: String,
/// Username
pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[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>,
@@ -172,6 +177,7 @@ pub enum FieldsUser {
StatusPresence,
ProfileContent,
ProfileBackground,
DisplayName,
}
/// Enumeration providing a hint to the type of user we are handling
@@ -110,6 +110,7 @@ pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| DEFAULT_PERMISSION_VIEW_
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = Permission::GrantAllSafe as u64;
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| DEFAULT_PERMISSION.add(Permission::ManageChannel + Permission::React));
pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| DEFAULT_PERMISSION.add(Permission::React + Permission::ChangeNickname + Permission::ChangeAvatar));
pub static DEFAULT_WEBHOOK_PERMISSIONS: Lazy<u64> = Lazy::new(|| Permission::SendMessage + Permission::SendEmbeds + Permission::Masquerade + Permission::React);
bitfield! {
#[derive(Default)]
+3 -3
View File
@@ -7,7 +7,7 @@ pub trait AbstractUser: Sync + Send {
async fn fetch_user(&self, id: &str) -> Result<User>;
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
@@ -29,8 +29,8 @@ pub trait AbstractUser: Sync + Send {
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
/// Check whether a username is already in use by another user
async fn is_username_taken(&self, username: &str) -> Result<bool>;
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>>;
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
+1 -1
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()
}
+2
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,
+127 -38
View File
@@ -1,56 +1,141 @@
use std::env;
use once_cell::sync::Lazy;
use std::env;
// Application Settings
pub static PUBLIC_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable."));
pub static APP_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable."));
pub static EXTERNAL_WS_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable."));
pub static PUBLIC_URL: Lazy<String> = Lazy::new(|| {
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.")
});
pub static APP_URL: Lazy<String> =
Lazy::new(|| env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable."));
pub static EXTERNAL_WS_URL: Lazy<String> = Lazy::new(|| {
env::var("REVOLT_EXTERNAL_WS_URL")
.expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.")
});
pub static AUTUMN_URL: Lazy<String> = Lazy::new(|| env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
pub static JANUARY_URL: Lazy<String> = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
pub static JANUARY_CONCURRENT_CONNECTIONS: Lazy<usize> = Lazy::new(|| env::var("JANUARY_CONCURRENT_CONNECTIONS").map_or(50, |v| v.parse().unwrap()));
pub static VOSO_URL: Lazy<String> = Lazy::new(|| env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
pub static VOSO_WS_HOST: Lazy<String> = Lazy::new(|| env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string()));
pub static VOSO_MANAGE_TOKEN: Lazy<String> = Lazy::new(|| env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string()));
pub static AUTUMN_URL: Lazy<String> = Lazy::new(|| {
env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string())
});
pub static JANUARY_URL: Lazy<String> = Lazy::new(|| {
env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string())
});
pub static JANUARY_CONCURRENT_CONNECTIONS: Lazy<usize> =
Lazy::new(|| env::var("JANUARY_CONCURRENT_CONNECTIONS").map_or(50, |v| v.parse().unwrap()));
pub static VOSO_URL: Lazy<String> =
Lazy::new(|| env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
pub static VOSO_WS_HOST: Lazy<String> =
Lazy::new(|| env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string()));
pub static VOSO_MANAGE_TOKEN: Lazy<String> =
Lazy::new(|| env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string()));
pub static HCAPTCHA_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string()));
pub static HCAPTCHA_SITEKEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string()));
pub static VAPID_PRIVATE_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable."));
pub static VAPID_PUBLIC_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable."));
pub static AUTHIFIER_SHIELD_KEY: Lazy<Option<String>> = Lazy::new(|| env::var("REVOLT_AUTHIFIER_SHIELD_KEY").ok());
pub static HCAPTCHA_KEY: Lazy<String> = Lazy::new(|| {
env::var("REVOLT_HCAPTCHA_KEY")
.unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string())
});
pub static HCAPTCHA_SITEKEY: Lazy<String> = Lazy::new(|| {
env::var("REVOLT_HCAPTCHA_SITEKEY")
.unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string())
});
pub static VAPID_PRIVATE_KEY: Lazy<String> = Lazy::new(|| {
env::var("REVOLT_VAPID_PRIVATE_KEY")
.expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.")
});
pub static VAPID_PUBLIC_KEY: Lazy<String> = Lazy::new(|| {
env::var("REVOLT_VAPID_PUBLIC_KEY")
.expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.")
});
pub static AUTHIFIER_SHIELD_KEY: Lazy<Option<String>> =
Lazy::new(|| env::var("REVOLT_AUTHIFIER_SHIELD_KEY").ok());
// Application Flags
pub static INVITE_ONLY: Lazy<bool> = Lazy::new(|| env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"));
pub static USE_EMAIL: Lazy<bool> = Lazy::new(|| env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
env::var("REVOLT_SMTP_HOST").is_ok()
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
&& env::var("REVOLT_SMTP_PASSWORD").is_ok()
&& env::var("REVOLT_SMTP_FROM").is_ok(),
|v| v == *"1"
));
pub static INVITE_ONLY: Lazy<bool> =
Lazy::new(|| env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"));
pub static USE_EMAIL: Lazy<bool> = Lazy::new(|| {
env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
env::var("REVOLT_SMTP_HOST").is_ok()
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
&& env::var("REVOLT_SMTP_PASSWORD").is_ok()
&& env::var("REVOLT_SMTP_FROM").is_ok(),
|v| v == *"1",
)
});
pub static USE_HCAPTCHA: Lazy<bool> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_KEY").is_ok());
pub static USE_AUTUMN: Lazy<bool> = Lazy::new(|| env::var("AUTUMN_PUBLIC_URL").is_ok());
pub static USE_JANUARY: Lazy<bool> = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").is_ok());
pub static USE_VOSO: Lazy<bool> = Lazy::new(|| env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok());
pub static USE_VOSO: Lazy<bool> =
Lazy::new(|| env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok());
// SMTP Settings
pub static SMTP_HOST: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string()));
pub static SMTP_USERNAME: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string()));
pub static SMTP_PASSWORD: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string()));
pub static SMTP_FROM: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string()));
pub static SMTP_HOST: Lazy<String> =
Lazy::new(|| env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string()));
pub static SMTP_USERNAME: Lazy<String> =
Lazy::new(|| env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string()));
pub static SMTP_PASSWORD: Lazy<String> =
Lazy::new(|| env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string()));
pub static SMTP_FROM: Lazy<String> =
Lazy::new(|| env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string()));
// Application Logic Settings
pub static MAX_GROUP_SIZE: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap());
pub static MAX_BOT_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "10".to_string()).parse().unwrap());
pub static MAX_EMBED_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
pub static MAX_SERVER_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap());
pub static MAX_CHANNEL_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_CHANNEL_COUNT").unwrap_or_else(|_| "200".to_string()).parse().unwrap());
pub static MAX_ROLE_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_ROLE_COUNT").unwrap_or_else(|_| "200".to_string()).parse().unwrap());
pub static MAX_EMOJI_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_EMOJI_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap());
pub static MAX_ATTACHMENT_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_ATTACHMENT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
pub static MAX_REPLY_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_REPLY_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
pub static MAX_GROUP_SIZE: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_GROUP_SIZE")
.unwrap_or_else(|_| "50".to_string())
.parse()
.unwrap()
});
pub static MAX_BOT_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_BOT_COUNT")
.unwrap_or_else(|_| "10".to_string())
.parse()
.unwrap()
});
pub static MAX_EMBED_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_EMBED_COUNT")
.unwrap_or_else(|_| "5".to_string())
.parse()
.unwrap()
});
pub static MAX_SERVER_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_SERVER_COUNT")
.unwrap_or_else(|_| "100".to_string())
.parse()
.unwrap()
});
pub static MAX_CHANNEL_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_CHANNEL_COUNT")
.unwrap_or_else(|_| "200".to_string())
.parse()
.unwrap()
});
pub static MAX_ROLE_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_ROLE_COUNT")
.unwrap_or_else(|_| "200".to_string())
.parse()
.unwrap()
});
pub static MAX_EMOJI_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_EMOJI_COUNT")
.unwrap_or_else(|_| "100".to_string())
.parse()
.unwrap()
});
pub static MAX_ATTACHMENT_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_ATTACHMENT_COUNT")
.unwrap_or_else(|_| "5".to_string())
.parse()
.unwrap()
});
pub static MAX_REPLY_COUNT: Lazy<usize> = Lazy::new(|| {
env::var("REVOLT_MAX_REPLY_COUNT")
.unwrap_or_else(|_| "5".to_string())
.parse()
.unwrap()
});
pub static EARLY_ADOPTER_BADGE: Lazy<i64> = Lazy::new(|| env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap());
pub static EARLY_ADOPTER_BADGE: Lazy<i64> = Lazy::new(|| {
env::var("REVOLT_EARLY_ADOPTER_BADGE")
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap()
});
pub fn preflight_checks() {
format!("url = {}", *APP_URL);
@@ -80,3 +165,7 @@ pub fn preflight_checks() {
warn!("No Captcha key specified! Remember to add hCaptcha key.");
}
}
// Production / staging configuration
pub static IS_STAGING: Lazy<bool> =
Lazy::new(|| env::var("REVOLT_IS_STAGING").map_or(false, |v| v == "1"));
+13 -9
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,