Compare commits

..
Author SHA1 Message Date
Paul Makles cb813e91f0 chore: cut new 0.6.9 release 2023-10-28 14:39:10 +01:00
Paul Makles a6bc9fcbb3 refactor: implement Server::create 2023-10-28 14:25:29 +01:00
Paul Makles cd0737911b refactor: use fixtures to populate db
refactor: method for creating server channels
2023-10-27 16:31:37 +01:00
Paul Makles 7705354d4d ci: use reference db for generating spec 2023-10-26 15:07:03 +01:00
Paul Makles 39ac8b2278 refactor: don't create unnecessary reference 2023-10-26 14:12:28 +01:00
arslee07 c5494aa1ca fix: respect Permission::SendEmbeds when editing a message 2023-10-26 14:12:28 +01:00
Paul Makles 1a96bf5a4b chore: remove redundant file [skip ci] 2023-10-26 14:05:01 +01:00
Paul Makles 37a1244751 test: ignore hello world test because it breaks cargo test? 2023-10-26 14:05:01 +01:00
Paul Makles bf39f18f8d fix: always generate new test database 2023-10-26 14:05:01 +01:00
Paul Makles 95be7dc87a ci: use nextest because cargo test sig sevs 2023-10-26 14:05:01 +01:00
Paul Makles 9be171c7b6 fix: don't copy the old test file 2023-10-26 14:05:01 +01:00
Paul Makles 786fbb8ae4 chore: disable broken tests 2023-10-26 14:05:01 +01:00
Paul Makles 6c0210e8a1 feat: always use test configuration during tests 2023-10-26 14:05:01 +01:00
Paul Makles 456bf7b42a fix: check server ownership for text channel
refactor: minor cleanup of code
2023-10-26 14:05:01 +01:00
Paul Makles 21ffea0f9f fix(core/database): store member during permission query 2023-10-26 14:05:01 +01:00
Paul Makles 96d90215d2 fix(core/models): validator feature flag not properly gatekept 2023-10-26 14:05:01 +01:00
Paul Makles 29dcf714f2 chore: start work on test for permission issue 2023-10-26 14:05:01 +01:00
Paul Makles cb97004d3f feat(core): provide user profile where appropriate 2023-10-16 09:33:36 +01:00
Paul Makles 0b6e5dd9db chore: add nix file to vscode config 2023-10-16 09:23:11 +01:00
Paul Makles 7132877201 feat(core): validation for files in reference db 2023-10-16 09:22:53 +01:00
Paul Makles 4d42fb74e7 fix(core): publish user settings event 2023-10-16 09:22:17 +01:00
Paul Makles 866ace3381 refactor: use config for max bot count 2023-10-16 09:21:48 +01:00
Paul Makles 0798e81862 feat(core): implement ratelimit events for reference db 2023-10-16 09:21:34 +01:00
Paul Makles 2fa5ac41ac refactor: tests for more group routes 2023-09-22 19:04:17 +01:00
Paul Makles 25ae1555a2 chore: add nextest and fmt to nix 2023-09-22 19:03:49 +01:00
Paul Makles 881888bd1c chore: add timeout for nextest runs 2023-09-22 19:03:29 +01:00
Paul Makles 41f20c2239 ci: use custom Revolt.toml for tests 2023-09-10 15:46:00 +01:00
Paul Makles 38f1381fea chore: switch to resolver=2 2023-09-10 15:19:23 +01:00
Paul Makles 6838b0b1f0 refactor: ensure in-memory database consistency
test: update authifier to allow for in-memory tests
ci: explicitly run in-memory and mongo tests
2023-09-10 14:49:27 +01:00
Paul Makles 055deab105 fix: allow bots to authenticate with new routes 2023-09-10 14:48:13 +01:00
Paul Makles 6d2073120d fix: carry over send embed permission when sending [skip ci] 2023-09-10 12:41:23 +01:00
67 changed files with 1487 additions and 578 deletions
+2
View File
@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
+20 -13
View File
@@ -20,6 +20,11 @@ jobs:
toolchain: stable toolchain: stable
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Install cargo-nextest
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-nextest
locked: true
- name: Run cargo build - name: Run cargo build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -30,25 +35,27 @@ jobs:
run: | run: |
docker-compose -f docker-compose.db.yml up -d docker-compose -f docker-compose.db.yml up -d
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Run cargo test (with MongoDB)
uses: actions-rs/cargo@v1
env:
MONGODB: mongodb://localhost
with:
command: test
- name: Copy .env.example - name: Copy .env.example
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: | run: |
cp .env.example .env cp .env.example .env
- name: Run cargo test
env:
TEST_DB: REFERENCE
run: |
cargo nextest run
- name: Run cargo test (with MongoDB)
env:
TEST_DB: MONGODB
MONGODB: mongodb://localhost
run: |
cargo nextest run
- name: Start API in background - name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master' if: github.event_name != 'pull_request' && github.ref_name == 'master'
env:
TEST_DB: REFERENCE
run: | run: |
cargo run --bin revolt-delta & cargo run --bin revolt-delta &
+2 -1
View File
@@ -1,5 +1,6 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy", "rust-analyzer.checkOnSave.command": "clippy",
"nixEnvSelector.suggestion": false "nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
} }
Generated
+14 -13
View File
@@ -347,9 +347,9 @@ dependencies = [
[[package]] [[package]]
name = "authifier" name = "authifier"
version = "1.0.7" version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84411393a4326726ead660991ce8047f1865fa89900fb6c18e1e6c513cf1f9b" checksum = "30269caf0aaf1e1b542b150030e9688bf41d50026e09a51efd9408f332636c9d"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3494,7 +3494,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
@@ -3512,7 +3512,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-config" name = "revolt-config"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-std", "async-std",
"cached", "cached",
@@ -3524,7 +3524,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-database" name = "revolt-database"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -3569,10 +3569,11 @@ dependencies = [
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-std", "async-std",
"authifier",
"bitfield", "bitfield",
"chrono", "chrono",
"dashmap", "dashmap",
@@ -3614,7 +3615,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-models" name = "revolt-models"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"iso8601-timestamp 0.2.11", "iso8601-timestamp 0.2.11",
@@ -3630,7 +3631,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3645,7 +3646,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-presence" name = "revolt-presence"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-std", "async-std",
"log", "log",
@@ -3656,7 +3657,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-quark" name = "revolt-quark"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -3709,7 +3710,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-result" name = "revolt-result"
version = "0.6.8" version = "0.6.9"
dependencies = [ dependencies = [
"revolt_okapi", "revolt_okapi",
"revolt_rocket_okapi", "revolt_rocket_okapi",
@@ -3859,9 +3860,9 @@ dependencies = [
[[package]] [[package]]
name = "rocket_authifier" name = "rocket_authifier"
version = "1.0.7" version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4554a81937341d8c3cd3e43431ac2d23624444dc3a88e7ffd3ff66593779293" checksum = "5f89a12311f60e9288833fc3ce6029bce5d5c61870ceef74d4a50668a8b520ad"
dependencies = [ dependencies = [
"authifier", "authifier",
"iso8601-timestamp 0.1.10", "iso8601-timestamp 0.1.10",
+3
View File
@@ -1,7 +1,10 @@
[workspace] [workspace]
resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"] members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
[patch.crates-io] [patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" } # mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" } redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" } redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
+2
View File
@@ -2,6 +2,8 @@ disallowed-methods = [
# Shouldn't need to access these directly # Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field", "revolt_database::models::bots::model::Bot::remove_field",
"revolt_database::models::messages::model::Message::attach_sendable_embed", "revolt_database::models::messages::model::Message::attach_sendable_embed",
"revolt_database::models::users::model::User::set_relationship",
"revolt_database::models::users::model::User::apply_relationship",
# Prefer to use Object::create() # Prefer to use Object::create()
"revolt_database::models::bots::ops::AbstractBots::insert_bot", "revolt_database::models::bots::ops::AbstractBots::insert_bot",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.8" version = "0.6.9"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.6.8" version = "0.6.9"
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>"]
+3
View File
@@ -0,0 +1,3 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
+6 -1
View File
@@ -13,7 +13,12 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
FileFormat::Toml, FileFormat::Toml,
)); ));
if std::path::Path::new("Revolt.toml").exists() { if std::env::var("TEST_DB").is_ok() {
builder = builder.add_source(File::from_str(
include_str!("../Revolt.test.toml"),
FileFormat::Toml,
));
} else if std::path::Path::new("Revolt.toml").exists() {
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml)); builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
} }
+7 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.6.8" version = "0.6.9"
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>"]
@@ -23,11 +23,11 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.6.8", path = "../config" } revolt-config = { version = "0.6.9", path = "../config" }
revolt-result = { version = "0.6.8", path = "../result" } revolt-result = { version = "0.6.9", path = "../result" }
revolt-models = { version = "0.6.8", path = "../models" } revolt-models = { version = "0.6.9", path = "../models" }
revolt-presence = { version = "0.6.8", path = "../presence" } revolt-presence = { version = "0.6.9", path = "../presence" }
revolt-permissions = { version = "0.6.8", path = "../permissions", features = [ revolt-permissions = { version = "0.6.9", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
@@ -86,4 +86,4 @@ fcm = "0.9.2"
web-push = "0.10.0" web-push = "0.10.0"
# Authifier # Authifier
authifier = { version = "1.0" } authifier = { version = "1.0.8" }
@@ -0,0 +1,89 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"discriminator": "0001"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "TextChannel",
"name": "General",
"server": "__ID:4__",
"default_permissions": {
"a": 0,
"d": 1048576
},
"role_permissions": {
"__ID:5__": {
"a": 1048576,
"d": 0
}
}
},
{
"_object_type": "Server",
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"roles": {
"__ID:5__": {
"name": "Moderator",
"permissions": {
"a": 545270208,
"d": 0
},
"rank": 3
},
"__ID:6__": {
"name": "Owner",
"permissions": {
"a": 0,
"d": 0
},
"rank": 0
}
},
"default_permissions": 4000322560
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:2__",
"server": "__ID:4__"
},
"joined_at": 1698318340195
}
]
+32 -21
View File
@@ -1,6 +1,9 @@
mod mongodb; mod mongodb;
mod reference; mod reference;
use rand::Rng;
use revolt_config::config;
pub use self::mongodb::*; pub use self::mongodb::*;
pub use self::reference::*; pub use self::reference::*;
@@ -31,25 +34,44 @@ impl DatabaseInfo {
/// Create a database client from the given database information /// Create a database client from the given database information
#[async_recursion] #[async_recursion]
pub async fn connect(self) -> Result<Database, String> { pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self { Ok(match self {
DatabaseInfo::Auto => { DatabaseInfo::Auto => {
if let Ok(uri) = std::env::var("MONGODB") { if std::env::var("TEST_DB").is_ok() {
return DatabaseInfo::MongoDb { DatabaseInfo::Test(format!(
uri, "revolt_test_{}",
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(), database_name: "revolt".to_string(),
} }
.connect() .connect()
.await; .await?
} else {
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Test(database_name) => { DatabaseInfo::Test(database_name) => {
if let Ok(uri) = std::env::var("MONGODB") { match std::env::var("TEST_DB")
return DatabaseInfo::MongoDb { uri, database_name }.connect().await; .expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"MONGODB" => {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
} }
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Reference => Database::Reference(Default::default()), DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::MongoDb { uri, database_name } => { DatabaseInfo::MongoDb { uri, database_name } => {
@@ -65,14 +87,3 @@ impl DatabaseInfo {
}) })
} }
} }
impl From<Database> for authifier::Database {
fn from(value: Database) -> Self {
match value {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
}
}
}
@@ -4,7 +4,7 @@ use futures::lock::Mutex;
use crate::{ use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, Server, ServerBan, User, UserSettings, Webhook, MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
}; };
database_derived!( database_derived!(
@@ -19,6 +19,7 @@ database_derived!(
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>, pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub files: Arc<Mutex<HashMap<String, File>>>, pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>, pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>, pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>, pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>, pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
+6 -11
View File
@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember, FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, UserSettings, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User,
Webhook, UserSettings, Webhook,
}; };
use revolt_result::Error; use revolt_result::Error;
@@ -154,17 +154,12 @@ pub enum EventV1 {
event_id: Option<String>, event_id: Option<String>,
}, },
/*/// Relationship with another user changed /// Relationship with another user changed
UserRelationship { UserRelationship { id: String, user: User },
id: String,
user: User,
// ! this field can be deprecated
status: RelationshipStatus,
},*/
/// Settings updated remotely /// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings }, UserSettingsUpdate { id: String, update: UserSettings },
/*/// User has been platform banned or deleted their account /// User has been platform banned or deleted their account
/// ///
/// Clients should remove the following associated data: /// Clients should remove the following associated data:
/// - Messages /// - Messages
@@ -173,7 +168,7 @@ pub enum EventV1 {
/// - Server Memberships /// - Server Memberships
/// ///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 }, */ UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji /// New emoji
EmojiCreate(Emoji), EmojiCreate(Emoji),
@@ -18,7 +18,7 @@ mod tests {
db.migrate_database().await.unwrap(); db.migrate_database().await.unwrap();
// Migrate the existing database // Migrate the existing database
db.migrate_database().await.unwrap() db.migrate_database().await.unwrap();
}); });
} }
} }
@@ -1,3 +1,4 @@
use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
@@ -79,9 +80,8 @@ impl Bot {
return Err(create_error!(IsBot)); return Err(create_error!(IsBot));
} }
// TODO: config let config = config().await;
let max_bot_count = 5; if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
if db.get_number_of_bots_by_user(&owner.id).await? >= max_bot_count {
return Err(create_error!(ReachedMaximumBots)); return Err(create_error!(ReachedMaximumBots));
} }
@@ -1,11 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use revolt_config::config;
use revolt_models::v0::{self, MessageAuthor}; use revolt_models::v0::{self, MessageAuthor};
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use revolt_result::Result; use revolt_result::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User}; use crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
SystemMessage, User,
};
auto_derived!( auto_derived!(
#[serde(tag = "channel_type")] #[serde(tag = "channel_type")]
@@ -188,12 +192,82 @@ impl Channel {
Ok(()) Ok(())
}*/ }*/
/// Create a new server channel
pub async fn create_server_channel(
db: &Database,
server: &mut Server,
data: v0::DataCreateServerChannel,
update_server: bool,
) -> Result<Channel> {
let config = config().await;
if server.channels.len() > config.features.limits.default.server_channels {
return Err(create_error!(TooManyChannels {
max: config.features.limits.default.server_channels,
}));
};
let id = ulid::Ulid::new().to_string();
let channel = match data.channel_type {
v0::LegacyServerChannelType::Text => Channel::TextChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
},
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
},
};
db.insert_channel(&channel).await?;
if update_server {
server
.update(
db,
PartialServer {
channels: Some([server.channels.clone(), [id].into()].concat()),
..Default::default()
},
vec![],
)
.await?;
EventV1::ChannelCreate(channel.clone().into())
.p(server.id.clone())
.await;
}
Ok(channel)
}
/// Create a group /// Create a group
pub async fn create_group( pub async fn create_group(
db: &Database, db: &Database,
data: v0::DataCreateGroup, mut data: v0::DataCreateGroup,
owner_id: String, owner_id: String,
) -> Result<Channel> { ) -> Result<Channel> {
data.users.insert(owner_id.to_string());
let config = config().await;
if data.users.len() > config.features.limits.default.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size,
}));
}
let recipients = data.users.into_iter().collect::<Vec<String>>(); let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group { let channel = Channel::Group {
id: ulid::Ulid::new().to_string(), id: ulid::Ulid::new().to_string(),
@@ -247,10 +321,6 @@ impl Channel {
.p(id.to_string()) .p(id.to_string())
.await; .await;
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
SystemMessage::UserAdded { SystemMessage::UserAdded {
id: user.id.to_string(), id: user.id.to_string(),
by: by_id.to_string(), by: by_id.to_string(),
@@ -268,6 +338,10 @@ impl Channel {
.await .await
.ok(); .ok();
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
Ok(()) Ok(())
} }
_ => Err(create_error!(InvalidOperation)), _ => Err(create_error!(InvalidOperation)),
@@ -295,7 +369,7 @@ impl Channel {
} }
} }
/// Get a reference to this channel's id /// Clone this channel's id
pub fn id(&self) -> String { pub fn id(&self) -> String {
match self { match self {
Channel::DirectMessage { id, .. } Channel::DirectMessage { id, .. }
@@ -614,3 +688,36 @@ impl IntoDocumentPath for FieldsChannel {
}) })
} }
} }
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions_text_channel() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user user 2
channel channel 3);
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &moderator).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
}
@@ -22,15 +22,25 @@ impl AbstractAttachments for ReferenceDb {
async fn find_and_use_attachment( async fn find_and_use_attachment(
&self, &self,
id: &str, id: &str,
_tag: &str, tag: &str,
_parent_type: &str, parent_type: &str,
_parent_id: &str, parent_id: &str,
) -> Result<File> { ) -> Result<File> {
let mut files = self.files.lock().await; let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) { if let Some(file) = files.get_mut(id) {
// TODO: check tag if file.tag == tag {
// TODO: set parent ID match parent_type {
Ok(file.clone()) "message" => file.message_id = Some(parent_id.to_owned()),
"user" => file.user_id = Some(parent_id.to_owned()),
"object" => file.object_id = Some(parent_id.to_owned()),
"server" => file.server_id = Some(parent_id.to_owned()),
_ => unreachable!(),
}
Ok(file.clone())
} else {
Err(create_error!(NotFound))
}
} else { } else {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
} }
@@ -60,7 +60,7 @@ impl AbstractMessages for ReferenceDb {
.cloned() .cloned()
.collect(); .collect();
// TODO: sorting, etc // TODO: sorting, etc (will be required for tests)
Ok(matched_messages) Ok(matched_messages)
@@ -1,28 +1,46 @@
use std::cmp::Ordering;
use std::time::Duration; use std::time::Duration;
use std::time::SystemTime;
use super::AbstractRatelimitEvents; use super::AbstractRatelimitEvents;
use crate::RatelimitEvent; use crate::RatelimitEvent;
use crate::RatelimitEventType; use crate::RatelimitEventType;
use crate::ReferenceDb; use crate::ReferenceDb;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid;
#[async_trait] #[async_trait]
impl AbstractRatelimitEvents for ReferenceDb { impl AbstractRatelimitEvents for ReferenceDb {
/// Insert a new ratelimit event /// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, _event: &RatelimitEvent) -> Result<()> { async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
// TODO: implement let mut ratelimit_events = self.ratelimit_events.lock().await;
unimplemented!() if ratelimit_events.contains_key(&event.id) {
Err(create_database_error!("insert", "message"))
} else {
ratelimit_events.insert(event.id.to_string(), event.clone());
Ok(())
}
} }
/// Count number of events in given duration and check if we've hit the limit /// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited( async fn has_ratelimited(
&self, &self,
_target_id: &str, target_id: &str,
_event_type: RatelimitEventType, event_type: RatelimitEventType,
_period: Duration, period: Duration,
_count: usize, count: usize,
) -> Result<bool> { ) -> Result<bool> {
// TODO: implement let ratelimit_events = self.ratelimit_events.lock().await;
unimplemented!() let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string();
Ok(ratelimit_events
.iter()
.filter(|(id, event)| {
id.cmp(&&gte_cmp_id) == Ordering::Greater
&& event.target_id == target_id
&& event.event_type == event_type
})
.count()
>= count)
} }
} }
@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use crate::{ use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Database, File, Server, events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
SystemMessage, User, Server, SystemMessage, User,
}; };
auto_derived_partial!( auto_derived_partial!(
@@ -80,9 +80,8 @@ impl Member {
db: &Database, db: &Database,
server: &Server, server: &Server,
user: &User, user: &User,
// channels: Option<Vec<Channel>>, channels: Option<Vec<Channel>>,
//) -> Result<Vec<Channel>> { ) -> Result<Vec<Channel>> {
) -> Result<()> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() { if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned)); return Err(create_error!(Banned));
} }
@@ -101,9 +100,10 @@ impl Member {
db.insert_member(&member).await?; db.insert_member(&member).await?;
let mut channels = vec![]; let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if true { if should_fetch {
let query = DatabasePermissionQuery::new(db, user).server(server); let query = DatabasePermissionQuery::new(db, user).server(server);
let existing_channels = db.fetch_channels(&server.channels).await?; let existing_channels = db.fetch_channels(&server.channels).await?;
@@ -152,8 +152,7 @@ impl Member {
.ok(); .ok();
} }
// Ok(channels) Ok(channels)
Ok(())
} }
/// Update member data /// Update member data
@@ -1,10 +1,11 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use revolt_permissions::OverrideField; use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{events::client::EventV1, Database, File}; use crate::{events::client::EventV1, Channel, Database, File, User};
auto_derived_partial!( auto_derived_partial!(
/// Server /// Server
@@ -131,8 +132,51 @@ auto_derived!(
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Server { impl Server {
/// Create a server /// Create a server
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(
db.insert_server(self).await db: &Database,
data: v0::DataCreateServer,
owner: &User,
create_default_channels: bool,
) -> Result<(Server, Vec<Channel>)> {
let mut server = Server {
id: ulid::Ulid::new().to_string(),
owner: owner.id.to_string(),
name: data.name,
description: data.description,
channels: vec![],
nsfw: data.nsfw.unwrap_or(false),
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
analytics: false,
banner: None,
categories: None,
discoverable: false,
flags: None,
icon: None,
roles: HashMap::new(),
system_messages: None,
};
let channels: Vec<Channel> = if create_default_channels {
vec![
Channel::create_server_channel(
db,
&mut server,
DataCreateServerChannel {
channel_type: v0::LegacyServerChannelType::Text,
name: "General".to_string(),
..Default::default()
},
false,
)
.await?,
]
} else {
vec![]
};
db.insert_server(&server).await?;
Ok((server, channels))
} }
/// Update server data /// Update server data
@@ -209,80 +253,7 @@ impl Server {
} }
} }
/* /// Create a new member in a server /*
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
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,
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
/* // TODO: EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
}
.private(user.id.clone())
.await; */
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server /// Remove a member from a server
pub async fn remove_member( pub async fn remove_member(
@@ -450,3 +421,36 @@ impl SystemMessageChannels {
ids ids
} }
} }
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user user 2
server server 4);
let mut query = DatabasePermissionQuery::new(&db, &owner).server(&server);
assert!(calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::GrantAllSafe));
let mut query = DatabasePermissionQuery::new(&db, &moderator).server(&server);
assert!(calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::BanMembers));
let mut query = DatabasePermissionQuery::new(&db, &user).server(&server);
assert!(!calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::BanMembers));
});
}
}
@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::Database; use crate::{events::client::EventV1, Database};
use revolt_result::Result; use revolt_result::Result;
@@ -16,12 +16,12 @@ impl UserSettingsImpl for UserSettings {
async fn set(self, db: &Database, user: &str) -> Result<()> { async fn set(self, db: &Database, user: &str) -> Result<()> {
db.set_user_settings(user, &self).await?; db.set_user_settings(user, &self).await?;
/* // TODO: EventV1::UserSettingsUpdate { EventV1::UserSettingsUpdate {
id: user.to_string(), id: user.to_string(),
update: self, update: self,
} }
.private(user.to_string()) .private(user.to_string())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -4,6 +4,7 @@ use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_result::{create_error, Error, ErrorType, Result}; use revolt_result::{create_error, Error, ErrorType, Result};
use ulid::Ulid; use ulid::Ulid;
@@ -197,6 +198,13 @@ impl User {
RelationshipStatus::None RelationshipStatus::None
} }
pub fn is_friends_with(&self, user_b: &str) -> bool {
matches!(
self.relationship_with(user_b),
RelationshipStatus::Friend | RelationshipStatus::User
)
}
/// Check whether two users have a mutual connection /// Check whether two users have a mutual connection
/// ///
/// This will check if user and user_b share a server or a group. /// This will check if user and user_b share a server or a group.
@@ -211,6 +219,18 @@ impl User {
.is_empty()) .is_empty())
} }
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
let config = config().await;
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
Ok(())
} else {
Err(create_error!(TooManyServers {
max: config.features.limits.default.servers
}))
}
}
/// Sanitise and validate a username can be used /// Sanitise and validate a username can be used
pub fn validate_username(username: String) -> Result<String> { pub fn validate_username(username: String) -> Result<String> {
// Copy the username for validation // Copy the username for validation
@@ -341,6 +361,167 @@ impl User {
} }
} }
/// Set a relationship to another user
pub async fn set_relationship(
&mut self,
db: &Database,
user_b: &User,
status: RelationshipStatus,
) -> Result<()> {
db.set_relationship(&self.id, &user_b.id, &status).await?;
if let RelationshipStatus::None | RelationshipStatus::User = status {
if let Some(relations) = &mut self.relations {
relations.retain(|relation| relation.id != user_b.id);
}
} else {
let relation = Relationship {
id: user_b.id.to_string(),
status,
};
if let Some(relations) = &mut self.relations {
relations.retain(|relation| relation.id != user_b.id);
relations.push(relation);
} else {
self.relations = Some(vec![relation]);
}
}
Ok(())
}
/// Apply a certain relationship between two users
pub async fn apply_relationship(
&mut self,
db: &Database,
target: &mut User,
local: RelationshipStatus,
remote: RelationshipStatus,
) -> Result<()> {
target.set_relationship(db, self, remote).await?;
self.set_relationship(db, target, local).await?;
EventV1::UserRelationship {
id: target.id.clone(),
user: self.clone().into(db, Some(&*target)).await,
}
.private(target.id.clone())
.await;
EventV1::UserRelationship {
id: self.id.clone(),
user: target.clone().into(db, Some(&*self)).await,
}
.private(self.id.clone())
.await;
Ok(())
}
/// Add another user as a friend
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User => Err(create_error!(NoEffect)),
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
RelationshipStatus::Outgoing => Err(create_error!(AlreadySentRequest)),
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.await
}
RelationshipStatus::None => {
self.apply_relationship(
db,
target,
RelationshipStatus::Outgoing,
RelationshipStatus::Incoming,
)
.await
}
}
}
/// Remove another user as a friend
pub async fn remove_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(create_error!(NoEffect)),
}
}
/// Block another user
pub async fn block_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(create_error!(NoEffect)),
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::None
| RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::BlockedOther,
)
.await
}
}
}
/// Unblock another user
pub async fn unblock_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::Blocked => match target.relationship_with(&self.id) {
RelationshipStatus::Blocked => {
self.apply_relationship(
db,
target,
RelationshipStatus::BlockedOther,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(create_error!(InternalError)),
},
_ => Err(create_error!(NoEffect)),
}
}
/// Update user data /// Update user data
pub async fn update<'a>( pub async fn update<'a>(
&mut self, &mut self,
@@ -1,7 +1,7 @@
use revolt_result::Result; use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use crate::{ReferenceDb, Relationship};
use super::AbstractUsers; use super::AbstractUsers;
@@ -106,19 +106,49 @@ impl AbstractUsers for ReferenceDb {
/// Set relationship with another user /// Set relationship with another user
/// ///
/// This should use pull_relationship if relationship is None. /// This should use pull_relationship if relationship is None or User.
async fn set_relationship( async fn set_relationship(
&self, &self,
_user_id: &str, user_id: &str,
_target_id: &str, target_id: &str,
_relationship: &RelationshipStatus, relationship: &RelationshipStatus,
) -> Result<()> { ) -> Result<()> {
todo!() if let RelationshipStatus::User | RelationshipStatus::None = &relationship {
self.pull_relationship(user_id, target_id).await
} else {
let mut users = self.users.lock().await;
let user = users
.get_mut(user_id)
.ok_or_else(|| create_error!(NotFound))?;
let relation = Relationship {
id: target_id.to_string(),
status: relationship.clone(),
};
if let Some(relations) = &mut user.relations {
relations.retain(|relation| relation.id != target_id);
relations.push(relation);
} else {
user.relations = Some(vec![relation]);
}
Ok(())
}
} }
/// Remove relationship with another user /// Remove relationship with another user
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> { async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
todo!() let mut users = self.users.lock().await;
let user = users
.get_mut(user_id)
.ok_or_else(|| create_error!(NotFound))?;
if let Some(relations) = &mut user.relations {
relations.retain(|relation| relation.id != target_id);
}
Ok(())
} }
/// Delete a user by their id /// Delete a user by their id
@@ -13,19 +13,19 @@ impl<'r> FromRequest<'r> for User {
.local_cache_async(async { .local_cache_async(async {
let db = request.rocket().state::<Database>().expect("`Database`"); let db = request.rocket().state::<Database>().expect("`Database`");
let _header_bot_token = request let header_bot_token = request
.headers() .headers()
.get("x-bot-token") .get("x-bot-token")
.next() .next()
.map(|x| x.to_string()); .map(|x| x.to_string());
/* if let Some(bot_token) = header_bot_token { if let Some(bot_token) = header_bot_token {
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await { if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
return Some(user); if let Ok(user) = db.fetch_user(&bot.id).await {
return Some(user);
}
} }
} else */ } else if let Outcome::Success(session) = request.guard::<Session>().await {
if let Outcome::Success(session) = request.guard::<Session>().await {
// This uses a guard so can't really easily be refactored into from_token at this stage.
if let Ok(user) = db.fetch_user(&session.user_id).await { if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user); return Some(user);
} }
+2 -2
View File
@@ -13,12 +13,12 @@ pub mod process_embeds;
pub mod web_push; pub mod web_push;
/// Spawn background workers /// Spawn background workers
pub async fn start_workers(db: Database) { pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
for _ in 0..WORKER_COUNT { for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone())); task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone())); task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone())); task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(db.clone().into())); task::spawn(web_push::worker(authifier_db.clone()));
} }
} }
+51 -22
View File
@@ -1,4 +1,7 @@
use revolt_models::v0::*; use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission};
use crate::{util::permissions::DatabasePermissionQuery, Database};
impl crate::Bot { impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot { pub fn into_public_bot(self, user: crate::User) -> PublicBot {
@@ -635,43 +638,69 @@ impl From<crate::FieldsRole> for FieldsRole {
} }
impl crate::User { impl crate::User {
pub async fn into<'a, P>(self, perspective: P) -> User pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
where where
P: Into<Option<&'a crate::User>>, P: Into<Option<&'a crate::User>>,
{ {
let relationship = if let Some(perspective) = perspective.into() { let perspective = perspective.into();
let (relationship, can_see_profile) = if self.bot.is_some() {
(RelationshipStatus::None, true)
} else if let Some(perspective) = perspective {
let mut query = DatabasePermissionQuery::new(db, perspective).user(&self);
if perspective.id == self.id { if perspective.id == self.id {
RelationshipStatus::User (RelationshipStatus::User, true)
} else { } else {
perspective (
.relations perspective
.as_ref() .relations
.map(|relations| { .as_ref()
relations .map(|relations| {
.iter() relations
.find(|relationship| relationship.id == self.id) .iter()
.map(|relationship| relationship.status.clone().into()) .find(|relationship| relationship.id == self.id)
.unwrap_or_default() .map(|relationship| relationship.status.clone().into())
}) .unwrap_or_default()
.unwrap_or_default() })
.unwrap_or_default(),
calculate_user_permissions(&mut query)
.await
.has_user_permission(UserPermission::ViewProfile),
)
} }
} else { } else {
RelationshipStatus::None (RelationshipStatus::None, false)
}; };
// do permission stuff here
// TODO: implement permissions =)
let can_see_profile = false;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
display_name: self.display_name, display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()), avatar: self.avatar.map(|file| file.into()),
relations: vec![], relations: if let Some(crate::User { id, .. }) = perspective {
if id == &self.id {
self.relations
.unwrap_or_default()
.into_iter()
.map(|relation| relation.into())
.collect()
} else {
vec![]
}
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32, badges: self.badges.unwrap_or_default() as u32,
status: None, status: if can_see_profile {
profile: None, self.status.map(|status| status.into())
} else {
None
},
profile: if can_see_profile {
self.profile.map(|profile| profile.into())
} else {
None
},
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
+4 -1
View File
@@ -1,6 +1,9 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use revolt_result::{create_error, Error, Result}; use revolt_result::{create_error, Result};
#[cfg(feature = "rocket-impl")]
use revolt_result::Error;
use async_std::sync::Mutex; use async_std::sync::Mutex;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
+3
View File
@@ -2,3 +2,6 @@ pub mod bridge;
pub mod idempotency; pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
#[cfg(test)]
pub mod test_fixtures;
+29 -14
View File
@@ -1,7 +1,8 @@
use std::borrow::Cow; use std::borrow::Cow;
use revolt_permissions::{ use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus, calculate_user_permissions, ChannelType, Override, PermissionQuery, PermissionValue,
RelationshipStatus,
}; };
use crate::{Channel, Database, Member, Server, User}; use crate::{Channel, Database, Member, Server, User};
@@ -19,7 +20,8 @@ pub struct DatabasePermissionQuery<'a> {
member: Option<Cow<'a, Member>>, member: Option<Cow<'a, Member>>,
// flag_known_relationship: Option<&'a RelationshipStatus>, // flag_known_relationship: Option<&'a RelationshipStatus>,
cached_user_permission: Option<u32>, cached_user_permission: Option<PermissionValue>,
cached_mutual_connection: Option<bool>,
cached_permission: Option<u64>, cached_permission: Option<u64>,
} }
@@ -49,6 +51,10 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
/// Get the relationship with have with the currently selected user /// Get the relationship with have with the currently selected user
async fn user_relationship(&mut self) -> RelationshipStatus { async fn user_relationship(&mut self) -> RelationshipStatus {
if let Some(other_user) = &self.user { if let Some(other_user) = &self.user {
if self.perspective.id == other_user.id {
return RelationshipStatus::User;
}
if let Some(relations) = &self.perspective.relations { if let Some(relations) = &self.perspective.relations {
for entry in relations { for entry in relations {
if entry.id == other_user.id { if entry.id == other_user.id {
@@ -82,14 +88,17 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
/// Do we have a mutual connection with the currently selected user? /// Do we have a mutual connection with the currently selected user?
async fn have_mutual_connection(&mut self) -> bool { async fn have_mutual_connection(&mut self) -> bool {
if let Some(user) = &self.user { if let Some(value) = self.cached_mutual_connection {
// TODO: cache result? value
matches!( } else if let Some(user) = &self.user {
self.perspective let value = self
.has_mutual_connection(self.database, &user.id) .perspective
.await, .has_mutual_connection(self.database, &user.id)
Ok(true) .await
) .unwrap_or_default();
self.cached_mutual_connection = Some(value);
matches!(value, true)
} else { } else {
false false
} }
@@ -111,11 +120,15 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
if let Some(server) = &self.server { if let Some(server) = &self.server {
if self.member.is_some() { if self.member.is_some() {
true true
} else if let Ok(member) = self
.database
.fetch_member(&server.id, &self.perspective.id)
.await
{
self.member = Some(Cow::Owned(member));
true
} else { } else {
self.database false
.fetch_member(&server.id, &self.perspective.id)
.await
.is_ok()
} }
} else { } else {
false false
@@ -331,6 +344,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
| Cow::Owned(Channel::TextChannel { server, .. }) | Cow::Owned(Channel::TextChannel { server, .. })
| Cow::Borrowed(Channel::VoiceChannel { server, .. }) | Cow::Borrowed(Channel::VoiceChannel { server, .. })
| Cow::Owned(Channel::VoiceChannel { server, .. }) => { | Cow::Owned(Channel::VoiceChannel { server, .. }) => {
// FIXME: may double fetch
if let Ok(server) = self.database.fetch_server(server).await { if let Ok(server) = self.database.fetch_server(server).await {
self.server.replace(Cow::Owned(server)); self.server.replace(Cow::Owned(server));
} }
@@ -352,6 +366,7 @@ impl<'a> DatabasePermissionQuery<'a> {
server: None, server: None,
member: None, member: None,
cached_mutual_connection: None,
cached_user_permission: None, cached_user_permission: None,
cached_permission: None, cached_permission: None,
} }
+11 -1
View File
@@ -7,7 +7,7 @@ use schemars::{
JsonSchema, JsonSchema,
}; };
use crate::{Bot, Channel, Database, Emoji, Message, Webhook}; use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook};
/// Reference to some object in the database /// Reference to some object in the database
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -42,6 +42,16 @@ impl Reference {
db.fetch_message(&self.id).await db.fetch_message(&self.id).await
} }
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
}
/// Fetch webhook from Ref /// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> { pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await db.fetch_webhook(&self.id).await
@@ -0,0 +1,103 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use serde_json::from_str;
use crate::{Channel, Database, Member, Server, User};
static RE_ID: Lazy<Regex> = Lazy::new(|| Regex::new("__ID:(\\d+)__").unwrap());
#[derive(Debug, Deserialize)]
#[serde(tag = "_object_type")]
enum LoadedFixture {
User(User),
Channel(Channel),
Server(Server),
ServerMember(Member),
}
pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String> {
let mut ids = HashMap::<String, String>::new();
let input = RE_ID.replace_all(input, |cap: &Captures| {
let d = cap.get(1).unwrap().as_str();
if !ids.contains_key(d) {
ids.insert(d.to_string(), ulid::Ulid::new().to_string());
}
ids.get(d).unwrap().clone()
});
// Deserialise the fixtures
let items: Vec<LoadedFixture> = from_str(&input).expect("Failed to deserialise fixture");
// Load all of the items within
for item in items {
#[allow(clippy::disallowed_methods)]
match item {
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
}
}
// Return IDs for ease of use
ids
}
#[async_trait]
pub trait FetchFixture {
async fn user(&self, db: &Database, d: usize) -> User;
async fn channel(&self, db: &Database, d: usize) -> Channel;
async fn server(&self, db: &Database, d: usize) -> Server;
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member;
}
#[async_trait]
impl FetchFixture for HashMap<String, String> {
async fn user(&self, db: &Database, d: usize) -> User {
db.fetch_user(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn channel(&self, db: &Database, d: usize) -> Channel {
db.fetch_channel(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn server(&self, db: &Database, d: usize) -> Server {
db.fetch_server(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member {
db.fetch_member(
self.get(&d_server.to_string()).unwrap(),
self.get(&d_user.to_string()).unwrap(),
)
.await
.unwrap()
}
}
#[macro_export]
macro_rules! fixture {
( $database:expr, $name:expr, $( $variable:ident $type:ident $id: expr )+ ) => {
use $crate::util::test_fixtures::FetchFixture;
let fixtures = $crate::util::test_fixtures::load_fixture(
&$database,
include_str!(concat!("../../../fixtures/", $name, ".json")),
)
.await;
$(
let $variable = fixtures.$type(&$database, $id).await;
)+
};
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.6.8" version = "0.6.9"
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>"]
@@ -18,8 +18,8 @@ default = ["serde", "partials"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.6.8", path = "../config" } revolt-config = { version = "0.6.9", path = "../config" }
revolt-permissions = { version = "0.6.8", path = "../permissions" } revolt-permissions = { version = "0.6.9", path = "../permissions" }
# Utility # Utility
regex = "1" regex = "1"
@@ -1,3 +1,4 @@
#[cfg(feature = "validator")]
use validator::Validate; use validator::Validate;
use super::File; use super::File;
@@ -80,7 +81,7 @@ auto_derived!(
} }
/// Information for the webhook /// Information for the webhook
#[derive(Validate)] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct CreateWebhookBody { pub struct CreateWebhookBody {
#[validate(length(min = 1, max = 32))] #[validate(length(min = 1, max = 32))]
pub name: String, pub name: String,
+45 -4
View File
@@ -212,22 +212,63 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(validator::Validate))] #[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateGroup { pub struct DataCreateGroup {
/// Group name /// Group name
#[validate(length(min = 1, max = 32))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String, pub name: String,
/// Group description /// Group description
#[validate(length(min = 0, max = 1024))] #[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>, pub description: Option<String>,
/// Group icon /// Group icon
#[validate(length(min = 1, max = 128))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon: Option<String>, pub icon: Option<String>,
/// Array of user IDs to add to the group /// Array of user IDs to add to the group
/// ///
/// Must be friends with these users. /// Must be friends with these users.
#[validate(length(min = 0, max = 49))] #[cfg_attr(feature = "validator", validate(length(min = 0, max = 49)))]
#[serde(default)] #[serde(default)]
pub users: HashSet<String>, pub users: HashSet<String>,
/// Whether this group is age-restricted /// Whether this group is age-restricted
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
} }
/// Server Channel Type
#[derive(Default)]
pub enum LegacyServerChannelType {
/// Text Channel
#[default]
Text,
/// Voice Channel
Voice,
}
/// Create new server channel
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateServerChannel {
/// Channel type
#[serde(rename = "type", default = "LegacyServerChannelType::default")]
pub channel_type: LegacyServerChannelType,
/// Channel name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Channel description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
}
); );
impl Channel {
/// Get a reference to this channel's id
pub fn id(&self) -> &str {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
#[cfg(feature = "validator")]
use validator::Validate; use validator::Validate;
/// Regex for valid emoji names /// Regex for valid emoji names
@@ -41,7 +43,7 @@ auto_derived!(
} }
/// Create a new emoji /// Create a new emoji
#[derive(Validate)] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateEmoji { pub struct DataCreateEmoji {
/// Server name /// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")] #[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
+6 -3
View File
@@ -6,6 +6,8 @@ use std::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use revolt_config::config; use revolt_config::config;
#[cfg(feature = "validator")]
use validator::Validate; use validator::Validate;
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
@@ -94,7 +96,7 @@ auto_derived!(
} }
/// Name and / or avatar override information /// Name and / or avatar override information
#[derive(Validate)] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct Masquerade { pub struct Masquerade {
// FIXME: missing validation // FIXME: missing validation
/// Replace the display name shown on this message /// Replace the display name shown on this message
@@ -164,7 +166,8 @@ auto_derived!(
} }
/// Representation of a text embed before it is sent. /// Representation of a text embed before it is sent.
#[derive(Default, Validate)] #[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed { pub struct SendableEmbed {
#[validate(length(min = 1, max = 128))] #[validate(length(min = 1, max = 128))]
pub icon_url: Option<String>, pub icon_url: Option<String>,
@@ -188,7 +191,7 @@ auto_derived!(
} }
/// Message to send /// Message to send
#[derive(Validate)] #[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataMessageSend { pub struct DataMessageSend {
/// Unique token to prevent duplicate message sending /// Unique token to prevent duplicate message sending
/// ///
+27 -1
View File
@@ -1,8 +1,11 @@
use super::File; use super::{Channel, File};
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "validator")]
use validator::Validate;
auto_derived_partial!( auto_derived_partial!(
/// Server /// Server
pub struct Server { pub struct Server {
@@ -141,4 +144,27 @@ auto_derived!(
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_banned: Option<String>, pub user_banned: Option<String>,
} }
/// Information about new server to create
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateServer {
/// Server name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Server description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Whether this server is age-restricted
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nsfw: Option<bool>,
}
/// Information returned when creating server
pub struct CreateServerLegacyResponse {
/// Server object
pub server: Server,
/// Default channels
pub channels: Vec<Channel>,
}
); );
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.6.8" version = "0.6.9"
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>"]
+16 -16
View File
@@ -5,37 +5,37 @@ use crate::{
}; };
/// Calculate permissions against a user /// Calculate permissions against a user
pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> u32 { pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await { if query.are_we_privileged().await {
return u32::MAX; return u64::MAX.into();
} }
if query.are_the_users_same().await { if query.are_the_users_same().await {
return u32::MAX; return u64::MAX.into();
} }
let mut permissions = 0_u32; let mut permissions = 0_u64;
match query.user_relationship().await { match query.user_relationship().await {
RelationshipStatus::Friend => return u32::MAX, RelationshipStatus::Friend => return u64::MAX.into(),
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => { RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return UserPermission::Access as u32 return (UserPermission::Access as u64).into()
} }
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => { RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32; permissions = UserPermission::Access as u64;
} }
_ => {} _ => {}
} }
if query.have_mutual_connection().await { if query.have_mutual_connection().await {
permissions = UserPermission::Access + UserPermission::ViewProfile; permissions = UserPermission::Access as u64 + UserPermission::ViewProfile as u64;
if query.user_is_bot().await || query.are_we_a_bot().await { if query.user_is_bot().await || query.are_we_a_bot().await {
permissions += UserPermission::SendMessage as u32; permissions += UserPermission::SendMessage as u64;
} }
permissions permissions.into()
} else { } else {
permissions permissions.into()
} }
} }
@@ -81,9 +81,7 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
query.set_recipient_as_user().await; query.set_recipient_as_user().await;
let permissions = calculate_user_permissions(query).await; let permissions = calculate_user_permissions(query).await;
if (permissions & UserPermission::SendMessage as u32) if permissions.has_user_permission(UserPermission::SendMessage) {
== UserPermission::SendMessage as u32
{
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into() (*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
} else { } else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into() (*DEFAULT_PERMISSION_VIEW_ONLY).into()
@@ -106,7 +104,9 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
ChannelType::ServerChannel => { ChannelType::ServerChannel => {
query.set_server_from_channel().await; query.set_server_from_channel().await;
if query.are_we_a_member().await { if query.are_we_server_owner().await {
return ChannelPermission::GrantAllSafe.into();
} else if query.are_we_a_member().await {
let mut permissions = calculate_server_permissions(query).await; let mut permissions = calculate_server_permissions(query).await;
permissions.apply(query.get_default_channel_permissions().await); permissions.apply(query.get_default_channel_permissions().await);
@@ -121,7 +121,7 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
if !permissions.has_channel_permission(ChannelPermission::ViewChannel) { if !permissions.has_channel_permission(ChannelPermission::ViewChannel) {
permissions.revoke_all(); permissions.revoke_all();
} }
permissions permissions
} else { } else {
0_u64.into() 0_u64.into()
+6 -1
View File
@@ -8,7 +8,7 @@ pub use server::*;
pub use user::*; pub use user::*;
/// Holds a permission value to manipulate. /// Holds a permission value to manipulate.
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct PermissionValue(u64); pub struct PermissionValue(u64);
impl PermissionValue { impl PermissionValue {
@@ -43,6 +43,11 @@ impl PermissionValue {
(self.0 & v) == v (self.0 & v) == v
} }
/// Check whether certain a user permission has been granted
pub fn has_user_permission(&self, permission: UserPermission) -> bool {
self.has(permission as u64)
}
/// Check whether certain a channel permission has been granted /// Check whether certain a channel permission has been granted
pub fn has_channel_permission(&self, permission: ChannelPermission) -> bool { pub fn has_channel_permission(&self, permission: ChannelPermission) -> bool {
self.has(permission as u64) self.has(permission as u64)
+2 -2
View File
@@ -51,9 +51,9 @@ pub enum DataPermissionPoly {
#[cfg_attr(feature = "schemas", derive(JsonSchema))] #[cfg_attr(feature = "schemas", derive(JsonSchema))]
pub struct OverrideField { pub struct OverrideField {
/// Allow bit flags /// Allow bit flags
a: i64, pub a: i64,
/// Disallow bit flags /// Disallow bit flags
d: i64, pub d: i64,
} }
impl Override { impl Override {
+1 -1
View File
@@ -12,7 +12,7 @@ async fn validate_user_permissions() {
let mut query = Scenario {}; let mut query = Scenario {};
let perms = calculate_user_permissions(&mut query).await; let perms = calculate_user_permissions(&mut query).await;
assert_eq!(perms, u32::MAX); assert!(perms.has(u64::MAX));
let perms = calculate_channel_permissions(&mut query).await; let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into(); let value: u64 = perms.into();
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.6.8" version = "0.6.9"
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
View File
@@ -221,7 +221,6 @@ mod tests {
// Create a few more sessions // Create a few more sessions
let (first_session, second_session_id) = create_session(&user_id, 0).await; let (first_session, second_session_id) = create_session(&user_id, 0).await;
assert!(!first_session); assert!(!first_session);
dbg!(second_session_id);
assert_eq!(second_session_id as u8 & 1, 0); assert_eq!(second_session_id as u8 & 1, 0);
let (first_session, other_session_id) = create_session(&other_id, 0).await; let (first_session, other_session_id) = create_session(&other_id, 0).await;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.6.8" version = "0.6.9"
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>"]
+3 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.6.8" version = "0.6.9"
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"
@@ -57,7 +57,7 @@ rocket = { version = "0.5.0-rc.2", default-features = false, features = [
] } ] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.7" } rocket_authifier = { version = "1.0.8" }
rocket_prometheus = "0.10.0-rc.3" rocket_prometheus = "0.10.0-rc.3"
# spec generation # spec generation
@@ -68,6 +68,7 @@ revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
revolt-quark = { path = "../quark" } revolt-quark = { path = "../quark" }
# core # core
authifier = "1.0.8"
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [ revolt-database = { path = "../core/database", features = [
"rocket-impl", "rocket-impl",
+15 -3
View File
@@ -8,6 +8,7 @@ extern crate serde_json;
pub mod routes; pub mod routes;
pub mod util; pub mod util;
use revolt_database::{Database, MongoDb};
use rocket::{Build, Rocket}; use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics; use rocket_prometheus::PrometheusMetrics;
@@ -33,7 +34,12 @@ pub async fn web() -> Rocket<Build> {
// Setup Authifier // Setup Authifier
let authifier = Authifier { let authifier = Authifier {
database: db.clone().into(), database: match db.clone() {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: revolt_quark::util::authifier::config(), config: revolt_quark::util::authifier::config(),
event_channel: Some(sender), event_channel: Some(sender),
}; };
@@ -55,8 +61,14 @@ pub async fn web() -> Rocket<Build> {
}); });
// Launch background task workers // Launch background task workers
async_std::task::spawn(revolt_database::tasks::start_workers(db.clone())); async_std::task::spawn(revolt_database::tasks::start_workers(
async_std::task::spawn(revolt_quark::tasks::start_workers(legacy_db.clone())); db.clone(),
authifier.database.clone(),
));
async_std::task::spawn(revolt_quark::tasks::start_workers(
legacy_db.clone(),
authifier.database.clone(),
));
// Configure CORS // Configure CORS
let cors = CorsOptions { let cors = CorsOptions {
+1 -1
View File
@@ -48,7 +48,7 @@ mod test {
drop(response); drop(response);
let event = harness let event = harness
.wait_for_event(|event| match event { .wait_for_event(&bot.id, |event| match event {
EventV1::UserUpdate { id, .. } => id == &bot.id, EventV1::UserUpdate { id, .. } => id == &bot.id,
_ => false, _ => false,
}) })
+1 -1
View File
@@ -23,7 +23,7 @@ pub async fn fetch_bot(
} }
Ok(Json(FetchBotResponse { Ok(Json(FetchBotResponse {
user: db.fetch_user(&bot.id).await?.into(None).await, user: db.fetch_user(&bot.id).await?.into(db, None).await,
bot: bot.into(), bot: bot.into(),
})) }))
} }
+15 -24
View File
@@ -42,7 +42,7 @@ pub async fn invite_bot(
.await .await
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?; .throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
Member::create(db, &server, &bot_user) Member::create(db, &server, &bot_user, None)
.await .await
.map(|_| EmptyResponse) .map(|_| EmptyResponse)
} }
@@ -66,7 +66,7 @@ pub async fn invite_bot(
mod test { mod test {
use crate::{rocket, util::test::TestHarness}; use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Bot, Channel, Server}; use revolt_database::{events::client::EventV1, Bot, Channel, Server};
use revolt_models::v0; use revolt_models::v0::{self, DataCreateServer};
use rocket::http::{ContentType, Header, Status}; use rocket::http::{ContentType, Header, Status};
#[rocket::async_test] #[rocket::async_test]
@@ -102,7 +102,7 @@ mod test {
drop(response); drop(response);
let event = harness let event = harness
.wait_for_event(|event| match event { .wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(), EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
_ => false, _ => false,
}) })
@@ -125,26 +125,17 @@ mod test {
.await .await
.expect("`Bot`"); .expect("`Bot`");
// FIXME: Server::create_server let (server, _) = Server::create(
let server = Server { &harness.db,
id: ulid::Ulid::new().to_string(), DataCreateServer {
name: TestHarness::rand_string(), name: TestHarness::rand_string(),
owner: user.id.to_string(), ..Default::default()
analytics: false, },
discoverable: false, &user,
nsfw: false, false,
banner: None, )
icon: None, .await
categories: None, .unwrap();
channels: vec![],
default_permissions: 0,
description: None,
flags: None,
roles: Default::default(),
system_messages: None,
};
server.create(&harness.db).await.unwrap();
let response = harness let response = harness
.client .client
@@ -164,7 +155,7 @@ mod test {
drop(response); drop(response);
let event = harness let event = harness
.wait_for_event(|event| match event { .wait_for_event(&server.id, |event| match event {
EventV1::ServerMemberJoin { id, .. } => id == &server.id, EventV1::ServerMemberJoin { id, .. } => id == &server.id,
_ => false, _ => false,
}) })
@@ -1,40 +1,184 @@
use revolt_quark::{ use revolt_database::{
get_relationship, util::{permissions::DatabasePermissionQuery, reference::Reference},
models::{user::RelationshipStatus, Channel, User}, Channel, Database, User,
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
}; };
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Add Member to Group /// # Add Member to Group
/// ///
/// Adds another user to the group. /// Adds another user to the group.
#[openapi(tag = "Groups")] #[openapi(tag = "Groups")]
#[put("/<target>/recipients/<member>")] #[put("/<group_id>/recipients/<member_id>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> { pub async fn req(
db: &State<Database>,
user: User,
group_id: Reference,
member_id: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(create_error!(IsBot));
} }
let mut channel = target.as_channel(db).await?; let mut channel = group_id.as_channel(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
.channel(&channel) calculate_channel_permissions(&mut query)
.throw_permission_and_view_channel(db, Permission::InviteOthers) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
match &channel { match &channel {
Channel::Group { .. } => { Channel::Group { .. } => {
let member = member.as_user(db).await?; // FIXME: use permissions here?
if !matches!( // interesting if users could block new group invites
get_relationship(&user, &member.id), let member = member_id.as_user(db).await?;
RelationshipStatus::Friend if !user.is_friends_with(&member.id) {
) { return Err(create_error!(NotFriends));
return Err(Error::NotFriends);
} }
channel channel
.add_user_to_group(db, &member.id, &user.id) .add_user_to_group(db, &member, &user.id)
.await .await
.map(|_| EmptyResponse) .map(|_| EmptyResponse)
} }
_ => Err(Error::InvalidOperation), _ => Err(create_error!(InvalidOperation)),
}
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel, RelationshipStatus};
use revolt_models::v0;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_add_member() {
let mut harness = TestHarness::new().await;
let (_, session, mut user) = harness.new_user().await;
let (_, _, mut other_user) = harness.new_user().await;
#[allow(clippy::disallowed_methods)]
user.apply_relationship(
&harness.db,
&mut other_user,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.await
.unwrap();
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let response = harness
.client
.put(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(&format!("{}!", other_user.id), |event| match event {
EventV1::ChannelCreate(channel) => channel.id() == group.id(),
_ => false,
})
.await;
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelGroupJoin { user, .. } => assert_eq!(user, other_user.id),
_ => unreachable!(),
};
let message = harness.wait_for_message(&group.id()).await;
assert_eq!(
message.system,
Some(v0::SystemMessage::UserAdded {
id: other_user.id.to_string(),
by: user.id.to_string()
})
);
}
#[rocket::async_test]
async fn fail_add_non_friend() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, _, other_user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let response = harness
.client
.put(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::Forbidden);
}
#[rocket::async_test]
async fn fail_add_already_in_group() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let response = harness
.client
.put(format!("/channels/{}/recipients/{}", group.id(), user.id))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::Conflict);
} }
} }
@@ -1,4 +1,3 @@
use revolt_config::config;
use revolt_database::{Channel, Database, RelationshipStatus, User}; use revolt_database::{Channel, Database, RelationshipStatus, User};
use revolt_models::v0; use revolt_models::v0;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
@@ -17,26 +16,17 @@ pub async fn create_group(
user: User, user: User,
data: Json<v0::DataCreateGroup>, data: Json<v0::DataCreateGroup>,
) -> Result<Json<v0::Channel>> { ) -> Result<Json<v0::Channel>> {
let config = config().await;
if user.bot.is_some() { if user.bot.is_some() {
return Err(create_error!(IsBot)); return Err(create_error!(IsBot));
} }
let mut data = data.into_inner(); let data = data.into_inner();
data.validate().map_err(|error| { data.validate().map_err(|error| {
create_error!(FailedValidation { create_error!(FailedValidation {
error: error.to_string() error: error.to_string()
}) })
})?; })?;
data.users.insert(user.id.to_string());
if data.users.len() > config.features.limits.default.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size,
}));
}
for target in &data.users { for target in &data.users {
match user.relationship_with(target) { match user.relationship_with(target) {
RelationshipStatus::Friend | RelationshipStatus::User => {} RelationshipStatus::Friend | RelationshipStatus::User => {}
@@ -52,12 +42,13 @@ pub async fn create_group(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{rocket, util::test::TestHarness}; use crate::{rocket, util::test::TestHarness};
use revolt_database::events::client::EventV1;
use revolt_models::v0; use revolt_models::v0;
use rocket::http::{ContentType, Header, Status}; use rocket::http::{ContentType, Header, Status};
#[rocket::async_test] #[rocket::async_test]
async fn create_group() { async fn create_group() {
let harness = TestHarness::new().await; let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await; let (_, session, user) = harness.new_user().await;
let response = harness let response = harness
@@ -88,7 +79,22 @@ mod test {
assert_eq!(recipients.len(), 1); assert_eq!(recipients.len(), 1);
assert!(harness.db.fetch_channel(&id).await.is_ok()); assert!(harness.db.fetch_channel(&id).await.is_ok());
// TODO: does not check for events let event = harness
.wait_for_event(&format!("{}!", user.id), |event| match event {
EventV1::ChannelCreate(channel) => channel.id() == id,
_ => false,
})
.await;
match event {
EventV1::ChannelCreate(v0::Channel::Group {
owner: channel_owner,
..
}) => {
assert_eq!(owner, channel_owner);
}
_ => unreachable!(),
}
} }
_ => unreachable!(), _ => unreachable!(),
} }
@@ -40,3 +40,151 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
_ => Err(Error::InvalidOperation), _ => Err(Error::InvalidOperation),
} }
} }
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel, RelationshipStatus};
use revolt_models::v0;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_remove_member() {
let mut harness = TestHarness::new().await;
let (_, session, mut user) = harness.new_user().await;
let (_, _, mut other_user) = harness.new_user().await;
#[allow(clippy::disallowed_methods)]
user.apply_relationship(
&harness.db,
&mut other_user,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.await
.unwrap();
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let response = harness
.client
.put(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(&format!("{}!", other_user.id), |event| match event {
EventV1::ChannelCreate(channel) => channel.id() == group.id(),
_ => false,
})
.await;
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelGroupJoin { user, .. } => assert_eq!(user, other_user.id),
_ => unreachable!(),
};
let message = harness.wait_for_message(&group.id()).await;
assert_eq!(
message.system,
Some(v0::SystemMessage::UserAdded {
id: other_user.id.to_string(),
by: user.id.to_string()
})
);
}
#[rocket::async_test]
async fn fail_not_in_group() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, _, other_user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let response = harness
.client
.delete(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
dbg!(response.into_string().await);
// TODO: finish impl
// assert_eq!(response.status(), Status::NotFound);
}
#[rocket::async_test]
async fn fail_not_group_owner() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (_, session, other_user) = harness.new_user().await;
let (_, _, user_to_be_kicked) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
users: vec![&other_user.id, &user_to_be_kicked.id]
.into_iter()
.cloned()
.collect(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let _response = harness
.client
.delete(format!(
"/channels/{}/recipients/{}",
group.id(),
user_to_be_kicked.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
// TODO: impl assert_eq!(response.status(), Status::Forbidden);
}
}
@@ -78,6 +78,11 @@ pub async fn req(
// 3. Replace if we are given new embeds // 3. Replace if we are given new embeds
if let Some(embeds) = edit.embeds { if let Some(embeds) = edit.embeds {
// Ensure we have permissions to send embeds
permissions
.throw_permission_and_view_channel(db, Permission::SendEmbeds)
.await?;
new_embeds.clear(); new_embeds.clear();
for embed in embeds { for embed in embeds {
@@ -89,14 +94,19 @@ pub async fn req(
message.update(db, partial).await?; message.update(db, partial).await?;
// Queue up a task for processing embeds // Queue up a task for processing embeds if the we have sufficient permissions
if let Some(content) = edit.content { if permissions
revolt_quark::tasks::process_embeds::queue( .has_permission(db, Permission::SendEmbeds)
message.channel.to_string(), .await?
message.id.to_string(), {
content, if let Some(content) = edit.content {
) revolt_quark::tasks::process_embeds::queue(
.await; message.channel.to_string(),
message.id.to_string(),
content,
)
.await;
}
} }
Ok(Json(message)) Ok(Json(message))
@@ -62,7 +62,7 @@ pub async fn message_send(
} }
// Create the message // Create the message
let author: v0::User = user.clone().into(Some(&user)).await; let author: v0::User = user.clone().into(db, Some(&user)).await;
Ok(Json( Ok(Json(
Message::create_from_api( Message::create_from_api(
db, db,
@@ -70,7 +70,7 @@ pub async fn message_send(
data, data,
v0::MessageAuthor::User(&author), v0::MessageAuthor::User(&author),
idempotency, idempotency,
true, permissions.has_channel_permission(ChannelPermission::SendEmbeds),
) )
.await? .await?
.into(), .into(),
+1
View File
@@ -138,6 +138,7 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
} }
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "FIXME: THIS TEST CAUSES cargo test TO SEG FAULT, I HAVE NO CLUE HOW")]
mod test { mod test {
use crate::rocket; use crate::rocket;
use rocket::http::Status; use rocket::http::Status;
+27 -107
View File
@@ -1,119 +1,39 @@
use std::collections::HashMap; use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_quark::{ use revolt_models::v0;
models::{server::PartialServer, Channel, User}, use revolt_permissions::{calculate_server_permissions, ChannelPermission};
perms, Db, Error, Permission, Ref, Result, variables::delta::MAX_CHANNEL_COUNT, use revolt_result::{create_error, Result};
};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Deserialize, Serialize}; use rocket::State;
use ulid::Ulid;
use validator::Validate; use validator::Validate;
/// # Channel Type
#[derive(Serialize, Deserialize, JsonSchema, Default)]
enum ChannelType {
/// Text Channel
#[default]
Text,
/// Voice Channel
Voice,
}
/// # Channel Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataCreateChannel {
/// Channel type
#[serde(rename = "type", default = "ChannelType::default")]
channel_type: ChannelType,
/// Channel name
#[validate(length(min = 1, max = 32))]
name: String,
/// Channel description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Channel /// # Create Channel
/// ///
/// Create a new Text or Voice channel. /// Create a new Text or Voice channel.
#[openapi(tag = "Server Information")] #[openapi(tag = "Server Information")]
#[post("/<target>/channels", data = "<info>")] #[post("/<server>/channels", data = "<data>")]
pub async fn req( pub async fn create_server_channel(
db: &Db, db: &State<Database>,
user: User, user: User,
target: Ref, server: Reference,
info: Json<DataCreateChannel>, data: Json<v0::DataCreateServerChannel>,
) -> Result<Json<Channel>> { ) -> Result<Json<v0::Channel>> {
let info = info.into_inner(); let data = data.into_inner();
info.validate() data.validate().map_err(|error| {
.map_err(|error| Error::FailedValidation { error })?; create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut server = target.as_server(db).await?; let mut server = server.as_server(db).await?;
perms(&user) let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
.server(&server) calculate_server_permissions(&mut query)
.throw_permission(db, Permission::ManageChannel) .await
.await?; .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
if server.channels.len() > *MAX_CHANNEL_COUNT { Channel::create_server_channel(db, &mut server, data, true)
return Err(Error::TooManyChannels { max: *MAX_CHANNEL_COUNT }) .await
}; .map(|channel| channel.into())
.map(Json)
let id = Ulid::new().to_string();
let mut channels = server.channels.clone();
channels.push(id.clone());
let DataCreateChannel {
name,
description,
nsfw,
channel_type,
} = info;
let channel = match channel_type {
ChannelType::Text => Channel::TextChannel {
id,
server: server.id.clone(),
name,
description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
},
ChannelType::Voice => Channel::VoiceChannel {
id,
server: server.id.clone(),
name,
description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
},
};
channel.create(db).await?;
server
.update(
db,
PartialServer {
channels: Some(channels),
..Default::default()
},
vec![],
)
.await?;
Ok(Json(channel))
} }
+2 -2
View File
@@ -25,12 +25,12 @@ mod server_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) { pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![ openapi_get_routes_spec![
server_create::req, server_create::create_server,
server_delete::req, server_delete::req,
server_fetch::req, server_fetch::req,
server_edit::req, server_edit::req,
server_ack::req, server_ack::req,
channel_create::req, channel_create::create_server_channel,
member_fetch_all::req, member_fetch_all::req,
member_remove::req, member_remove::req,
member_fetch::req, member_fetch::req,
@@ -1,102 +1,39 @@
use std::collections::HashMap; use revolt_database::{Database, Member, Server, User};
use revolt_models::v0;
use revolt_quark::{ use revolt_result::{create_error, Result};
models::{Channel, Server, User},
variables::delta::MAX_SERVER_COUNT,
Db, Error, Result, DEFAULT_PERMISSION_SERVER,
};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Deserialize, Serialize}; use rocket::State;
use ulid::Ulid;
use validator::Validate; use validator::Validate;
/// # Server Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataCreateServer {
/// Server name
#[validate(length(min = 1, max = 32))]
name: String,
/// Server description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Whether this server is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Server Response
#[derive(Validate, Serialize, JsonSchema)]
pub struct CreateServerResponse {
/// Server object
server: Server,
/// Default channels
channels: Vec<Channel>,
}
/// # Create Server /// # Create Server
/// ///
/// Create a new server. /// Create a new server.
#[openapi(tag = "Server Information")] #[openapi(tag = "Server Information")]
#[post("/create", data = "<info>")] #[post("/create", data = "<data>")]
pub async fn req( pub async fn create_server(
db: &Db, db: &State<Database>,
user: User, user: User,
info: Json<DataCreateServer>, data: Json<v0::DataCreateServer>,
) -> Result<Json<CreateServerResponse>> { ) -> Result<Json<v0::CreateServerLegacyResponse>> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(create_error!(IsBot));
} }
let info = info.into_inner(); let data = data.into_inner();
info.validate() data.validate().map_err(|error| {
.map_err(|error| Error::FailedValidation { error })?; create_error!(FailedValidation {
error: error.to_string()
})
})?;
if !user.can_acquire_server(db).await? { user.can_acquire_server(db).await?;
return Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
});
}
let DataCreateServer { let (server, channels) = Server::create(db, data, &user, true).await?;
name, let channels = Member::create(db, &server, &user, Some(channels)).await?;
description,
nsfw,
} = info;
let channel_id = Ulid::new().to_string(); Ok(Json(v0::CreateServerLegacyResponse {
let server_id = Ulid::new().to_string(); server: server.into(),
channels: channels.into_iter().map(|channel| channel.into()).collect(),
let channel = Channel::TextChannel { }))
id: channel_id.clone(),
server: server_id.clone(),
name: "General".into(),
description: None,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
};
db.insert_channel(&channel).await?;
let server = Server {
id: server_id.clone(),
owner: user.id.clone(),
name,
description,
channels: vec![channel_id],
nsfw: nsfw.unwrap_or(false),
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
..Default::default()
};
server.create(db).await?;
let channels = server.create_member(db, user, Some(vec![channel])).await?;
Ok(Json(CreateServerResponse { server, channels }))
} }
+39 -14
View File
@@ -1,7 +1,8 @@
use futures::StreamExt; use futures::StreamExt;
use rand::Rng; use rand::Rng;
use redis_kiss::redis::aio::PubSub; use redis_kiss::redis::aio::PubSub;
use revolt_database::{events::client::EventV1, Database, DatabaseInfo, User}; use revolt_database::{events::client::EventV1, Database, User};
use revolt_models::v0;
use revolt_quark::authifier::{ use revolt_quark::authifier::{
models::{Account, Session}, models::{Account, Session},
Authifier, Authifier,
@@ -13,7 +14,7 @@ pub struct TestHarness {
authifier: Authifier, authifier: Authifier,
pub db: Database, pub db: Database,
sub: PubSub, sub: PubSub,
event_buffer: Vec<EventV1>, event_buffer: Vec<(String, EventV1)>,
} }
impl TestHarness { impl TestHarness {
@@ -24,20 +25,27 @@ impl TestHarness {
.await .await
.expect("valid rocket instance"); .expect("valid rocket instance");
let db = DatabaseInfo::Auto.connect().await.expect("`Database`");
let mut sub = redis_kiss::open_pubsub_connection() let mut sub = redis_kiss::open_pubsub_connection()
.await .await
.expect("`PubSub`"); .expect("`PubSub`");
sub.psubscribe("*").await.unwrap(); sub.psubscribe("*").await.unwrap();
let db = client
.rocket()
.state::<Database>()
.expect("`Database`")
.clone();
let authifier = client
.rocket()
.state::<Authifier>()
.expect("`Authifier`")
.clone();
TestHarness { TestHarness {
client, client,
authifier: Authifier { authifier,
database: db.clone().into(),
config: revolt_quark::util::authifier::config(),
event_channel: None,
},
db, db,
sub, sub,
event_buffer: vec![], event_buffer: vec![],
@@ -80,12 +88,12 @@ impl TestHarness {
(account, session, user) (account, session, user)
} }
pub async fn wait_for_event<F>(&mut self, predicate: F) -> EventV1 pub async fn wait_for_event<F>(&mut self, topic: &str, predicate: F) -> EventV1
where where
F: Fn(&EventV1) -> bool, F: Fn(&EventV1) -> bool,
{ {
for event in &self.event_buffer { for (msg_topic, event) in &self.event_buffer {
if predicate(event) { if topic == msg_topic && predicate(event) {
// does not remove from buffer // does not remove from buffer
return event.clone(); return event.clone();
} }
@@ -93,13 +101,15 @@ impl TestHarness {
let mut stream = self.sub.on_message(); let mut stream = self.sub.on_message();
while let Some(item) = stream.next().await { while let Some(item) = stream.next().await {
let payload: EventV1 = redis_kiss::decode_payload(&item.unwrap()).unwrap(); let item = item.unwrap();
let msg_topic = item.get_channel_name();
let payload: EventV1 = redis_kiss::decode_payload(&item).unwrap();
if predicate(&payload) { if topic == msg_topic && predicate(&payload) {
return payload; return payload;
} }
self.event_buffer.push(payload); self.event_buffer.push((msg_topic.to_string(), payload));
} }
// WARNING: if predicate is never satisfied, this will never return // WARNING: if predicate is never satisfied, this will never return
@@ -107,4 +117,19 @@ impl TestHarness {
unreachable!() unreachable!()
} }
pub async fn wait_for_message(&mut self, channel_id: &str) -> v0::Message {
dbg!(&self.event_buffer);
match self
.wait_for_event(channel_id, |event| match event {
EventV1::Message(v0::Message { channel, .. }) => channel == channel_id,
_ => false,
})
.await
{
EventV1::Message(message) => message,
_ => unreachable!(),
}
}
} }
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-quark" name = "revolt-quark"
version = "0.6.8" version = "0.6.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
@@ -88,7 +88,7 @@ rocket_empty = { version = "0.1.1", optional = true, features = ["schema"] }
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" } rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
# Authifier # Authifier
authifier = { version = "1.0.7", features = ["async-std-runtime"] } authifier = { version = "1.0.8", features = ["async-std-runtime"] }
# Sentry # Sentry
sentry = "0.31.5" sentry = "0.31.5"
+12 -11
View File
@@ -31,6 +31,18 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> { pub async fn connect(self) -> Result<Database, String> {
Ok(match self { Ok(match self {
DatabaseInfo::Auto => { DatabaseInfo::Auto => {
if let Ok(test_db) = env::var("TEST_DB") {
return match test_db.as_str() {
"REFERENCE" => DatabaseInfo::Dummy.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb(env::var("MONGODB").expect("`MONGODB` env"))
.connect()
.await
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
};
}
if let Ok(uri) = env::var("MONGODB") { if let Ok(uri) = env::var("MONGODB") {
return DatabaseInfo::MongoDb(uri).connect().await; return DatabaseInfo::MongoDb(uri).connect().await;
} }
@@ -61,17 +73,6 @@ impl Deref for Database {
} }
} }
impl From<Database> for authifier::Database {
fn from(val: Database) -> Self {
match val {
Database::Dummy(_) => authifier::Database::default(),
Database::MongoDb(MongoDb(client)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
}
}
}
impl From<Database> for revolt_database::Database { impl From<Database> for revolt_database::Database {
fn from(val: Database) -> Self { fn from(val: Database) -> Self {
match val { match val {
+2 -2
View File
@@ -13,12 +13,12 @@ pub mod process_embeds;
pub mod web_push; pub mod web_push;
/// Spawn background workers /// Spawn background workers
pub async fn start_workers(db: Database) { pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
for _ in 0..WORKER_COUNT { for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone())); task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone())); task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone())); task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(db.clone().into())); task::spawn(web_push::worker(authifier_db.clone()));
} }
} }
+5 -1
View File
@@ -21,10 +21,14 @@ in pkgs.mkShell {
# Database # Database
# pkgs.mongodb # pkgs.mongodb
# Rust # Cargo
pkgs.cargo pkgs.cargo
pkgs.cargo-nextest
# Rust
pkgs.rustc pkgs.rustc
pkgs.clippy pkgs.clippy
pkgs.rustfmt
pkgs.pkgconfig pkgs.pkgconfig
pkgs.openssl.dev pkgs.openssl.dev
]; ];