diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 8a1e862a..92368144 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -307,6 +307,13 @@ impl Channel { return Err(create_error!(AlreadyInGroup)); } + let config = config().await; + if recipients.len() >= config.features.limits.default.group_size { + return Err(create_error!(GroupTooLarge { + max: config.features.limits.default.group_size + })); + } + recipients.push(String::from(&user.id)); } @@ -696,6 +703,9 @@ impl Channel { pub async fn delete(&self, db: &Database) -> Result<()> { let id = self.id().to_string(); EventV1::ChannelDelete { id: id.clone() }.p(id).await; + // TODO: missing functionality: + // - group invites + // - channels list / categories list on server db.delete_channel(self).await } } diff --git a/crates/core/database/src/models/channels/ops/mongodb.rs b/crates/core/database/src/models/channels/ops/mongodb.rs index d631a7bc..9344d67b 100644 --- a/crates/core/database/src/models/channels/ops/mongodb.rs +++ b/crates/core/database/src/models/channels/ops/mongodb.rs @@ -1,6 +1,6 @@ use super::AbstractChannels; -use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel}; -use bson::Document; +use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel}; +use bson::{Bson, Document}; use futures::StreamExt; use revolt_permissions::OverrideField; use revolt_result::Result; @@ -188,6 +188,125 @@ impl AbstractChannels for MongoDb { // Delete a channel async fn delete_channel(&self, channel: &Channel) -> Result<()> { + let id = channel.id().to_string(); + let server_id = match channel { + Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { + Some(server) + } + _ => None, + }; + + // Delete invites and unreads. + self.delete_associated_channel_objects(Bson::String(id.to_string())) + .await?; + + // Delete messages. + self.delete_bulk_messages(doc! { + "channel": &id + }) + .await?; + + // Remove from server object. + if let Some(server) = server_id { + let server = self.fetch_server(server).await?; + let mut update = doc! { + "$pull": { + "channels": &id + } + }; + + if let Some(sys) = &server.system_messages { + let mut unset = doc! {}; + + if let Some(cid) = &sys.user_joined { + if &id == cid { + unset.insert("system_messages.user_joined", 1_i32); + } + } + + if let Some(cid) = &sys.user_left { + if &id == cid { + unset.insert("system_messages.user_left", 1_i32); + } + } + + if let Some(cid) = &sys.user_kicked { + if &id == cid { + unset.insert("system_messages.user_kicked", 1_i32); + } + } + + if let Some(cid) = &sys.user_banned { + if &id == cid { + unset.insert("system_messages.user_banned", 1_i32); + } + } + + if !unset.is_empty() { + update.insert("$unset", unset); + } + } + + self.col::("servers") + .update_one( + doc! { + "_id": server.id + }, + update, + None, + ) + .await + .map_err(|_| create_database_error!("update_one", "servers"))?; + } + + // Delete associated attachments + self.delete_many_attachments(doc! { + "object_id": &id + }) + .await?; + + // Delete the channel itself query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ()) } } + +impl MongoDb { + pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> { + // Delete all invites to these channels. + self.col::("channel_invites") + .delete_many( + doc! { + "channel": &id + }, + None, + ) + .await + .map_err(|_| create_database_error!("delete_many", "channel_invites"))?; + + // Delete unread message objects on channels. + self.col::("channel_unreads") + .delete_many( + doc! { + "_id.channel": &id + }, + None, + ) + .await + .map_err(|_| create_database_error!("delete_many", "channel_unreads")) + .map(|_| ())?; + + // update many attachments with parent id + + // Delete all webhooks on this channel. + self.col::("webhooks") + .delete_many( + doc! { + "channel": &id + }, + None, + ) + .await + .map_err(|_| create_database_error!("delete_many", "webhooks")) + .map(|_| ()) + } +} diff --git a/crates/core/database/src/models/files/ops/mongodb.rs b/crates/core/database/src/models/files/ops/mongodb.rs index 050c0857..f7c5960a 100644 --- a/crates/core/database/src/models/files/ops/mongodb.rs +++ b/crates/core/database/src/models/files/ops/mongodb.rs @@ -115,3 +115,21 @@ impl AbstractAttachments for MongoDb { .map_err(|_| create_database_error!("update_one", COL)) } } + +impl MongoDb { + pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> { + self.col::(COL) + .update_many( + projection, + doc! { + "$set": { + "deleted": true + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_many", COL)) + } +} diff --git a/crates/core/database/src/models/messages/ops/mongodb.rs b/crates/core/database/src/models/messages/ops/mongodb.rs index b15bd119..530fd2f6 100644 --- a/crates/core/database/src/models/messages/ops/mongodb.rs +++ b/crates/core/database/src/models/messages/ops/mongodb.rs @@ -4,7 +4,9 @@ use mongodb::options::FindOptions; use revolt_models::v0::MessageSort; use revolt_result::Result; -use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage}; +use crate::{ + AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage, +}; use super::AbstractMessages; @@ -293,3 +295,57 @@ impl AbstractMessages for MongoDb { .map_err(|_| create_database_error!("delete_many", COL)) } } + +impl MongoDb { + pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> { + let mut for_attachments = projection.clone(); + for_attachments.insert( + "attachments", + doc! { + "$exists": 1_i32 + }, + ); + + // Check if there are any attachments we need to delete. + let message_ids_with_attachments = self + .find_with_options::<_, DocumentId>( + COL, + for_attachments, + FindOptions::builder() + .projection(doc! { "_id": 1_i32 }) + .build(), + ) + .await + .map_err(|_| create_database_error!("find_many", "attachments"))? + .into_iter() + .map(|x| x.id) + .collect::>(); + + // If we found any, mark them as deleted. + if !message_ids_with_attachments.is_empty() { + self.col::("attachments") + .update_many( + doc! { + "message_id": { + "$in": message_ids_with_attachments + } + }, + doc! { + "$set": { + "deleted": true + } + }, + None, + ) + .await + .map_err(|_| create_database_error!("update_many", "attachments"))?; + } + + // And then delete said messages. + self.col::(COL) + .delete_many(projection, None) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("delete_many", COL)) + } +} diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index a86611e2..6beebe06 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -214,6 +214,8 @@ impl Server { .p(self.id.clone()) .await; + // TODO: missing functionality + db.delete_server(&self.id).await } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 547db3d1..4ed2c0b5 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -701,6 +701,77 @@ impl crate::User { } } + pub async fn into_known<'a, P>(self, perspective: P) -> User + where + P: Into>, + { + let perspective = perspective.into(); + let (relationship, can_see_profile) = if self.bot.is_some() { + (RelationshipStatus::None, true) + } else if let Some(perspective) = perspective { + if perspective.id == self.id { + (RelationshipStatus::User, true) + } else { + let relationship = perspective + .relations + .as_ref() + .map(|relations| { + relations + .iter() + .find(|relationship| relationship.id == self.id) + .map(|relationship| relationship.status.clone().into()) + .unwrap_or_default() + }) + .unwrap_or_default(); + + // TODO: refactor all of this to be less code? (as in with the method above) + // TODO: also not sure if this is the best solution, but we don't want to + // TODO: fetch mutual information again here + let can_see_profile = relationship == RelationshipStatus::Friend; + (relationship, can_see_profile) + } + } else { + (RelationshipStatus::None, false) + }; + + User { + username: self.username, + discriminator: self.discriminator, + display_name: self.display_name, + avatar: self.avatar.map(|file| file.into()), + 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, + status: if can_see_profile { + 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, + privileged: self.privileged, + bot: self.bot.map(|bot| bot.into()), + relationship, + online: can_see_profile && revolt_presence::is_online(&self.id).await, + id: self.id, + } + } + pub async fn into_self(self) -> User { User { username: self.username, diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index 4e8c4842..c3a6fd21 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -7,7 +7,7 @@ use schemars::{ JsonSchema, }; -use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook}; +use crate::{Bot, Channel, Database, Emoji, Invite, Message, Server, User, Webhook}; /// Reference to some object in the database #[derive(Serialize, Deserialize)] @@ -37,6 +37,11 @@ impl Reference { db.fetch_channel(&self.id).await } + /// Fetch invite from Ref + pub async fn as_invite(&self, db: &Database) -> Result { + db.fetch_invite(&self.id).await + } + /// Fetch message from Ref pub async fn as_message(&self, db: &Database) -> Result { db.fetch_message(&self.id).await diff --git a/crates/core/models/src/v0/channel_invites.rs b/crates/core/models/src/v0/channel_invites.rs index 0c007ae8..32a3d084 100644 --- a/crates/core/models/src/v0/channel_invites.rs +++ b/crates/core/models/src/v0/channel_invites.rs @@ -1,3 +1,5 @@ +use super::{Channel, File, Server, User}; + auto_derived!( /// Invite pub enum Invite { @@ -24,4 +26,76 @@ auto_derived!( channel: String, }, } + + /// Public invite response + #[allow(clippy::large_enum_variant)] + #[serde(tag = "type")] + pub enum InviteResponse { + /// Server channel invite + Server { + /// Invite code + code: String, + /// Id of the server + server_id: String, + /// Name of the server + server_name: String, + /// Attachment for server icon + #[serde(skip_serializing_if = "Option::is_none")] + server_icon: Option, + /// Attachment for server banner + #[serde(skip_serializing_if = "Option::is_none")] + server_banner: Option, + /// Enum of server flags + #[serde(skip_serializing_if = "Option::is_none")] + server_flags: Option, + /// Id of server channel + channel_id: String, + /// Name of server channel + channel_name: String, + /// Description of server channel + #[serde(skip_serializing_if = "Option::is_none")] + channel_description: Option, + /// Name of user who created the invite + user_name: String, + /// Avatar of the user who created the invite + #[serde(skip_serializing_if = "Option::is_none")] + user_avatar: Option, + /// Number of members in this server + member_count: i64, + }, + /// Group channel invite + Group { + /// Invite code + code: String, + /// Id of group channel + channel_id: String, + /// Name of group channel + channel_name: String, + /// Description of group channel + #[serde(skip_serializing_if = "Option::is_none")] + channel_description: Option, + /// Name of user who created the invite + user_name: String, + /// Avatar of the user who created the invite + #[serde(skip_serializing_if = "Option::is_none")] + user_avatar: Option, + }, + } + + /// Invite join response + #[serde(tag = "type")] + pub enum InviteJoinResponse { + Server { + /// Channels in the server + channels: Vec, + /// Server we are joining + server: Server, + }, + Group { + /// Group channel we are joining + channel: Channel, + /// Members of this group + users: Vec, + }, + } ); diff --git a/crates/delta/src/routes/channels/members_fetch.rs b/crates/delta/src/routes/channels/members_fetch.rs index b7815138..9b0cbf47 100644 --- a/crates/delta/src/routes/channels/members_fetch.rs +++ b/crates/delta/src/routes/channels/members_fetch.rs @@ -11,6 +11,8 @@ use rocket::{serde::json::Json, State}; /// # Fetch Group Members /// /// Retrieves all users who are part of this group. +/// +/// This may not return full user information if users are not friends but have mutual connections. #[openapi(tag = "Groups")] #[get("//members")] pub async fn fetch_members( @@ -30,7 +32,7 @@ pub async fn fetch_members( db.fetch_users(&recipients) .await? .into_iter() - .map(|other_user| other_user.into(db, &user)), + .map(|other_user| other_user.into_known(&user)), ) .await, )) diff --git a/crates/delta/src/routes/invites/invite_delete.rs b/crates/delta/src/routes/invites/invite_delete.rs index c2b15c10..a13c3953 100644 --- a/crates/delta/src/routes/invites/invite_delete.rs +++ b/crates/delta/src/routes/invites/invite_delete.rs @@ -1,14 +1,18 @@ -use revolt_quark::{ - models::{Invite, User}, - perms, Db, EmptyResponse, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, Invite, User, }; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::Result; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Delete Invite /// /// Delete an invite by its id. #[openapi(tag = "Invites")] #[delete("/")] -pub async fn req(db: &Db, user: User, target: Ref) -> Result { +pub async fn delete(db: &State, user: User, target: Reference) -> Result { let invite = target.as_invite(db).await?; if user.id == invite.creator() { @@ -17,10 +21,10 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result { match invite { Invite::Server { code, server, .. } => { let server = db.fetch_server(&server).await?; - perms(&user) - .server(&server) - .throw_permission(db, Permission::ManageServer) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?; db.delete_invite(&code).await } diff --git a/crates/delta/src/routes/invites/invite_fetch.rs b/crates/delta/src/routes/invites/invite_fetch.rs index 8eb3b439..a56c30bb 100644 --- a/crates/delta/src/routes/invites/invite_fetch.rs +++ b/crates/delta/src/routes/invites/invite_fetch.rs @@ -1,71 +1,14 @@ -use revolt_quark::models::{Channel, File, Invite}; -use revolt_quark::{Db, Ref, Result}; - -use rocket::serde::json::Json; -use serde::Serialize; - -/// # Invite -#[allow(clippy::large_enum_variant)] -#[derive(Serialize, Debug, Clone, JsonSchema)] -#[serde(tag = "type")] -pub enum InviteResponse { - /// Server channel invite - Server { - /// Invite code - code: String, - /// Id of the server - server_id: String, - /// Name of the server - server_name: String, - /// Attachment for server icon - #[serde(skip_serializing_if = "Option::is_none")] - server_icon: Option, - /// Attachment for server banner - #[serde(skip_serializing_if = "Option::is_none")] - server_banner: Option, - /// Enum of server flags - #[serde(skip_serializing_if = "Option::is_none")] - server_flags: Option, - /// Id of server channel - channel_id: String, - /// Name of server channel - channel_name: String, - /// Description of server channel - #[serde(skip_serializing_if = "Option::is_none")] - channel_description: Option, - /// Name of user who created the invite - user_name: String, - /// Avatar of the user who created the invite - #[serde(skip_serializing_if = "Option::is_none")] - user_avatar: Option, - /// Number of members in this server - member_count: i64, - }, - /// Group channel invite - Group { - /// Invite code - code: String, - /// Id of group channel - channel_id: String, - /// Name of group channel - channel_name: String, - /// Description of group channel - #[serde(skip_serializing_if = "Option::is_none")] - channel_description: Option, - /// Name of user who created the invite - user_name: String, - /// Avatar of the user who created the invite - #[serde(skip_serializing_if = "Option::is_none")] - user_avatar: Option, - }, -} +use revolt_database::{util::reference::Reference, Channel, Database, Invite}; +use revolt_models::v0; +use revolt_result::Result; +use rocket::{serde::json::Json, State}; /// # Fetch Invite /// /// Fetch an invite by its id. #[openapi(tag = "Invites")] #[get("/")] -pub async fn req(db: &Db, target: Ref) -> Result> { +pub async fn fetch(db: &State, target: Reference) -> Result> { Ok(Json(match target.as_invite(db).await? { Invite::Server { channel, creator, .. @@ -90,19 +33,19 @@ pub async fn req(db: &Db, target: Ref) -> Result> { } => { let server = db.fetch_server(&server).await?; - InviteResponse::Server { + v0::InviteResponse::Server { code: target.id, member_count: db.fetch_member_count(&server.id).await? as i64, server_id: server.id, server_name: server.name, - server_icon: server.icon, - server_banner: server.banner, + server_icon: server.icon.map(|f| f.into()), + server_banner: server.banner.map(|f| f.into()), server_flags: server.flags, channel_id: id, channel_name: name, channel_description: description, user_name: user.username, - user_avatar: user.avatar, + user_avatar: user.avatar.map(|f| f.into()), } } _ => unreachable!(), @@ -120,13 +63,13 @@ pub async fn req(db: &Db, target: Ref) -> Result> { name, description, .. - } => InviteResponse::Group { + } => v0::InviteResponse::Group { code: target.id, channel_id: id, channel_name: name, channel_description: description, user_name: user.username, - user_avatar: user.avatar, + user_avatar: user.avatar.map(|f| f.into()), }, _ => unreachable!(), } diff --git a/crates/delta/src/routes/invites/invite_join.rs b/crates/delta/src/routes/invites/invite_join.rs index 36e4886a..b558948e 100644 --- a/crates/delta/src/routes/invites/invite_join.rs +++ b/crates/delta/src/routes/invites/invite_join.rs @@ -1,47 +1,54 @@ -use revolt_quark::{ - models::{Channel, Invite, Server, User}, - variables::delta::MAX_SERVER_COUNT, - Db, Error, Ref, Result, -}; - -use rocket::serde::json::Json; -use serde::Serialize; - -/// # Join Response -#[derive(Serialize, JsonSchema)] -#[serde(tag = "type")] -pub enum InviteJoinResponse { - Server { - /// Channels in the server - channels: Vec, - /// Server we are joining - server: Server, - }, -} +use futures::future::join_all; +use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User}; +use revolt_models::v0::{self, InviteJoinResponse}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; /// # Join Invite /// -/// Join an invite by its ID. +/// Join an invite by its ID #[openapi(tag = "Invites")] #[post("/")] -pub async fn req(db: &Db, user: User, target: Ref) -> Result> { +pub async fn join( + db: &State, + user: User, + target: Reference, +) -> Result> { if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } - if !user.can_acquire_server(db).await? { - return Err(Error::TooManyServers { - max: *MAX_SERVER_COUNT, - }); - } + user.can_acquire_server(db).await?; let invite = target.as_invite(db).await?; match &invite { Invite::Server { server, .. } => { let server = db.fetch_server(server).await?; - let channels = server.create_member(db, user, None).await?; - Ok(Json(InviteJoinResponse::Server { channels, server })) + let channels = Member::create(db, &server, &user, None).await?; + Ok(Json(InviteJoinResponse::Server { + channels: channels.into_iter().map(|c| c.into()).collect(), + server: server.into(), + })) + } + Invite::Group { + channel, creator, .. + } => { + let mut channel = db.fetch_channel(channel).await?; + channel.add_user_to_group(db, &user, creator).await?; + if let Channel::Group { recipients, .. } = &channel { + Ok(Json(InviteJoinResponse::Group { + users: join_all( + db.fetch_users(&recipients) + .await? + .into_iter() + .map(|other_user| other_user.into_known(&user)), + ) + .await, + channel: channel.into(), + })) + } else { + unreachable!() + } } - _ => unreachable!(), } } diff --git a/crates/delta/src/routes/invites/mod.rs b/crates/delta/src/routes/invites/mod.rs index 7d64a7d9..00e6fa39 100644 --- a/crates/delta/src/routes/invites/mod.rs +++ b/crates/delta/src/routes/invites/mod.rs @@ -6,5 +6,9 @@ mod invite_fetch; mod invite_join; pub fn routes() -> (Vec, OpenApi) { - openapi_get_routes_spec![invite_fetch::req, invite_join::req, invite_delete::req] + openapi_get_routes_spec![ + invite_fetch::fetch, + invite_join::join, + invite_delete::delete + ] }