diff --git a/set_version.sh b/set_version.sh index 7f975188..0df975c7 100755 --- a/set_version.sh +++ b/set_version.sh @@ -1,3 +1,3 @@ #!/bin/bash -export version=0.4.2-alpha.3 +export version=0.5.0-alpha.0 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs diff --git a/src/database/entities/microservice/mod.rs b/src/database/entities/microservice/mod.rs index 334ed6f0..da3f42a0 100644 --- a/src/database/entities/microservice/mod.rs +++ b/src/database/entities/microservice/mod.rs @@ -1,2 +1,2 @@ pub mod autumn; -pub mod january; \ No newline at end of file +pub mod january; diff --git a/src/database/entities/mod.rs b/src/database/entities/mod.rs index 358b2ea4..4e6cc7be 100644 --- a/src/database/entities/mod.rs +++ b/src/database/entities/mod.rs @@ -1,9 +1,9 @@ -mod microservice; mod channel; mod message; +mod microservice; mod server; -mod user; mod sync; +mod user; use microservice::*; @@ -12,5 +12,5 @@ pub use channel::*; pub use january::*; pub use message::*; pub use server::*; -pub use user::*; pub use sync::*; +pub use user::*; diff --git a/src/database/entities/server.rs b/src/database/entities/server.rs index 51809537..236bb5d0 100644 --- a/src/database/entities/server.rs +++ b/src/database/entities/server.rs @@ -1,8 +1,15 @@ +use crate::database::*; +use crate::notifications::events::ClientboundNotification; +use crate::util::result::{Error, Result}; +use mongodb::bson::doc; +use mongodb::bson::from_document; +use mongodb::bson::to_document; +use rocket_contrib::json::JsonValue; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MemberCompositeKey { - pub guild: String, + pub server: String, pub user: String, } @@ -34,9 +41,67 @@ pub struct Server { pub nonce: Option, pub owner: String, + pub name: String, + // pub default_permissions: u32, pub channels: Vec, - pub invites: Vec, - pub bans: Vec, - - pub default_permissions: u32, + // pub invites: Vec, + // pub bans: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub banner: Option, +} + +impl Server { + pub async fn get(id: &str) -> Result { + let doc = get_collection("servers") + .find_one(doc! { "_id": id }, None) + .await + .map_err(|_| Error::DatabaseError { + operation: "find_one", + with: "server", + })? + .ok_or_else(|| Error::UnknownServer)?; + + from_document::(doc).map_err(|_| Error::DatabaseError { + operation: "from_document", + with: "server", + }) + } + + pub async fn publish(self) -> Result<()> { + get_collection("servers") + .insert_one( + to_document(&self).map_err(|_| Error::DatabaseError { + operation: "to_bson", + with: "channel", + })?, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "insert_one", + with: "server", + })?; + + let server_id = self.id.clone(); + ClientboundNotification::ServerCreate(self).publish(server_id); + + Ok(()) + } + + pub async fn publish_update(&self, data: JsonValue) -> Result<()> { + ClientboundNotification::ServerUpdate { + id: self.id.clone(), + data, + clear: None, + } + .publish(self.id.clone()); + + Ok(()) + } + + pub async fn delete(&self) -> Result<()> { + unimplemented!() + } } diff --git a/src/database/guards/reference.rs b/src/database/guards/reference.rs index 65e0dfbe..1c374976 100644 --- a/src/database/guards/reference.rs +++ b/src/database/guards/reference.rs @@ -47,6 +47,10 @@ impl Ref { self.fetch("channels").await } + pub async fn fetch_server(&self) -> Result { + self.fetch("servers").await + } + pub async fn fetch_message(&self, channel: &Channel) -> Result { let message: Message = self.fetch("messages").await?; if &message.channel != channel.id() { diff --git a/src/database/migrations/init.rs b/src/database/migrations/init.rs index d089c0d8..d08971f8 100644 --- a/src/database/migrations/init.rs +++ b/src/database/migrations/init.rs @@ -29,6 +29,18 @@ pub async fn create_database() { .await .expect("Failed to create servers collection."); + db.create_collection("server_members", None) + .await + .expect("Failed to create server_members collection."); + + db.create_collection("server_bans", None) + .await + .expect("Failed to create server_bans collection."); + + db.create_collection("invites", None) + .await + .expect("Failed to create invites collection."); + db.create_collection("migrations", None) .await .expect("Failed to create migrations collection."); diff --git a/src/database/migrations/scripts.rs b/src/database/migrations/scripts.rs index 8e28d7d7..0186353b 100644 --- a/src/database/migrations/scripts.rs +++ b/src/database/migrations/scripts.rs @@ -14,7 +14,7 @@ struct MigrationInfo { revision: i32, } -pub const LATEST_REVISION: i32 = 4; +pub const LATEST_REVISION: i32 = 5; pub async fn migrate_database() { let migrations = get_collection("migrations"); @@ -138,6 +138,25 @@ pub async fn run_migrations(revision: i32) -> i32 { .expect("Failed to create user_settings collection."); } + if revision <= 4 { + info!("Running migration [revision 4 / 2021-06-01]: Add more server collections."); + + get_db() + .create_collection("server_members", None) + .await + .expect("Failed to create server_members collection."); + + get_db() + .create_collection("server_bans", None) + .await + .expect("Failed to create server_bans collection."); + + get_db() + .create_collection("invites", None) + .await + .expect("Failed to create invites collection."); + } + // Reminder to update LATEST_REVISION when adding new migrations. LATEST_REVISION } diff --git a/src/database/permissions/mod.rs b/src/database/permissions/mod.rs index 61644789..376a4408 100644 --- a/src/database/permissions/mod.rs +++ b/src/database/permissions/mod.rs @@ -11,6 +11,7 @@ pub struct PermissionCalculator<'a> { user: Option<&'a User>, relationship: Option<&'a RelationshipStatus>, channel: Option<&'a Channel>, + server: Option<&'a Server>, has_mutual_connection: bool, } @@ -23,6 +24,7 @@ impl<'a> PermissionCalculator<'a> { user: None, relationship: None, channel: None, + server: None, has_mutual_connection: false, } @@ -49,6 +51,13 @@ impl<'a> PermissionCalculator<'a> { } } + pub fn with_server(self, server: &'a Server) -> PermissionCalculator { + PermissionCalculator { + server: Some(&server), + ..self + } + } + pub fn with_mutual_connection(self) -> PermissionCalculator<'a> { PermissionCalculator { has_mutual_connection: true, diff --git a/src/database/permissions/server.rs b/src/database/permissions/server.rs new file mode 100644 index 00000000..94166d70 --- /dev/null +++ b/src/database/permissions/server.rs @@ -0,0 +1,42 @@ +use crate::database::*; +use crate::util::result::Result; + +use super::PermissionCalculator; + +use num_enum::TryFromPrimitive; +use std::ops; + +#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)] +#[repr(u32)] +pub enum ServerPermission { + View = 1, +} + +impl_op_ex!(+ |a: &ServerPermission, b: &ServerPermission| -> u32 { *a as u32 | *b as u32 }); +impl_op_ex_commutative!(+ |a: &u32, b: &ServerPermission| -> u32 { *a | *b as u32 }); + +bitfield! { + pub struct ServerPermissions(MSB0 [u32]); + u32; + pub get_view, _: 31; +} + +impl<'a> PermissionCalculator<'a> { + pub async fn calculate_server(self) -> Result { + let server = if let Some(server) = self.server { + server + } else { + unreachable!() + }; + + if &self.perspective.id == server.owner { + Ok(u32::MAX) + } else { + Ok(ServerPermission::View as u32) + } + } + + pub async fn for_server(self) -> Result> { + Ok(ServerPermissions([self.calculate_server().await?])) + } +} diff --git a/src/notifications/events.rs b/src/notifications/events.rs index 2f961077..4e401cf9 100644 --- a/src/notifications/events.rs +++ b/src/notifications/events.rs @@ -39,6 +39,12 @@ pub enum RemoveChannelField { Icon, } +#[derive(Serialize, Deserialize, Debug)] +pub enum RemoveServerField { + Icon, + Banner, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type")] pub enum ClientboundNotification { @@ -46,6 +52,7 @@ pub enum ClientboundNotification { Authenticated, Ready { users: Vec, + servers: Vec, channels: Vec, }, @@ -67,6 +74,9 @@ pub enum ClientboundNotification { #[serde(skip_serializing_if = "Option::is_none")] clear: Option, }, + ChannelDelete { + id: String, + }, ChannelGroupJoin { id: String, user: String, @@ -75,9 +85,6 @@ pub enum ClientboundNotification { id: String, user: String, }, - ChannelDelete { - id: String, - }, ChannelStartTyping { id: String, user: String, @@ -87,6 +94,25 @@ pub enum ClientboundNotification { user: String, }, + ServerCreate(Server), + ServerUpdate { + id: String, + data: JsonValue, + #[serde(skip_serializing_if = "Option::is_none")] + clear: Option, + }, + ServerDelete { + id: String, + }, + ServerMemberJoin { + id: String, + user: String, + }, + ServerMemberLeave { + id: String, + user: String, + }, + UserUpdate { id: String, data: JsonValue, @@ -104,8 +130,8 @@ pub enum ClientboundNotification { }, UserSettingsUpdate { id: String, - update: JsonValue - } + update: JsonValue, + }, } impl ClientboundNotification { @@ -137,6 +163,12 @@ pub fn prehandle_hook(notification: &ClientboundNotification) { } } } + ClientboundNotification::ServerMemberJoin { id, user } => { + subscribe_if_exists(user.clone(), id.clone()).ok(); + } + ClientboundNotification::ServerCreate(server) => { + subscribe_if_exists(server.owner.clone(), server.id.clone()).ok(); + } ClientboundNotification::UserRelationship { id, user, status } => { if status != &RelationshipStatus::None { subscribe_if_exists(id.clone(), user.id.clone()).ok(); @@ -151,6 +183,21 @@ pub fn posthandle_hook(notification: &ClientboundNotification) { ClientboundNotification::ChannelDelete { id } => { get_hive().hive.drop_topic(&id).ok(); } + ClientboundNotification::ChannelGroupLeave { id, user } => { + get_hive() + .hive + .unsubscribe(&user.to_string(), &id.to_string()) + .ok(); + } + ClientboundNotification::ServerDelete { id } => { + get_hive().hive.drop_topic(&id).ok(); + } + ClientboundNotification::ServerMemberLeave { id, user } => { + get_hive() + .hive + .unsubscribe(&user.to_string(), &id.to_string()) + .ok(); + } ClientboundNotification::UserRelationship { id, user, status } => { if status == &RelationshipStatus::None { get_hive() @@ -159,12 +206,6 @@ pub fn posthandle_hook(notification: &ClientboundNotification) { .ok(); } } - ClientboundNotification::ChannelGroupLeave { id, user } => { - get_hive() - .hive - .unsubscribe(&user.to_string(), &id.to_string()) - .ok(); - } _ => {} } } diff --git a/src/notifications/payload.rs b/src/notifications/payload.rs index 12e49b59..84daf3ee 100644 --- a/src/notifications/payload.rs +++ b/src/notifications/payload.rs @@ -6,7 +6,7 @@ use crate::{ util::result::{Error, Result}, }; use futures::StreamExt; -use mongodb::bson::{doc, from_document}; +use mongodb::bson::{doc, from_document, Document}; pub async fn generate_ready(mut user: User) -> Result { let mut user_ids: HashSet = HashSet::new(); @@ -19,10 +19,68 @@ pub async fn generate_ready(mut user: User) -> Result { ); } + let server_ids = get_collection("server_members") + .find( + doc! { + "_id.user": &user.id + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "find", + with: "server_members", + })? + .filter_map(async move |s| s.ok()) + .collect::>() + .await + .into_iter() + .filter_map(|x| { + x.get_document("_id") + .ok() + .map(|i| i.get_str("server").ok().map(|x| x.to_string())) + }) + .flatten() + .collect::>(); + + let mut cursor = get_collection("servers") + .find( + doc! { + "_id": { + "$in": server_ids + } + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "find", + with: "servers", + })?; + + let mut servers = vec![]; + let mut channel_ids = vec![]; + while let Some(result) = cursor.next().await { + if let Ok(doc) = result { + let server: Server = from_document(doc).map_err(|_| Error::DatabaseError { + operation: "from_document", + with: "server", + })?; + + channel_ids.extend(server.channels.iter().cloned()); + servers.push(server); + } + } + let mut cursor = get_collection("channels") .find( doc! { "$or": [ + { + "_id": { + "$in": channel_ids + } + }, { "channel_type": "SavedMessages", "user": &user.id @@ -75,5 +133,9 @@ pub async fn generate_ready(mut user: User) -> Result { user.online = Some(true); users.push(user); - Ok(ClientboundNotification::Ready { users, channels }) + Ok(ClientboundNotification::Ready { + users, + servers, + channels, + }) } diff --git a/src/notifications/subscriptions.rs b/src/notifications/subscriptions.rs index 6ef88cd2..df07afe3 100644 --- a/src/notifications/subscriptions.rs +++ b/src/notifications/subscriptions.rs @@ -45,5 +45,7 @@ pub async fn generate_subscriptions(user: &User) -> Result<(), String> { } } + // ! FIXME: fetch memberships for servers + Ok(()) } diff --git a/src/notifications/websocket.rs b/src/notifications/websocket.rs index d9d8e70a..6e275e1a 100644 --- a/src/notifications/websocket.rs +++ b/src/notifications/websocket.rs @@ -243,8 +243,8 @@ pub fn publish(ids: Vec, notification: ClientboundNotification) { for id in ids { // Block certain notifications from reaching users that aren't meant to see them. match ¬ification { - ClientboundNotification::UserRelationship { id: user_id, .. } | - ClientboundNotification::UserSettingsUpdate { id: user_id, .. } => { + ClientboundNotification::UserRelationship { id: user_id, .. } + | ClientboundNotification::UserSettingsUpdate { id: user_id, .. } => { if &id != user_id { continue; } diff --git a/src/routes/guild/mod.rs b/src/routes/guild/mod.rs deleted file mode 100644 index 018a956f..00000000 --- a/src/routes/guild/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -use rocket::Route; - -pub fn routes() -> Vec { - routes![] -} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 4a038d94..557f9c7e 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -3,12 +3,12 @@ pub use rocket::response::Redirect; use rocket::Rocket; mod channels; -mod guild; mod onboard; mod push; mod root; -mod users; +mod servers; mod sync; +mod users; pub fn mount(rocket: Rocket) -> Rocket { rocket @@ -16,7 +16,7 @@ pub fn mount(rocket: Rocket) -> Rocket { .mount("/onboard", onboard::routes()) .mount("/users", users::routes()) .mount("/channels", channels::routes()) - .mount("/guild", guild::routes()) + .mount("/servers", servers::routes()) .mount("/push", push::routes()) .mount("/sync", sync::routes()) } diff --git a/src/routes/servers/mod.rs b/src/routes/servers/mod.rs new file mode 100644 index 00000000..74d3b848 --- /dev/null +++ b/src/routes/servers/mod.rs @@ -0,0 +1,8 @@ +use rocket::Route; + +mod server_create; +mod server_delete; + +pub fn routes() -> Vec { + routes![server_create::req, server_delete::req] +} diff --git a/src/routes/servers/server_create.rs b/src/routes/servers/server_create.rs new file mode 100644 index 00000000..96a2f8c0 --- /dev/null +++ b/src/routes/servers/server_create.rs @@ -0,0 +1,73 @@ +use crate::database::*; +use crate::util::result::{Error, Result}; + +use mongodb::bson::doc; +use rocket_contrib::json::{Json, JsonValue}; +use serde::{Deserialize, Serialize}; +use ulid::Ulid; +use validator::Validate; + +#[derive(Validate, Serialize, Deserialize)] +pub struct Data { + #[validate(length(min = 1, max = 32))] + name: String, + // Maximum length of 36 allows both ULIDs and UUIDs. + #[validate(length(min = 1, max = 36))] + nonce: String, +} + +#[post("/create", data = "")] +pub async fn req(user: User, info: Json) -> Result { + info.validate() + .map_err(|error| Error::FailedValidation { error })?; + + if get_collection("servers") + .find_one( + doc! { + "nonce": &info.nonce + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "find_one", + with: "server", + })? + .is_some() + { + Err(Error::DuplicateNonce)? + } + + let id = Ulid::new().to_string(); + let server = Server { + id: id.clone(), + nonce: Some(info.nonce.clone()), + owner: user.id.clone(), + + name: info.name.clone(), + channels: vec![], + + icon: None, + banner: None, + }; + + get_collection("server_members") + .insert_one( + doc! { + "_id": { + "server": id, + "user": user.id + } + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "insert_one", + with: "server_members", + })?; + + server.clone().publish().await?; + + Ok(json!(server)) +} diff --git a/src/routes/servers/server_delete.rs b/src/routes/servers/server_delete.rs new file mode 100644 index 00000000..91d8ec7f --- /dev/null +++ b/src/routes/servers/server_delete.rs @@ -0,0 +1,19 @@ +use crate::util::result::{Error, Result}; +use crate::database::*; + +use mongodb::bson::doc; + +#[delete("/")] +pub async fn req(user: User, target: Ref) -> Result<()> { + let target = target.fetch_server().await?; + + /*let perm = permissions::PermissionCalculator::new(&user) + .with_channel(&target) + .for_channel() + .await?; + if !perm.get_view() { + Err(Error::MissingPermission)? + }*/ + + target.delete().await +} diff --git a/src/routes/sync/get_settings.rs b/src/routes/sync/get_settings.rs index b9126371..582e49c6 100644 --- a/src/routes/sync/get_settings.rs +++ b/src/routes/sync/get_settings.rs @@ -2,13 +2,13 @@ use crate::database::*; use crate::util::result::{Error, Result}; use mongodb::bson::doc; -use serde::{Deserialize, Serialize}; use mongodb::options::FindOneOptions; use rocket_contrib::json::{Json, JsonValue}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct Options { - keys: Vec + keys: Vec, } #[post("/settings/fetch", data = "")] @@ -27,14 +27,16 @@ pub async fn req(user: User, options: Json) -> Result { doc! { "_id": user.id }, - FindOneOptions::builder() - .projection(projection) - .build() + FindOneOptions::builder().projection(projection).build(), ) .await - .map_err(|_| Error::DatabaseError { operation: "find_one", with: "user_settings" })? { + .map_err(|_| Error::DatabaseError { + operation: "find_one", + with: "user_settings", + })? + { Ok(json!(doc)) } else { - Ok(json!({ })) + Ok(json!({})) } } diff --git a/src/routes/sync/mod.rs b/src/routes/sync/mod.rs index 0b1ecf64..872fef1d 100644 --- a/src/routes/sync/mod.rs +++ b/src/routes/sync/mod.rs @@ -4,8 +4,5 @@ mod get_settings; mod set_settings; pub fn routes() -> Vec { - routes![ - get_settings::req, - set_settings::req - ] + routes![get_settings::req, set_settings::req] } diff --git a/src/routes/sync/set_settings.rs b/src/routes/sync/set_settings.rs index f25405c3..cbff28e7 100644 --- a/src/routes/sync/set_settings.rs +++ b/src/routes/sync/set_settings.rs @@ -3,12 +3,12 @@ use crate::notifications::events::ClientboundNotification; use crate::util::result::{Error, Result}; use chrono::prelude::*; -use rocket::request::Form; -use std::collections::HashMap; -use rocket_contrib::json::Json; use mongodb::bson::{doc, to_bson}; -use serde::{Serialize, Deserialize}; use mongodb::options::UpdateOptions; +use rocket::request::Form; +use rocket_contrib::json::Json; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; type Data = HashMap; @@ -35,10 +35,10 @@ pub async fn req(user: User, data: Json, options: Form) -> Result for (key, data) in &data { set.insert( key.clone(), - vec! [ + vec![ to_bson(×tamp).unwrap(), - to_bson(&data.clone()).unwrap() - ] + to_bson(&data.clone()).unwrap(), + ], ); } @@ -51,19 +51,20 @@ pub async fn req(user: User, data: Json, options: Form) -> Result doc! { "$set": &set }, - UpdateOptions::builder() - .upsert(true) - .build() + UpdateOptions::builder().upsert(true).build(), ) .await - .map_err(|_| Error::DatabaseError { operation: "update_one", with: "user_settings" })?; + .map_err(|_| Error::DatabaseError { + operation: "update_one", + with: "user_settings", + })?; } ClientboundNotification::UserSettingsUpdate { id: user.id.clone(), - update: json!(set) + update: json!(set), } .publish(user.id); - + Ok(()) } diff --git a/src/util/result.rs b/src/util/result.rs index 8b39c954..c00dca4a 100644 --- a/src/util/result.rs +++ b/src/util/result.rs @@ -35,6 +35,9 @@ pub enum Error { AlreadyInGroup, NotInGroup, + // ? Server related errors. + UnknownServer, + // ? General errors. TooManyIds, FailedValidation { @@ -80,6 +83,8 @@ impl<'r> Responder<'r, 'static> for Error { Error::AlreadyInGroup => Status::Conflict, Error::NotInGroup => Status::NotFound, + Error::UnknownServer => Status::NotFound, + Error::FailedValidation { .. } => Status::UnprocessableEntity, Error::DatabaseError { .. } => Status::InternalServerError, Error::InternalError => Status::InternalServerError, diff --git a/src/version.rs b/src/version.rs index d3ed56bd..ab774c50 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "0.4.2-alpha.3"; +pub const VERSION: &str = "0.5.0-alpha.0";