diff --git a/Cargo.lock b/Cargo.lock index 25cee682..93ffd280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3669,6 +3669,7 @@ dependencies = [ "env_logger", "futures", "impl_ops", + "iso8601-timestamp 0.2.11", "lettre", "linkify 0.6.0", "log", diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index d8be9c92..61cebca7 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -3,9 +3,12 @@ use std::collections::HashSet; use indexmap::{IndexMap, IndexSet}; use iso8601_timestamp::Timestamp; use revolt_config::config; -use revolt_models::v0::{ - self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification, - ReplyIntent, SendableEmbed, RE_MENTION, +use revolt_models::{ + v0::{ + self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification, + ReplyIntent, SendableEmbed, Text, RE_MENTION, + }, + validator::Validate, }; use revolt_permissions::{ChannelPermission, PermissionValue}; use revolt_result::Result; @@ -155,6 +158,7 @@ auto_derived!( } /// Message Filter + #[derive(Default)] pub struct MessageFilter { /// Parent channel ID pub channel: Option, @@ -441,6 +445,33 @@ impl Message { Ok(()) } + /// Create text embed from sendable embed + pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result { + embed.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let media = if let Some(id) = embed.media { + Some( + db.find_and_use_attachment(&id, "attachments", "message", &self.id) + .await?, + ) + } else { + None + }; + + Ok(Embed::Text(Text { + icon_url: embed.icon_url, + url: embed.url, + title: embed.title, + description: embed.description, + media: media.map(|m| m.into()), + colour: embed.colour, + })) + } + /// Update message data pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> { self.apply_options(partial.clone()); diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index be034c42..8815fb8a 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -268,6 +268,12 @@ auto_derived!( /// Whether to not send a leave message pub leave_silently: Option, } + + /// Voice server token response + pub struct LegacyCreateVoiceUserResponse { + /// Token for authenticating with the voice server + token: String, + } ); impl Channel { diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index b2a17a43..46fd4419 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -8,9 +8,12 @@ use revolt_config::config; #[cfg(feature = "validator")] use validator::Validate; +#[cfg(feature = "rocket")] +use rocket::{FromForm, FromFormField}; + use iso8601_timestamp::Timestamp; -use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR}; +use super::{Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR}; pub static RE_MENTION: Lazy = Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap()); @@ -66,6 +69,24 @@ auto_derived_partial!( ); auto_derived!( + /// Bulk Message Response + #[serde(untagged)] + pub enum BulkMessageResponse { + JustMessages( + /// List of messages + Vec, + ), + MessagesAndUsers { + /// List of messages + messages: Vec, + /// List of users + users: Vec, + /// List of members + #[serde(skip_serializing_if = "Option::is_none")] + members: Option>, + }, + } + /// System Event #[serde(tag = "type")] pub enum SystemMessage { @@ -136,6 +157,7 @@ auto_derived!( /// /// Sort used for retrieving messages #[derive(Default)] + #[cfg_attr(feature = "rocket", derive(FromFormField))] pub enum MessageSort { /// Sort by the most relevant messages #[default] @@ -221,6 +243,71 @@ auto_derived!( pub interactions: Option, } + /// Options for querying messages + #[cfg_attr(feature = "validator", derive(Validate))] + #[cfg_attr(feature = "rocket", derive(FromForm))] + pub struct OptionsQueryMessages { + /// Maximum number of messages to fetch + /// + /// For fetching nearby messages, this is \`(limit + 1)\`. + #[validate(range(min = 1, max = 100))] + pub limit: Option, + /// Message id before which messages should be fetched + #[validate(length(min = 26, max = 26))] + pub before: Option, + /// Message id after which messages should be fetched + #[validate(length(min = 26, max = 26))] + pub after: Option, + /// Message sort direction + pub sort: Option, + /// Message id to search around + /// + /// Specifying 'nearby' ignores 'before', 'after' and 'sort'. + /// It will also take half of limit rounded as the limits to each side. + /// It also fetches the message ID specified. + #[validate(length(min = 26, max = 26))] + pub nearby: Option, + /// Whether to include user (and member, if server channel) objects + pub include_users: Option, + } + + /// Options for searching for messages + pub struct OptionsMessageSearch { + /// Full-text search query + /// + /// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information. + #[validate(length(min = 1, max = 64))] + pub query: String, + + /// Maximum number of messages to fetch + #[validate(range(min = 1, max = 100))] + pub limit: Option, + /// Message id before which messages should be fetched + #[validate(length(min = 26, max = 26))] + pub before: Option, + /// Message id after which messages should be fetched + #[validate(length(min = 26, max = 26))] + pub after: Option, + /// Message sort direction + /// + /// By default, it will be sorted by latest. + #[serde(default = "MessageSort::default")] + pub sort: MessageSort, + /// Whether to include user (and member, if server channel) objects + pub include_users: Option, + } + + /// Changes to make to message + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataEditMessage { + /// New message content + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] + pub content: Option, + /// Embeds to include in the message + #[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))] + pub embeds: Option>, + } + /// Options for bulk deleting messages #[cfg_attr( feature = "validator", diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index cdfdcafd..a02f427e 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -36,6 +36,7 @@ nanoid = "0.4.0" serde_json = "1.0.57" serde = { version = "1.0.115", features = ["derive"] } validator = { version = "0.16", features = ["derive"] } +iso8601-timestamp = { version = "0.2.11", features = [] } # async futures = "0.3.8" diff --git a/crates/delta/src/routes/channels/message_edit.rs b/crates/delta/src/routes/channels/message_edit.rs index d088a796..1966cef7 100644 --- a/crates/delta/src/routes/channels/message_edit.rs +++ b/crates/delta/src/routes/channels/message_edit.rs @@ -1,60 +1,53 @@ -use revolt_quark::{ - models::message::{PartialMessage, SendableEmbed}, - models::{Message, User}, - perms, - types::january::Embed, - Db, Error, Permission, Ref, Result, Timestamp, +use iso8601_timestamp::Timestamp; +use revolt_config::config; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, Message, PartialMessage, User, }; - -use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; +use revolt_models::v0::{self, Embed}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; use validator::Validate; -/// # Message Details -#[derive(Validate, Serialize, Deserialize, JsonSchema)] -pub struct DataEditMessage { - /// New message content - #[validate(length(min = 1, max = 2000))] - content: Option, - /// Embeds to include in the message - #[validate(length(min = 0, max = 10))] - embeds: Option>, -} - /// # Edit Message /// /// Edits a message that you've previously sent. #[openapi(tag = "Messaging")] #[patch("//messages/", data = "")] -pub async fn req( - db: &Db, +pub async fn edit( + db: &State, user: User, - target: Ref, - msg: Ref, - edit: Json, -) -> Result> { + target: Reference, + msg: Reference, + edit: Json, +) -> Result> { let edit = edit.into_inner(); - edit.validate() - .map_err(|error| Error::FailedValidation { error })?; + edit.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let config = config().await; + Message::validate_sum( + &edit.content, + edit.embeds.as_deref().unwrap_or_default(), + config.features.limits.default.message_length, + )?; // Ensure we have permissions to send a message let channel = target.as_channel(db).await?; - let mut permissions = perms(&user).channel(&channel); - permissions - .throw_permission_and_view_channel(db, Permission::SendMessage) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + let permissions = calculate_channel_permissions(&mut query).await; - let mut message = msg.as_message(db).await?; - if message.channel != channel.id() { - return Err(Error::NotFound); - } + permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?; + let mut message = msg.as_message_in_channel(db, &channel.id()).await?; if message.author != user.id { - return Err(Error::CannotEditMessage); + return Err(create_error!(CannotEditMessage)); } - Message::validate_sum(&edit.content, edit.embeds.as_deref().unwrap_or_default())?; - message.edited = Some(Timestamp::now_utc()); let mut partial = PartialMessage { edited: message.edited, @@ -79,14 +72,12 @@ pub async fn req( // 3. Replace if we are given new embeds if let Some(embeds) = edit.embeds { // Ensure we have permissions to send embeds - permissions - .throw_permission_and_view_channel(db, Permission::SendEmbeds) - .await?; + permissions.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?; new_embeds.clear(); for embed in embeds { - new_embeds.push(embed.clone().into_embed(db, &message.id).await?); + new_embeds.push(message.create_embed(db, embed).await?); } } @@ -95,10 +86,7 @@ pub async fn req( message.update(db, partial).await?; // Queue up a task for processing embeds if the we have sufficient permissions - if permissions - .has_permission(db, Permission::SendEmbeds) - .await? - { + if permissions.has_channel_permission(ChannelPermission::SendEmbeds) { if let Some(content) = edit.content { revolt_quark::tasks::process_embeds::queue( message.channel.to_string(), @@ -109,5 +97,5 @@ pub async fn req( } } - Ok(Json(message)) + Ok(Json(message.into())) } diff --git a/crates/delta/src/routes/channels/message_fetch.rs b/crates/delta/src/routes/channels/message_fetch.rs index 85897ffa..e2d4f94a 100644 --- a/crates/delta/src/routes/channels/message_fetch.rs +++ b/crates/delta/src/routes/channels/message_fetch.rs @@ -12,14 +12,13 @@ use rocket::{serde::json::Json, State}; /// Retrieves a message by its id. #[openapi(tag = "Messaging")] #[get("//messages/")] -pub async fn req( +pub async fn fetch( db: &State, user: User, target: Reference, msg: Reference, ) -> Result> { let channel = target.as_channel(db).await?; - let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); calculate_channel_permissions(&mut query) .await diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index ed89f937..28e1870b 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -37,20 +37,20 @@ pub fn routes() -> (Vec, OpenApi) { message_send::message_send, message_query::req, message_search::req, - message_fetch::req, - message_edit::req, + message_fetch::fetch, + message_edit::edit, message_bulk_delete::bulk_delete_messages, message_delete::delete, group_create::create_group, group_add_member::add_member, group_remove_member::remove_member, - voice_join::req, + voice_join::call, permissions_set::req, permissions_set_default::req, message_react::react_message, message_unreact::unreact_message, message_clear_reactions::clear_reactions, - webhook_create::req, - webhook_fetch_all::req, + webhook_create::create_webhook, + webhook_fetch_all::fetch_webhooks, ] } diff --git a/crates/delta/src/routes/channels/voice_join.rs b/crates/delta/src/routes/channels/voice_join.rs index 5ceb2273..33f4e05c 100644 --- a/crates/delta/src/routes/channels/voice_join.rs +++ b/crates/delta/src/routes/channels/voice_join.rs @@ -1,40 +1,37 @@ -use revolt_quark::{ - models::{Channel, User}, - perms, - variables::delta::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL}, - Db, Error, Permission, Ref, Result, +use revolt_config::config; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Channel, Database, User, }; - -use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; - -/// # Voice Server Token Response -#[derive(Serialize, Deserialize, JsonSchema)] -pub struct CreateVoiceUserResponse { - /// Token for authenticating with the voice server - token: String, -} +use revolt_models::v0; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; /// # Join Call /// /// Asks the voice server for a token to join the call. #[openapi(tag = "Voice")] #[post("//join_call")] -pub async fn req(db: &Db, user: User, target: Ref) -> Result> { +pub async fn call( + db: &State, + user: User, + target: Reference, +) -> Result> { let channel = target.as_channel(db).await?; - let mut permissions = perms(&user).channel(&channel); + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::Connect)?; - permissions - .throw_permission_and_view_channel(db, Permission::Connect) - .await?; - - if !*USE_VOSO { - return Err(Error::VosoUnavailable); + let config = config().await; + if config.api.security.voso_legacy_token.is_empty() { + return Err(create_error!(VosoUnavailable)); } match channel { Channel::SavedMessages { .. } | Channel::TextChannel { .. } => { - return Err(Error::CannotJoinCall) + return Err(create_error!(CannotJoinCall)) } _ => {} } @@ -44,33 +41,41 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result return Err(Error::VosoUnavailable), + Err(_) => return Err(create_error!(VosoUnavailable)), Ok(result) => match result.status() { reqwest::StatusCode::OK => (), reqwest::StatusCode::NOT_FOUND => { if (client - .post(&format!("{}/room/{}", *VOSO_URL, channel.id())) + .post(&format!( + "{}/room/{}", + config.hosts.voso_legacy, + channel.id() + )) .header( reqwest::header::AUTHORIZATION, - VOSO_MANAGE_TOKEN.to_string(), + config.api.security.voso_legacy_token.clone(), ) .send() .await) .is_err() { - return Err(Error::VosoUnavailable); + return Err(create_error!(VosoUnavailable)); } } - _ => return Err(Error::VosoUnavailable), + _ => return Err(create_error!(VosoUnavailable)), }, } @@ -78,13 +83,13 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result Result/webhooks", data = "")] -pub async fn req( +pub async fn create_webhook( db: &State, user: User, target: Reference, diff --git a/crates/delta/src/routes/channels/webhook_fetch_all.rs b/crates/delta/src/routes/channels/webhook_fetch_all.rs index 5727537f..1d10ccea 100644 --- a/crates/delta/src/routes/channels/webhook_fetch_all.rs +++ b/crates/delta/src/routes/channels/webhook_fetch_all.rs @@ -12,7 +12,7 @@ use rocket::{serde::json::Json, State}; /// Gets all webhooks inside the channel #[openapi(tag = "Webhooks")] #[get("//webhooks")] -pub async fn req( +pub async fn fetch_webhooks( db: &State, user: User, channel_id: Reference,