From 54a4eff623717f0234b3f31047251779778377dc Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 7 Apr 2024 15:41:43 +0100 Subject: [PATCH] refactor(quark): port channel_ack, channel_delete, channel_ edit, group_remove_member, invite_create, members_fetch, message_bulk_delete, message_clear_reactions, message_delete #283 --- Cargo.lock | 1 + .../src/models/channel_invites/model.rs | 18 +-- .../database/src/models/channels/model.rs | 29 ++++- .../database/src/models/messages/model.rs | 58 ++++++++++ .../core/database/src/models/messages/ops.rs | 3 + .../src/models/messages/ops/mongodb.rs | 15 +++ .../src/models/messages/ops/reference.rs | 6 + crates/core/database/src/util/bridge/v0.rs | 7 ++ crates/core/database/src/util/mod.rs | 2 - crates/core/database/src/util/reference.rs | 10 ++ .../core/database/src/util/test_fixtures.rs | 2 +- crates/core/models/Cargo.toml | 6 +- crates/core/models/src/v0/channels.rs | 10 ++ crates/core/models/src/v0/messages.rs | 32 ++++-- crates/delta/Cargo.toml | 6 +- .../delta/src/routes/channels/channel_ack.rs | 75 ++++++++++-- .../src/routes/channels/channel_delete.rs | 94 ++++++++++----- .../delta/src/routes/channels/channel_edit.rs | 108 ++++++++---------- .../src/routes/channels/channel_fetch.rs | 2 +- .../src/routes/channels/group_add_member.rs | 2 +- .../routes/channels/group_remove_member.rs | 31 +++-- .../src/routes/channels/invite_create.rs | 32 ++++-- .../src/routes/channels/members_fetch.rs | 41 ++++--- .../routes/channels/message_bulk_delete.rs | 49 ++++---- .../channels/message_clear_reactions.rs | 27 +++-- .../src/routes/channels/message_delete.rs | 30 +++-- crates/delta/src/routes/channels/mod.rs | 20 ++-- 27 files changed, 500 insertions(+), 216 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31fc617d..25cee682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3712,6 +3712,7 @@ dependencies = [ "revolt-config", "revolt-permissions", "revolt_optional_struct", + "rocket", "schemars", "serde", "validator 0.16.0", diff --git a/crates/core/database/src/models/channel_invites/model.rs b/crates/core/database/src/models/channel_invites/model.rs index 48603e52..aaaff55e 100644 --- a/crates/core/database/src/models/channel_invites/model.rs +++ b/crates/core/database/src/models/channel_invites/model.rs @@ -1,12 +1,12 @@ use revolt_result::{create_error, Result}; -use crate::Database; +use crate::{Channel, Database, User}; -/* static ALPHABET: [char; 54] = [ +static ALPHABET: [char; 54] = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z', -]; */ +]; auto_derived!( /// Invite @@ -56,9 +56,13 @@ impl Invite { } /// Create a new invite from given information - /*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result { + pub async fn create_channel_invite( + db: &Database, + creator: &User, + channel: &Channel, + ) -> Result { let code = nanoid::nanoid!(8, &ALPHABET); - let invite = match &target { + let invite = match &channel { Channel::Group { id, .. } => Ok(Invite::Group { code, creator: creator.id.clone(), @@ -72,12 +76,12 @@ impl Invite { channel: id.clone(), }) } - _ => Err(Error::InvalidOperation), + _ => Err(create_error!(InvalidOperation)), }?; db.insert_invite(&invite).await?; Ok(invite) - }*/ + } /// Resolve an invite by its ID or by a public server ID pub async fn find(db: &Database, code: &str) -> Result { diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 8f04a0aa..8a1e862a 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -7,8 +7,8 @@ use revolt_result::Result; use serde::{Deserialize, Serialize}; use crate::{ - events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server, - SystemMessage, User, + events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer, + Server, SystemMessage, User, }; auto_derived!( @@ -582,6 +582,28 @@ impl Channel { } } + /// Acknowledge a message + pub async fn ack(&self, user: &str, message: &str) -> Result<()> { + EventV1::ChannelAck { + id: self.id().to_string(), + user: user.to_string(), + message_id: message.to_string(), + } + .private(user.to_string()) + .await; + + crate::tasks::ack::queue( + self.id().to_string(), + user.to_string(), + AckEvent::AckMessage { + id: message.to_string(), + }, + ) + .await; + + Ok(()) + } + /// Remove user from a group pub async fn remove_user_from_group( &self, @@ -627,8 +649,7 @@ impl Channel { .await .ok(); } else { - db.delete_channel(self).await?; - return Ok(()); + return self.delete(db).await; } } diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index 1973aac1..d8be9c92 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -441,6 +441,22 @@ impl Message { Ok(()) } + /// Update message data + pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> { + self.apply_options(partial.clone()); + db.update_message(&self.id, &partial).await?; + + EventV1::MessageUpdate { + id: self.id.clone(), + channel: self.channel.clone(), + data: partial.into(), + } + .p(self.channel.clone()) + .await; + + Ok(()) + } + /// Append content to message pub async fn append( db: &Database, @@ -518,6 +534,48 @@ impl Message { Err(create_error!(PayloadTooLarge)) } } + + /// Delete a message + pub async fn delete(self, db: &Database) -> Result<()> { + let file_ids: Vec = self + .attachments + .map(|files| files.iter().map(|file| file.id.to_string()).collect()) + .unwrap_or_default(); + + if !file_ids.is_empty() { + db.mark_attachments_as_deleted(&file_ids).await?; + } + + db.delete_message(&self.id).await?; + + EventV1::MessageDelete { + id: self.id, + channel: self.channel.clone(), + } + .p(self.channel) + .await; + Ok(()) + } + + /// Bulk delete messages + pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec) -> Result<()> { + let valid_ids = db + .fetch_messages_by_id(&ids) + .await? + .into_iter() + .filter(|msg| msg.channel == channel) + .map(|msg| msg.id) + .collect::>(); + + db.delete_messages(channel, &valid_ids).await?; + EventV1::BulkMessageDelete { + channel: channel.to_string(), + ids: valid_ids, + } + .p(channel.to_string()) + .await; + Ok(()) + } } impl SystemMessage { diff --git a/crates/core/database/src/models/messages/ops.rs b/crates/core/database/src/models/messages/ops.rs index 7d5ec2ef..c89caf9b 100644 --- a/crates/core/database/src/models/messages/ops.rs +++ b/crates/core/database/src/models/messages/ops.rs @@ -16,6 +16,9 @@ pub trait AbstractMessages: Sync + Send { /// Fetch multiple messages by given query async fn fetch_messages(&self, query: MessageQuery) -> Result>; + /// Fetch multiple messages by given IDs + async fn fetch_messages_by_id(&self, ids: &[String]) -> Result>; + /// Update a given message with new information async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>; diff --git a/crates/core/database/src/models/messages/ops/mongodb.rs b/crates/core/database/src/models/messages/ops/mongodb.rs index 42c2f270..b15bd119 100644 --- a/crates/core/database/src/models/messages/ops/mongodb.rs +++ b/crates/core/database/src/models/messages/ops/mongodb.rs @@ -159,6 +159,21 @@ impl AbstractMessages for MongoDb { } } + /// Fetch multiple messages by given IDs + async fn fetch_messages_by_id(&self, ids: &[String]) -> Result> { + self.find_with_options( + COL, + doc! { + "ids": { + "$in": ids + } + }, + None, + ) + .await + .map_err(|_| create_database_error!("find", COL)) + } + /// Update a given message with new information async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> { query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ()) diff --git a/crates/core/database/src/models/messages/ops/reference.rs b/crates/core/database/src/models/messages/ops/reference.rs index 63c7cea8..80e024ee 100644 --- a/crates/core/database/src/models/messages/ops/reference.rs +++ b/crates/core/database/src/models/messages/ops/reference.rs @@ -1,3 +1,4 @@ +use futures::future::try_join_all; use indexmap::IndexSet; use revolt_result::Result; @@ -176,6 +177,11 @@ impl AbstractMessages for ReferenceDb { }*/ } + /// Fetch multiple messages by given IDs + async fn fetch_messages_by_id(&self, ids: &[String]) -> Result> { + try_join_all(ids.iter().map(|id| self.fetch_message(id))).await + } + /// Update a given message with new information async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> { let mut messages = self.messages.lock().await; diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index f436f402..547db3d1 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -727,6 +727,13 @@ impl crate::User { id: self.id, } } + + pub fn as_author_for_system(&self) -> MessageAuthor { + MessageAuthor::System { + username: &self.username, + avatar: self.avatar.as_ref().map(|file| file.id.as_ref()), + } + } } impl From for PartialUser { diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index 3315e41b..26cf436e 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -2,6 +2,4 @@ pub mod bridge; pub mod idempotency; pub mod permissions; pub mod reference; - -#[cfg(test)] pub mod test_fixtures; diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index f1200644..4e8c4842 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -42,6 +42,16 @@ impl Reference { db.fetch_message(&self.id).await } + /// Fetch message from Ref and validate channel + pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result { + let msg = db.fetch_message(&self.id).await?; + if msg.channel != channel { + return Err(create_error!(NotFound)); + } + + Ok(msg) + } + /// Fetch server from Ref pub async fn as_server(&self, db: &Database) -> Result { db.fetch_server(&self.id).await diff --git a/crates/core/database/src/util/test_fixtures.rs b/crates/core/database/src/util/test_fixtures.rs index 0ec3acfb..f08fe91b 100644 --- a/crates/core/database/src/util/test_fixtures.rs +++ b/crates/core/database/src/util/test_fixtures.rs @@ -92,7 +92,7 @@ macro_rules! fixture { let fixtures = $crate::util::test_fixtures::load_fixture( &$database, - include_str!(concat!("../../../fixtures/", $name, ".json")), + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/", $name, ".json")), ) .await; diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 29744bd0..794a52eb 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -12,9 +12,10 @@ description = "Revolt Backend: API Models" serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"] schemas = ["dep:schemars", "revolt-permissions/schemas"] validator = ["dep:validator"] +rocket = ["dep:rocket"] partials = ["dep:revolt_optional_struct", "serde", "schemas"] -default = ["serde", "partials"] +default = ["serde", "partials", "rocket"] [dependencies] # Core @@ -26,6 +27,9 @@ regex = "1" indexmap = "1.9.3" once_cell = "1.17.1" +# Rocket +rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } + # Serialisation revolt_optional_struct = { version = "0.2.0", optional = true } serde = { version = "1", features = ["derive"], optional = true } diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index d06888d2..be034c42 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -3,6 +3,9 @@ use super::File; use revolt_permissions::OverrideField; use std::collections::{HashMap, HashSet}; +#[cfg(feature = "rocket")] +use rocket::FromForm; + auto_derived!( /// Channel #[serde(tag = "channel_type")] @@ -258,6 +261,13 @@ auto_derived!( #[serde(skip_serializing_if = "Option::is_none")] pub nsfw: Option, } + + /// Options when deleting a channel + #[cfg_attr(feature = "rocket", derive(FromForm))] + pub struct OptionsChannelDelete { + /// Whether to not send a leave message + pub leave_silently: Option, + } ); impl Channel { diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index f97595f9..b2a17a43 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -169,16 +169,19 @@ auto_derived!( #[derive(Default)] #[cfg_attr(feature = "validator", derive(Validate))] pub struct SendableEmbed { - #[validate(length(min = 1, max = 128))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))] pub icon_url: Option, - #[validate(length(min = 1, max = 256))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))] pub url: Option, - #[validate(length(min = 1, max = 100))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))] pub title: Option, - #[validate(length(min = 1, max = 2000))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] pub description: Option, pub media: Option, - #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")] + #[cfg_attr( + feature = "validator", + validate(length(min = 1, max = 128), regex = "RE_COLOUR") + )] pub colour: Option, } @@ -196,11 +199,11 @@ auto_derived!( /// Unique token to prevent duplicate message sending /// /// **This is deprecated and replaced by `Idempotency-Key`!** - #[validate(length(min = 1, max = 64))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))] pub nonce: Option, /// Message content to send - #[validate(length(min = 0, max = 2000))] + #[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))] pub content: Option, /// Attachments to include in message pub attachments: Option>, @@ -209,14 +212,25 @@ auto_derived!( /// Embeds to include in message /// /// Text embed content contributes to the content length cap - #[validate] + #[cfg_attr(feature = "validator", validate)] pub embeds: Option>, /// Masquerade to apply to this message - #[validate] + #[cfg_attr(feature = "validator", validate)] pub masquerade: Option, /// Information about how this message should be interacted with pub interactions: Option, } + + /// Options for bulk deleting messages + #[cfg_attr( + feature = "validator", + cfg_attr(feature = "validator", derive(Validate)) + )] + pub struct OptionsBulkDelete { + /// Message IDs + #[validate(length(min = 1, max = 100))] + pub ids: Vec, + } ); /// Message Author Abstraction diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 5bf0b433..cdfdcafd 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -74,7 +74,11 @@ revolt-database = { path = "../core/database", features = [ "rocket-impl", "redis-is-patched", ] } -revolt-models = { path = "../core/models", features = ["schemas", "validator"] } +revolt-models = { path = "../core/models", features = [ + "schemas", + "validator", + "rocket", +] } revolt-result = { path = "../core/result", features = ["rocket", "okapi"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] } diff --git a/crates/delta/src/routes/channels/channel_ack.rs b/crates/delta/src/routes/channels/channel_ack.rs index 19956548..e8b1cdae 100644 --- a/crates/delta/src/routes/channels/channel_ack.rs +++ b/crates/delta/src/routes/channels/channel_ack.rs @@ -1,23 +1,84 @@ -use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, +}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Acknowledge Message /// /// Lets the server and all other clients know that we've seen this message id in this channel. #[openapi(tag = "Messaging")] #[put("//ack/")] -pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result { +pub async fn ack( + db: &State, + user: User, + target: Reference, + message: Reference, +) -> Result { if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } let channel = target.as_channel(db).await?; - perms(&user) - .channel(&channel) - .throw_permission(db, Permission::ViewChannel) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; channel .ack(&user.id, &message.id) .await .map(|_| EmptyResponse) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{events::client::EventV1, Channel}; + use revolt_models::v0::DataCreateGroup; + use rocket::http::{Header, Status}; + + #[rocket::async_test] + async fn success_ack_channel() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let group = Channel::create_group( + &harness.db, + DataCreateGroup { + ..Default::default() + }, + user.id.clone(), + ) + .await + .expect("`Channel`"); + + let message_id = ulid::Ulid::new().to_string(); + let response = harness + .client + .put(format!("/channels/{}/ack/{}", group.id(), message_id)) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + drop(response); + + let event = harness + .wait_for_event(&format!("{}!", user.id), |event| match event { + EventV1::ChannelAck { id, .. } => id == &group.id(), + _ => false, + }) + .await; + + match event { + EventV1::ChannelAck { + message_id: m_id, .. + } => assert_eq!(m_id, message_id), + _ => unreachable!(), + }; + } +} diff --git a/crates/delta/src/routes/channels/channel_delete.rs b/crates/delta/src/routes/channels/channel_delete.rs index 7b3638d6..39b5ae7e 100644 --- a/crates/delta/src/routes/channels/channel_delete.rs +++ b/crates/delta/src/routes/channels/channel_delete.rs @@ -1,34 +1,32 @@ -use revolt_quark::{ - models::{channel::PartialChannel, Channel, User}, - perms, Db, EmptyResponse, Error, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, PartialChannel, User, }; -use serde::{Deserialize, Serialize}; -use validator::Validate; - -/// # Query Parameters -#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)] -pub struct OptionsChannelDelete { - /// Whether to not send a leave message - leave_silently: Option, -} +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Close Channel /// /// Deletes a server channel, leaves a group or closes a group. #[openapi(tag = "Channel Information")] #[delete("/?")] -pub async fn req( - db: &Db, +pub async fn delete( + db: &State, user: User, - target: Ref, - options: OptionsChannelDelete, + target: Reference, + options: v0::OptionsChannelDelete, ) -> Result { let mut channel = target.as_channel(db).await?; - let mut perms = perms(&user).channel(&channel); - perms.throw_permission(db, Permission::ViewChannel).await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + let permissions = calculate_channel_permissions(&mut query).await; + + permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; match &channel { - Channel::SavedMessages { .. } => Err(Error::NoEffect), + Channel::SavedMessages { .. } => Err(create_error!(NoEffect)), Channel::DirectMessage { .. } => channel .update( db, @@ -41,20 +39,58 @@ pub async fn req( .await .map(|_| EmptyResponse), Channel::Group { .. } => channel - .remove_user_from_group( - db, - &user.id, - None, - options.leave_silently.unwrap_or_default(), - ) + .remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default()) .await .map(|_| EmptyResponse), Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => { - perms - .throw_permission(db, Permission::ManageChannel) - .await?; - + permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; channel.delete(db).await.map(|_| EmptyResponse) } } } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{events::client::EventV1, Channel}; + use revolt_models::v0::DataCreateGroup; + use rocket::http::{Header, Status}; + + #[rocket::async_test] + async fn success_delete_group() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let group = Channel::create_group( + &harness.db, + DataCreateGroup { + ..Default::default() + }, + user.id.clone(), + ) + .await + .expect("`Channel`"); + + let response = harness + .client + .delete(format!("/channels/{}", group.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(&group.id(), |event| match event { + EventV1::ChannelDelete { id, .. } => id == &group.id(), + _ => false, + }) + .await; + } + + // TEST: member leaves group (no delete) + // TEST: no effect with saved messages + // TEST: DM set to inactive + // TEST: server channel deleted +} diff --git a/crates/delta/src/routes/channels/channel_edit.rs b/crates/delta/src/routes/channels/channel_edit.rs index 73c09690..3ee2774b 100644 --- a/crates/delta/src/routes/channels/channel_edit.rs +++ b/crates/delta/src/routes/channels/channel_edit.rs @@ -1,60 +1,36 @@ -use revolt_quark::{ - models::{ - channel::{Channel, FieldsChannel, PartialChannel}, - message::SystemMessage, - File, User, - }, - perms, Database, Error, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, File, PartialChannel, SystemMessage, User, }; - +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; -use serde::{Deserialize, Serialize}; use validator::Validate; -/// # Channel Details -#[derive(Validate, Serialize, Deserialize, JsonSchema)] -pub struct DataEditChannel { - /// Channel name - #[validate(length(min = 1, max = 32))] - name: Option, - /// Channel description - #[validate(length(min = 0, max = 1024))] - description: Option, - /// Group owner - owner: Option, - /// Icon - /// - /// Provide an Autumn attachment Id. - #[validate(length(min = 1, max = 128))] - icon: Option, - /// Whether this channel is age-restricted - nsfw: Option, - /// Whether this channel is archived - archived: Option, - #[validate(length(min = 1))] - remove: Option>, -} - /// # Edit Channel /// /// Edit a channel object by its id. #[openapi(tag = "Channel Information")] #[patch("/", data = "")] -pub async fn req( +pub async fn edit( db: &State, user: User, - target: Ref, - data: Json, -) -> Result> { + target: Reference, + data: Json, +) -> Result> { let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; let mut channel = target.as_channel(db).await?; - perms(&user) - .channel(&channel) - .throw_permission_and_view_channel(db, Permission::ManageChannel) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; if data.name.is_none() && data.description.is_none() @@ -63,7 +39,7 @@ pub async fn req( && data.owner.is_none() && data.remove.is_none() { - return Ok(Json(channel)); + return Ok(Json(channel.into())); } let mut partial: PartialChannel = Default::default(); @@ -76,12 +52,12 @@ pub async fn req( { // Make sure we are the owner of this group if owner != &user.id { - return Err(Error::NotOwner); + return Err(create_error!(NotOwner)); } // Ensure user is part of group if !recipients.contains(&new_owner) { - return Err(Error::NotInGroup); + return Err(create_error!(NotInGroup)); } // Transfer ownership @@ -94,10 +70,10 @@ pub async fn req( to: new_owner, } } else { - return Err(Error::InvalidOperation); + return Err(create_error!(InvalidOperation)); } .into_message(channel.id().to_string()) - .create(db, &channel, None) + .send(db, user.as_author_for_system(), &channel, false) .await .ok(); } @@ -128,7 +104,7 @@ pub async fn req( .. } => { if let Some(fields) = &data.remove { - if fields.contains(&FieldsChannel::Icon) { + if fields.contains(&v0::FieldsChannel::Icon) { if let Some(icon) = &icon { db.mark_attachment_as_deleted(&icon.id).await?; } @@ -136,10 +112,10 @@ pub async fn req( for field in fields { match field { - FieldsChannel::Description => { + v0::FieldsChannel::Description => { description.take(); } - FieldsChannel::Icon => { + v0::FieldsChannel::Icon => { icon.take(); } _ => {} @@ -175,7 +151,7 @@ pub async fn req( by: user.id.clone(), } .into_message(channel.id().to_string()) - .create(db, &channel, None) + .send(db, user.as_author_for_system(), &channel, false) .await .ok(); } @@ -185,26 +161,36 @@ pub async fn req( by: user.id.clone(), } .into_message(channel.id().to_string()) - .create(db, &channel, None) + .send(db, user.as_author_for_system(), &channel, false) .await .ok(); } if partial.icon.is_some() { - SystemMessage::ChannelIconChanged { by: user.id } - .into_message(channel.id().to_string()) - .create(db, &channel, None) - .await - .ok(); + SystemMessage::ChannelIconChanged { + by: user.id.clone(), + } + .into_message(channel.id().to_string()) + .send(db, user.as_author_for_system(), &channel, false) + .await + .ok(); } } channel - .update(db, partial, data.remove.unwrap_or_default()) + .update( + db, + partial, + data.remove + .unwrap_or_default() + .into_iter() + .map(|f| f.into()) + .collect(), + ) .await?; } - _ => return Err(Error::InvalidOperation), + _ => return Err(create_error!(InvalidOperation)), }; - Ok(Json(channel)) + Ok(Json(channel.into())) } diff --git a/crates/delta/src/routes/channels/channel_fetch.rs b/crates/delta/src/routes/channels/channel_fetch.rs index 3589f636..dfcc918d 100644 --- a/crates/delta/src/routes/channels/channel_fetch.rs +++ b/crates/delta/src/routes/channels/channel_fetch.rs @@ -13,7 +13,7 @@ use rocket::{serde::json::Json, State}; /// Fetch channel by its id. #[openapi(tag = "Channel Information")] #[get("/")] -pub async fn fetch_channel( +pub async fn fetch( db: &State, user: User, target: Reference, diff --git a/crates/delta/src/routes/channels/group_add_member.rs b/crates/delta/src/routes/channels/group_add_member.rs index 0fc701fb..6434ca53 100644 --- a/crates/delta/src/routes/channels/group_add_member.rs +++ b/crates/delta/src/routes/channels/group_add_member.rs @@ -13,7 +13,7 @@ use rocket_empty::EmptyResponse; /// Adds another user to the group. #[openapi(tag = "Groups")] #[put("//recipients/")] -pub async fn req( +pub async fn add_member( db: &State, user: User, group_id: Reference, diff --git a/crates/delta/src/routes/channels/group_remove_member.rs b/crates/delta/src/routes/channels/group_remove_member.rs index 5a86da40..185d0e26 100644 --- a/crates/delta/src/routes/channels/group_remove_member.rs +++ b/crates/delta/src/routes/channels/group_remove_member.rs @@ -1,16 +1,23 @@ -use revolt_quark::{ - models::{Channel, User}, - Db, EmptyResponse, Error, Permission, Ref, Result, -}; +use revolt_database::{util::reference::Reference, Channel, Database, User}; +use revolt_permissions::ChannelPermission; +use revolt_result::{create_error, Result}; + +use rocket::State; +use rocket_empty::EmptyResponse; /// # Remove Member from Group /// /// Removes a user from the group. #[openapi(tag = "Groups")] #[delete("//recipients/")] -pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result { +pub async fn remove_member( + db: &State, + user: User, + target: Reference, + member: Reference, +) -> Result { if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } let channel = target.as_channel(db).await?; @@ -20,24 +27,26 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result { if &user.id != owner { - return Error::from_permission(Permission::ManageChannel); + return Err(create_error!(MissingPermission { + permission: ChannelPermission::ManageChannel.to_string() + })); } let member = member.as_user(db).await?; if user.id == member.id { - return Err(Error::CannotRemoveYourself); + return Err(create_error!(CannotRemoveYourself)); } if !recipients.iter().any(|x| *x == member.id) { - return Err(Error::NotInGroup); + return Err(create_error!(NotInGroup)); } channel - .remove_user_from_group(db, &member.id, Some(&user.id), false) + .remove_user_from_group(db, &member, Some(&user.id), false) .await .map(|_| EmptyResponse) } - _ => Err(Error::InvalidOperation), + _ => Err(create_error!(InvalidOperation)), } } diff --git a/crates/delta/src/routes/channels/invite_create.rs b/crates/delta/src/routes/channels/invite_create.rs index 2bf4700a..1214816e 100644 --- a/crates/delta/src/routes/channels/invite_create.rs +++ b/crates/delta/src/routes/channels/invite_create.rs @@ -1,9 +1,12 @@ -use revolt_quark::{ - models::{Invite, User}, - perms, Db, Error, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, Invite, User, }; +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; -use rocket::serde::json::Json; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; /// # Create Invite /// @@ -12,16 +15,23 @@ use rocket::serde::json::Json; /// Channel must be a `TextChannel`. #[openapi(tag = "Channel Invites")] #[post("//invites")] -pub async fn req(db: &Db, user: User, target: Ref) -> Result> { +pub async fn create_invite( + db: &State, + user: User, + target: Reference, +) -> Result> { if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } let channel = target.as_channel(db).await?; - perms(&user) - .channel(&channel) - .throw_permission_and_view_channel(db, Permission::InviteOthers) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?; - Invite::create(db, &user, &channel).await.map(Json) + Invite::create_channel_invite(db, &user, &channel) + .await + .map(|invite| invite.into()) + .map(Json) } diff --git a/crates/delta/src/routes/channels/members_fetch.rs b/crates/delta/src/routes/channels/members_fetch.rs index 51510bd2..b7815138 100644 --- a/crates/delta/src/routes/channels/members_fetch.rs +++ b/crates/delta/src/routes/channels/members_fetch.rs @@ -1,31 +1,40 @@ -use revolt_quark::{ - models::{Channel, User}, - perms, Db, Error, Permission, Ref, Result, +use futures::future::join_all; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, User, }; - -use rocket::serde::json::Json; +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; /// # Fetch Group Members /// /// Retrieves all users who are part of this group. #[openapi(tag = "Groups")] #[get("//members")] -pub async fn req(db: &Db, user: User, target: Ref) -> Result>> { +pub async fn fetch_members( + db: &State, + user: User, + target: Reference, +) -> Result>> { let channel = target.as_channel(db).await?; - perms(&user) - .channel(&channel) - .throw_permission(db, Permission::ViewChannel) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; if let Channel::Group { recipients, .. } = channel { Ok(Json( - db.fetch_users(&recipients) - .await? - .into_iter() - .map(|x| x.with_relationship(&user)) - .collect::>(), + join_all( + db.fetch_users(&recipients) + .await? + .into_iter() + .map(|other_user| other_user.into(db, &user)), + ) + .await, )) } else { - Err(Error::InvalidOperation) + Err(create_error!(InvalidOperation)) } } diff --git a/crates/delta/src/routes/channels/message_bulk_delete.rs b/crates/delta/src/routes/channels/message_bulk_delete.rs index 65aae1bf..6cb2b37a 100644 --- a/crates/delta/src/routes/channels/message_bulk_delete.rs +++ b/crates/delta/src/routes/channels/message_bulk_delete.rs @@ -1,20 +1,16 @@ use chrono::Utc; -use revolt_quark::{ - models::{Message, User}, - perms, Db, EmptyResponse, Error, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, Message, User, }; -use rocket::serde::json::Json; +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use rocket_empty::EmptyResponse; use serde::Deserialize; use validator::Validate; -/// # Search Parameters -#[derive(Validate, Deserialize, JsonSchema)] -pub struct OptionsBulkDelete { - /// Message IDs - #[validate(length(min = 1, max = 100))] - ids: Vec, -} - /// # Bulk Delete Messages /// /// Delete multiple messages you've sent or one you have permission to delete. @@ -24,34 +20,37 @@ pub struct OptionsBulkDelete { /// Messages must have been sent within the past 1 week. #[openapi(tag = "Messaging")] #[delete("//messages/bulk", data = "", rank = 1)] -pub async fn req( - db: &Db, +pub async fn bulk_delete_messages( + db: &State, user: User, - target: Ref, - options: Json, + target: Reference, + options: Json, ) -> Result { let options = options.into_inner(); - options - .validate() - .map_err(|error| Error::FailedValidation { error })?; + options.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; for id in &options.ids { if ulid::Ulid::from_string(id) - .map_err(|_| Error::InvalidOperation)? + .map_err(|_| create_error!(InvalidOperation))? .datetime() .signed_duration_since(Utc::now()) .num_days() .abs() > 7 { - return Err(Error::InvalidOperation); + return Err(create_error!(InvalidOperation)); } } - perms(&user) - .channel(&target.as_channel(db).await?) - .throw_permission(db, Permission::ManageMessages) - .await?; + let channel = target.as_channel(db).await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; Message::bulk_delete(db, &target.id, options.ids) .await diff --git a/crates/delta/src/routes/channels/message_clear_reactions.rs b/crates/delta/src/routes/channels/message_clear_reactions.rs index 683a225d..2dffc3fa 100644 --- a/crates/delta/src/routes/channels/message_clear_reactions.rs +++ b/crates/delta/src/routes/channels/message_clear_reactions.rs @@ -1,7 +1,11 @@ -use revolt_quark::{ - models::{message::PartialMessage, User}, - perms, Db, EmptyResponse, Permission, Ref, Result, +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, PartialMessage, User, }; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::Result; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Remove All Reactions from Message /// @@ -10,15 +14,20 @@ use revolt_quark::{ /// Requires `ManageMessages` permission. #[openapi(tag = "Interactions")] #[delete("//messages//reactions")] -pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result { +pub async fn clear_reactions( + db: &State, + user: User, + target: Reference, + msg: Reference, +) -> Result { let channel = target.as_channel(db).await?; - perms(&user) - .channel(&channel) - .throw_permission_and_view_channel(db, Permission::ManageMessages) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; // Fetch relevant message - let mut message = msg.as_message_in(db, channel.id()).await?; + let mut message = msg.as_message_in_channel(db, &channel.id()).await?; // Clear reactions message diff --git a/crates/delta/src/routes/channels/message_delete.rs b/crates/delta/src/routes/channels/message_delete.rs index 6a427316..3c1f1e7e 100644 --- a/crates/delta/src/routes/channels/message_delete.rs +++ b/crates/delta/src/routes/channels/message_delete.rs @@ -1,21 +1,31 @@ -use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, +}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::Result; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Delete Message /// /// Delete a message you've sent or one you have permission to delete. #[openapi(tag = "Messaging")] #[delete("//messages/", rank = 2)] -pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result { - let message = msg.as_message(db).await?; - if message.channel != target.id { - return Err(Error::NotFound); - } +pub async fn delete( + db: &State, + user: User, + target: Reference, + msg: Reference, +) -> Result { + let message = msg.as_message_in_channel(db, &target.id).await?; if message.author != user.id { - perms(&user) - .channel(&target.as_channel(db).await?) - .throw_permission(db, Permission::ManageMessages) - .await?; + let channel = target.as_channel(db).await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; } message.delete(db).await.map(|_| EmptyResponse) diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index 114df3d4..ed89f937 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -28,22 +28,22 @@ mod webhook_fetch_all; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ - channel_ack::req, - channel_fetch::fetch_channel, - members_fetch::req, - channel_delete::req, - channel_edit::req, - invite_create::req, + channel_ack::ack, + channel_fetch::fetch, + members_fetch::fetch_members, + channel_delete::delete, + channel_edit::edit, + invite_create::create_invite, message_send::message_send, message_query::req, message_search::req, message_fetch::req, message_edit::req, - message_bulk_delete::req, - message_delete::req, + message_bulk_delete::bulk_delete_messages, + message_delete::delete, group_create::create_group, - group_add_member::req, - group_remove_member::req, + group_add_member::add_member, + group_remove_member::remove_member, voice_join::req, permissions_set::req, permissions_set_default::req,