Compare commits
10
Commits
20230604-1
...
20230611-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8deec1f80a | ||
|
|
26afbeed84 | ||
|
|
9975de01bc | ||
|
|
ac525466b8 | ||
|
|
c7a04e4559 | ||
|
|
5bbe30edbc | ||
|
|
31c7dc0577 | ||
|
|
aba5c7d8af | ||
|
|
2f4ea4cabb | ||
|
|
7e801a31bd |
No files matched your search
Generated
+17
-8
@@ -2837,7 +2837,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@@ -2854,7 +2854,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-std",
|
"async-std",
|
||||||
@@ -2867,7 +2867,9 @@ dependencies = [
|
|||||||
"mongodb",
|
"mongodb",
|
||||||
"nanoid",
|
"nanoid",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rand 0.8.5",
|
||||||
"redis-kiss",
|
"redis-kiss",
|
||||||
|
"regex",
|
||||||
"revolt-models",
|
"revolt-models",
|
||||||
"revolt-permissions",
|
"revolt-permissions",
|
||||||
"revolt-presence",
|
"revolt-presence",
|
||||||
@@ -2878,11 +2880,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ulid 1.0.0",
|
"ulid 1.0.0",
|
||||||
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-delta"
|
name = "revolt-delta"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"async-std",
|
"async-std",
|
||||||
@@ -2922,7 +2925,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"revolt-permissions",
|
"revolt-permissions",
|
||||||
"revolt_optional_struct",
|
"revolt_optional_struct",
|
||||||
@@ -2933,7 +2936,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2947,7 +2950,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"log",
|
"log",
|
||||||
@@ -2958,7 +2961,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-quark"
|
name = "revolt-quark"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
@@ -3009,7 +3012,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"revolt_okapi",
|
"revolt_okapi",
|
||||||
"revolt_rocket_okapi",
|
"revolt_rocket_okapi",
|
||||||
@@ -4327,6 +4330,12 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
@@ -22,13 +22,14 @@ default = [ "mongodb", "async-std-runtime" ]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-result = { version = "0.6.0-rc.2", path = "../result" }
|
revolt-result = { version = "0.6.0", path = "../result" }
|
||||||
revolt-models = { version = "0.6.0-rc.2", path = "../models" }
|
revolt-models = { version = "0.6.0", path = "../models" }
|
||||||
revolt-presence = { version = "0.6.0-rc.2", path = "../presence" }
|
revolt-presence = { version = "0.6.0", path = "../presence" }
|
||||||
revolt-permissions = { version = "0.6.0-rc.2", path = "../permissions", features = [ "serde", "bson" ] }
|
revolt-permissions = { version = "0.6.0", path = "../permissions", features = [ "serde", "bson" ] }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
rand = "0.8.5"
|
||||||
ulid = "1.0.0"
|
ulid = "1.0.0"
|
||||||
nanoid = "0.4.0"
|
nanoid = "0.4.0"
|
||||||
once_cell = "1.17"
|
once_cell = "1.17"
|
||||||
@@ -46,6 +47,10 @@ redis-kiss = { version = "0.1.4" }
|
|||||||
bson = { optional = true, version = "2.1.0" }
|
bson = { optional = true, version = "2.1.0" }
|
||||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||||
|
|
||||||
|
# Database Migration
|
||||||
|
unicode-segmentation = "1.10.1"
|
||||||
|
regex = "1"
|
||||||
|
|
||||||
# Async Language Features
|
# Async Language Features
|
||||||
futures = "0.3.19"
|
futures = "0.3.19"
|
||||||
async-trait = "0.1.51"
|
async-trait = "0.1.51"
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create channel_unreads collection.");
|
.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)
|
db.create_collection("migrations", None)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create migrations collection.");
|
.expect("Failed to create migrations collection.");
|
||||||
@@ -91,6 +95,18 @@ pub async fn create_database(db: &MongoDb) {
|
|||||||
"username": 1_i32
|
"username": 1_i32
|
||||||
},
|
},
|
||||||
"name": "username",
|
"name": "username",
|
||||||
|
"unique": false,
|
||||||
|
"collation": {
|
||||||
|
"locale": "en",
|
||||||
|
"strength": 2_i32
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"username": 1_i32,
|
||||||
|
"discriminator": 1_i32
|
||||||
|
},
|
||||||
|
"name": "username_discriminator",
|
||||||
"unique": true,
|
"unique": true,
|
||||||
"collation": {
|
"collation": {
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use std::{ops::BitXor, time::Duration};
|
use std::{collections::HashSet, ops::BitXor, time::Duration};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
mongodb::{
|
mongodb::{
|
||||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||||
options::FindOptions,
|
options::FindOptions,
|
||||||
},
|
},
|
||||||
MongoDb,
|
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct MigrationInfo {
|
struct MigrationInfo {
|
||||||
@@ -16,7 +18,7 @@ struct MigrationInfo {
|
|||||||
revision: i32,
|
revision: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const LATEST_REVISION: i32 = 23;
|
pub const LATEST_REVISION: i32 = 25;
|
||||||
|
|
||||||
pub async fn migrate_database(db: &MongoDb) {
|
pub async fn migrate_database(db: &MongoDb) {
|
||||||
let migrations = db.col::<Document>("migrations");
|
let migrations = db.col::<Document>("migrations");
|
||||||
@@ -768,6 +770,183 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
|||||||
.expect("Failed to update server members.");
|
.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.");
|
||||||
|
}
|
||||||
|
|
||||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||||
|
|
||||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::{Database, File};
|
use crate::{Database, File};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use revolt_result::{Error, ErrorType, Result};
|
use revolt_result::{Error, ErrorType, Result};
|
||||||
|
|
||||||
auto_derived_partial!(
|
auto_derived_partial!(
|
||||||
@@ -10,6 +13,11 @@ auto_derived_partial!(
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
/// Username
|
/// Username
|
||||||
pub username: String,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
/// Avatar attachment
|
/// Avatar attachment
|
||||||
pub avatar: Option<File>,
|
pub avatar: Option<File>,
|
||||||
@@ -195,3 +203,17 @@ impl User {
|
|||||||
.await
|
.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()
|
||||||
|
});
|
||||||
@@ -257,6 +257,8 @@ impl crate::User {
|
|||||||
|
|
||||||
User {
|
User {
|
||||||
username: self.username,
|
username: self.username,
|
||||||
|
discriminator: self.discriminator,
|
||||||
|
display_name: self.display_name,
|
||||||
avatar: self.avatar.map(|file| file.into()),
|
avatar: self.avatar.map(|file| file.into()),
|
||||||
relations: vec![],
|
relations: vec![],
|
||||||
badges: self.badges.unwrap_or_default() as u32,
|
badges: self.badges.unwrap_or_default() as u32,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
serde = [ "dep:serde" ]
|
serde = [ "dep:serde", "revolt-permissions/serde" ]
|
||||||
schemas = [ "dep:schemars" ]
|
schemas = [ "dep:schemars", "revolt-permissions/schemas" ]
|
||||||
validator = [ "dep:validator" ]
|
validator = [ "dep:validator" ]
|
||||||
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
|
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ default = [ "serde", "partials" ]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-permissions = { version = "0.6.0-rc.2", path = "../permissions", features = [ "serde" ] }
|
revolt-permissions = { version = "0.6.0", path = "../permissions" }
|
||||||
|
|
||||||
# Serialisation
|
# Serialisation
|
||||||
revolt_optional_struct = { version = "0.2.0", optional = true }
|
revolt_optional_struct = { version = "0.2.0", optional = true }
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ auto_derived!(
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
/// Username
|
/// Username
|
||||||
pub username: String,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
/// Avatar attachment
|
/// Avatar attachment
|
||||||
pub avatar: Option<File>,
|
pub avatar: Option<File>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub struct DataPermissionsValue {
|
|||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||||
#[serde(untagged)]
|
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||||
pub enum DataPermissionPoly {
|
pub enum DataPermissionPoly {
|
||||||
Value {
|
Value {
|
||||||
/// Permission values to set for members in a `Group`
|
/// Permission values to set for members in a `Group`
|
||||||
@@ -88,6 +88,8 @@ impl From<OverrideField> for Override {
|
|||||||
|
|
||||||
#[cfg(feature = "bson")]
|
#[cfg(feature = "bson")]
|
||||||
use bson::Bson;
|
use bson::Bson;
|
||||||
|
|
||||||
|
#[cfg(feature = "bson")]
|
||||||
impl From<OverrideField> for Bson {
|
impl From<OverrideField> for Bson {
|
||||||
fn from(v: OverrideField) -> Self {
|
fn from(v: OverrideField) -> Self {
|
||||||
Self::Document(bson::to_document(&v).unwrap())
|
Self::Document(bson::to_document(&v).unwrap())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-delta"
|
name = "revolt-delta"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|||||||
@@ -38,14 +38,12 @@ pub async fn create_bot(db: &Db, user: User, info: Json<DataCreateBot>) -> Resul
|
|||||||
return Err(Error::ReachedMaximumBots);
|
return Err(Error::ReachedMaximumBots);
|
||||||
}
|
}
|
||||||
|
|
||||||
if db.is_username_taken(&info.name).await? {
|
|
||||||
return Err(Error::UsernameTaken);
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = Ulid::new().to_string();
|
let id = Ulid::new().to_string();
|
||||||
|
let username = User::validate_username(info.name)?;
|
||||||
let bot_user = User {
|
let bot_user = User {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
username: info.name.trim().to_string(),
|
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||||
|
username,
|
||||||
bot: Some(BotInformation {
|
bot: Some(BotInformation {
|
||||||
owner: user.id.clone(),
|
owner: user.id.clone(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ pub async fn edit_bot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = data.name {
|
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?;
|
let mut user = db.fetch_user(&bot.id).await?;
|
||||||
user.update_username(db, name).await?;
|
user.update_username(db, name).await?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use revolt_quark::variables::delta::IS_STAGING;
|
||||||
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
|
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
|
||||||
pub use rocket::http::Status;
|
pub use rocket::http::Status;
|
||||||
pub use rocket::response::Redirect;
|
pub use rocket::response::Redirect;
|
||||||
@@ -20,26 +21,48 @@ mod webhooks;
|
|||||||
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
||||||
let settings = OpenApiSettings::default();
|
let settings = OpenApiSettings::default();
|
||||||
|
|
||||||
mount_endpoints_and_merged_docs! {
|
if *IS_STAGING {
|
||||||
rocket, "/".to_owned(), settings,
|
mount_endpoints_and_merged_docs! {
|
||||||
"/" => (vec![], custom_openapi_spec()),
|
rocket, "/".to_owned(), settings,
|
||||||
"" => openapi_get_routes_spec![root::root, root::ping],
|
"/" => (vec![], custom_openapi_spec()),
|
||||||
"/admin" => admin::routes(),
|
"" => openapi_get_routes_spec![root::root, root::ping],
|
||||||
"/users" => users::routes(),
|
"/admin" => admin::routes(),
|
||||||
"/bots" => bots::routes(),
|
"/users" => users::routes(),
|
||||||
"/channels" => channels::routes(),
|
"/bots" => bots::routes(),
|
||||||
"/servers" => servers::routes(),
|
"/channels" => channels::routes(),
|
||||||
"/invites" => invites::routes(),
|
"/servers" => servers::routes(),
|
||||||
"/custom" => customisation::routes(),
|
"/invites" => invites::routes(),
|
||||||
"/safety" => safety::routes(),
|
"/custom" => customisation::routes(),
|
||||||
"/auth/account" => rocket_authifier::routes::account::routes(),
|
"/safety" => safety::routes(),
|
||||||
"/auth/session" => rocket_authifier::routes::session::routes(),
|
"/auth/account" => rocket_authifier::routes::account::routes(),
|
||||||
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
|
"/auth/session" => rocket_authifier::routes::session::routes(),
|
||||||
"/onboard" => onboard::routes(),
|
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
|
||||||
"/push" => push::routes(),
|
"/onboard" => onboard::routes(),
|
||||||
"/sync" => sync::routes(),
|
"/push" => push::routes(),
|
||||||
"/webhooks" => webhooks::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
|
rocket
|
||||||
}
|
}
|
||||||
@@ -298,11 +321,9 @@ fn custom_openapi_spec() -> OpenApi {
|
|||||||
},
|
},
|
||||||
Tag {
|
Tag {
|
||||||
name: "Webhooks".to_owned(),
|
name: "Webhooks".to_owned(),
|
||||||
description: Some(
|
description: Some("Send messages from 3rd party services".to_owned()),
|
||||||
"Send messages from 3rd party services".to_owned(),
|
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ pub async fn req(
|
|||||||
data.validate()
|
data.validate()
|
||||||
.map_err(|error| Error::FailedValidation { error })?;
|
.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 {
|
let user = User {
|
||||||
id: session.user_id,
|
id: session.user_id,
|
||||||
|
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||||
username,
|
username,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use rocket::State;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
|
use crate::util::regex::RE_DISPLAY_NAME;
|
||||||
|
|
||||||
/// # Profile Data
|
/// # Profile Data
|
||||||
#[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)]
|
#[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)]
|
||||||
pub struct UserProfileData {
|
pub struct UserProfileData {
|
||||||
@@ -24,6 +26,10 @@ pub struct UserProfileData {
|
|||||||
/// # User Data
|
/// # User Data
|
||||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct DataEditUser {
|
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
|
/// Attachment Id for avatar
|
||||||
#[validate(length(min = 1, max = 128))]
|
#[validate(length(min = 1, max = 128))]
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
@@ -84,7 +90,8 @@ pub async fn req(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exit out early if nothing is changed
|
// 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.profile.is_none()
|
||||||
&& data.avatar.is_none()
|
&& data.avatar.is_none()
|
||||||
&& data.badges.is_none()
|
&& data.badges.is_none()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// # User Lookup Information
|
/// # User Lookup Information
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct DataSendFriendRequest {
|
pub struct DataSendFriendRequest {
|
||||||
|
/// Username and discriminator combo separated by #
|
||||||
username: String,
|
username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,12 +22,16 @@ pub async fn req(
|
|||||||
user: User,
|
user: User,
|
||||||
data: Json<DataSendFriendRequest>,
|
data: Json<DataSendFriendRequest>,
|
||||||
) -> Result<Json<User>> {
|
) -> 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() {
|
if user.bot.is_some() || target.bot.is_some() {
|
||||||
return Err(Error::IsBot);
|
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))
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
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
|
/// Regex for valid usernames
|
||||||
///
|
///
|
||||||
/// Block zero width space
|
/// Block zero width space
|
||||||
/// Block lookalike characters
|
/// Block lookalike characters
|
||||||
pub static RE_USERNAME: Lazy<Regex> =
|
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
|
||||||
Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω@#:\n\r\[\]]+$").unwrap());
|
|
||||||
|
|
||||||
/// Regex for valid emoji names
|
/// Regex for valid emoji names
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-quark"
|
name = "revolt-quark"
|
||||||
version = "0.6.0-rc.2"
|
version = "0.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ impl AbstractUser for DummyDb {
|
|||||||
Ok(User {
|
Ok(User {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
username: "username".into(),
|
username: "username".into(),
|
||||||
|
discriminator: "0000".into(),
|
||||||
..Default::default()
|
..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
|
self.fetch_user(username).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +46,8 @@ impl AbstractUser for DummyDb {
|
|||||||
Ok(vec![self.fetch_user("id").await.unwrap()])
|
Ok(vec![self.fetch_user("id").await.unwrap()])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_username_taken(&self, _username: &str) -> Result<bool> {
|
async fn fetch_discriminators_in_use(&self, _username: &str) -> Result<Vec<String>> {
|
||||||
Ok(false)
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ use crate::{perms, Database, Error, Result};
|
|||||||
|
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
use impl_ops::impl_op_ex_commutative;
|
use impl_ops::impl_op_ex_commutative;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use revolt_presence::filter_online;
|
use revolt_presence::filter_online;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
|
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
|
||||||
@@ -169,15 +172,7 @@ impl User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sanitise and validate a username can be used
|
/// Sanitise and validate a username can be used
|
||||||
pub async fn validate_username(db: &Database, username: String) -> Result<String> {
|
pub fn validate_username(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the username for validation
|
// Copy the username for validation
|
||||||
let username_lowercase = username.to_lowercase();
|
let username_lowercase = username.to_lowercase();
|
||||||
|
|
||||||
@@ -199,25 +194,74 @@ impl User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the username isn't taken
|
Ok(username)
|
||||||
if db.is_username_taken(&username).await? {
|
}
|
||||||
|
|
||||||
|
// Find a free discriminator for a given username
|
||||||
|
pub async fn find_discriminator(
|
||||||
|
db: &Database,
|
||||||
|
username: &str,
|
||||||
|
preferred: Option<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);
|
return Err(Error::UsernameTaken);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(username)
|
if let Some(preferred) = preferred {
|
||||||
|
if available_discriminators.contains(&&preferred) {
|
||||||
|
return Ok(preferred);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
/// Update a user's username
|
||||||
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
|
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
|
||||||
self.update(
|
let username = User::validate_username(username)?;
|
||||||
db,
|
if self.username.to_lowercase() == username.to_lowercase() {
|
||||||
PartialUser {
|
self.update(
|
||||||
username: Some(User::validate_username(db, username).await?),
|
db,
|
||||||
..Default::default()
|
PartialUser {
|
||||||
},
|
username: Some(username),
|
||||||
vec![],
|
..Default::default()
|
||||||
)
|
},
|
||||||
.await
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
self.update(
|
||||||
|
db,
|
||||||
|
PartialUser {
|
||||||
|
discriminator: Some(
|
||||||
|
User::find_discriminator(
|
||||||
|
db,
|
||||||
|
&username,
|
||||||
|
Some(self.discriminator.to_string()),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
username: Some(username),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a certain relationship between two users
|
/// Apply a certain relationship between two users
|
||||||
@@ -407,3 +451,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()
|
||||||
|
});
|
||||||
@@ -9,14 +9,16 @@ use crate::{AbstractUser, Error, Result};
|
|||||||
|
|
||||||
use super::super::MongoDb;
|
use super::super::MongoDb;
|
||||||
|
|
||||||
static FIND_USERNAME_OPTIONS: Lazy<FindOneOptions> = Lazy::new(|| FindOneOptions::builder()
|
static FIND_USERNAME_OPTIONS: Lazy<FindOneOptions> = Lazy::new(|| {
|
||||||
.collation(
|
FindOneOptions::builder()
|
||||||
Collation::builder()
|
.collation(
|
||||||
.locale("en")
|
Collation::builder()
|
||||||
.strength(CollationStrength::Secondary)
|
.locale("en")
|
||||||
.build()
|
.strength(CollationStrength::Secondary)
|
||||||
)
|
.build(),
|
||||||
.build());
|
)
|
||||||
|
.build()
|
||||||
|
});
|
||||||
|
|
||||||
static COL: &str = "users";
|
static COL: &str = "users";
|
||||||
|
|
||||||
@@ -26,11 +28,12 @@ impl AbstractUser for MongoDb {
|
|||||||
self.find_one_by_id(COL, id).await
|
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(
|
self.find_one_with_options(
|
||||||
COL,
|
COL,
|
||||||
doc! {
|
doc! {
|
||||||
"username": username
|
"username": username,
|
||||||
|
"discriminator": discriminator
|
||||||
},
|
},
|
||||||
FIND_USERNAME_OPTIONS.clone(),
|
FIND_USERNAME_OPTIONS.clone(),
|
||||||
)
|
)
|
||||||
@@ -106,13 +109,34 @@ impl AbstractUser for MongoDb {
|
|||||||
Ok(users)
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_username_taken(&self, username: &str) -> Result<bool> {
|
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
|
||||||
// ! FIXME: move this up to generic
|
Ok(self
|
||||||
match self.fetch_user_by_username(username).await {
|
.col::<Document>(COL)
|
||||||
Ok(_) => Ok(true),
|
.find(
|
||||||
Err(Error::NotFound) => Ok(false),
|
doc! {
|
||||||
Err(error) => Err(error),
|
"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>> {
|
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ pub struct User {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
/// Username
|
/// Username
|
||||||
pub username: String,
|
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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
/// Avatar attachment
|
/// Avatar attachment
|
||||||
pub avatar: Option<File>,
|
pub avatar: Option<File>,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub trait AbstractUser: Sync + Send {
|
|||||||
async fn fetch_user(&self, id: &str) -> Result<User>;
|
async fn fetch_user(&self, id: &str) -> Result<User>;
|
||||||
|
|
||||||
/// Fetch a user from the database by their username
|
/// 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
|
/// Fetch a user from the database by their session token
|
||||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
|
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
|
/// Fetch multiple users by their ids
|
||||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
|
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
|
||||||
|
|
||||||
/// Check whether a username is already in use by another user
|
/// Fetch all discriminators in use for a username
|
||||||
async fn is_username_taken(&self, username: &str) -> Result<bool>;
|
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>>;
|
||||||
|
|
||||||
/// Fetch ids of users that both users are friends with
|
/// 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>>;
|
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
|
||||||
|
|||||||
@@ -1,56 +1,141 @@
|
|||||||
use std::env;
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
// Application Settings
|
// Application Settings
|
||||||
pub static PUBLIC_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable."));
|
pub static PUBLIC_URL: Lazy<String> = Lazy::new(|| {
|
||||||
pub static APP_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable."));
|
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_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 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 AUTUMN_URL: Lazy<String> = Lazy::new(|| {
|
||||||
pub static JANUARY_URL: Lazy<String> = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
|
env::var("AUTUMN_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 JANUARY_URL: Lazy<String> = Lazy::new(|| {
|
||||||
pub static VOSO_WS_HOST: Lazy<String> = Lazy::new(|| env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string()));
|
env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://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 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_KEY: Lazy<String> = Lazy::new(|| {
|
||||||
pub static HCAPTCHA_SITEKEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string()));
|
env::var("REVOLT_HCAPTCHA_KEY")
|
||||||
pub static VAPID_PRIVATE_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable."));
|
.unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string())
|
||||||
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_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
|
// Application Flags
|
||||||
pub static INVITE_ONLY: Lazy<bool> = Lazy::new(|| env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"));
|
pub static INVITE_ONLY: Lazy<bool> =
|
||||||
pub static USE_EMAIL: Lazy<bool> = Lazy::new(|| env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
|
Lazy::new(|| env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"));
|
||||||
env::var("REVOLT_SMTP_HOST").is_ok()
|
pub static USE_EMAIL: Lazy<bool> = Lazy::new(|| {
|
||||||
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
|
env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
|
||||||
&& env::var("REVOLT_SMTP_PASSWORD").is_ok()
|
env::var("REVOLT_SMTP_HOST").is_ok()
|
||||||
&& env::var("REVOLT_SMTP_FROM").is_ok(),
|
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
|
||||||
|v| v == *"1"
|
&& 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_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_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_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
|
// SMTP Settings
|
||||||
pub static SMTP_HOST: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string()));
|
pub static SMTP_HOST: Lazy<String> =
|
||||||
pub static SMTP_USERNAME: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string()));
|
Lazy::new(|| env::var("REVOLT_SMTP_HOST").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_USERNAME: Lazy<String> =
|
||||||
pub static SMTP_FROM: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_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
|
// 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_GROUP_SIZE: Lazy<usize> = Lazy::new(|| {
|
||||||
pub static MAX_BOT_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "10".to_string()).parse().unwrap());
|
env::var("REVOLT_MAX_GROUP_SIZE")
|
||||||
pub static MAX_EMBED_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
|
.unwrap_or_else(|_| "50".to_string())
|
||||||
pub static MAX_SERVER_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap());
|
.parse()
|
||||||
pub static MAX_CHANNEL_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_CHANNEL_COUNT").unwrap_or_else(|_| "200".to_string()).parse().unwrap());
|
.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_BOT_COUNT: Lazy<usize> = Lazy::new(|| {
|
||||||
pub static MAX_ATTACHMENT_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_ATTACHMENT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
|
env::var("REVOLT_MAX_BOT_COUNT")
|
||||||
pub static MAX_REPLY_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_REPLY_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
|
.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() {
|
pub fn preflight_checks() {
|
||||||
format!("url = {}", *APP_URL);
|
format!("url = {}", *APP_URL);
|
||||||
@@ -80,3 +165,7 @@ pub fn preflight_checks() {
|
|||||||
warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
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"));
|
||||||
Reference in new issue
Block a user