diff --git a/crates/core/database/src/lib.rs b/crates/core/database/src/lib.rs index 90365598..dd911f0d 100644 --- a/crates/core/database/src/lib.rs +++ b/crates/core/database/src/lib.rs @@ -87,3 +87,8 @@ pub mod tasks; pub fn if_false(t: &bool) -> bool { !t } + +/// Utility function to check if an option doesnt contain true +pub fn if_option_false(t: &Option) -> bool { + t != &Some(true) +} \ No newline at end of file diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index dd507a30..25b6c625 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -65,6 +65,9 @@ auto_derived_partial!( /// Name and / or avatar overrides for this message #[serde(skip_serializing_if = "Option::is_none")] pub masquerade: Option, + /// Whether or not the message in pinned + #[serde(skip_serializing_if = "crate::if_option_false")] + pub pinned: Option, /// Bitfield of message flags #[serde(skip_serializing_if = "Option::is_none")] @@ -168,6 +171,8 @@ auto_derived!( pub author: Option, /// Search query pub query: Option, + /// Search for pinned + pub pinned: Option, } /// Message Query @@ -205,6 +210,7 @@ impl Default for Message { interactions: Default::default(), masquerade: None, flags: None, + pinned: None, } } } @@ -571,6 +577,8 @@ impl Message { users.push(id.clone()); } v0::SystemMessage::Text { .. } => {} + v0::SystemMessage::MessagePinned { .. } => {}, + v0::SystemMessage::MessageUnpinned { .. } => {}, } } users diff --git a/crates/core/database/src/models/messages/ops/mongodb.rs b/crates/core/database/src/models/messages/ops/mongodb.rs index 530fd2f6..faaeca36 100644 --- a/crates/core/database/src/models/messages/ops/mongodb.rs +++ b/crates/core/database/src/models/messages/ops/mongodb.rs @@ -50,6 +50,10 @@ impl AbstractMessages for MongoDb { false }; + if let Some(pinned) = query.filter.pinned { + filter.insert("pinned", pinned); + }; + // 2. Find query limit let limit = query.limit.unwrap_or(50); @@ -156,6 +160,7 @@ impl AbstractMessages for MongoDb { .build(), ) .await + .inspect_err(|e| eprintln!("{e:?}")) .map_err(|_| create_database_error!("find", COL)) } } diff --git a/crates/core/database/src/models/messages/ops/reference.rs b/crates/core/database/src/models/messages/ops/reference.rs index 80e024ee..636cadb0 100644 --- a/crates/core/database/src/models/messages/ops/reference.rs +++ b/crates/core/database/src/models/messages/ops/reference.rs @@ -56,6 +56,12 @@ impl AbstractMessages for ReferenceDb { } } + if let Some(pinned) = query.filter.pinned { + if message.pinned.unwrap_or_default() == pinned { + return false + } + } + true }) .cloned() diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 87ec4ba1..814fae2c 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -482,6 +482,7 @@ impl crate::Message { interactions: self.interactions.into(), masquerade: self.masquerade.map(Into::into), flags: self.flags.map(|flags| flags as u32).unwrap_or_default(), + pinned: self.pinned, } } } @@ -509,6 +510,7 @@ impl From for PartialMessage { interactions: value.interactions.map(Into::into), masquerade: value.masquerade.map(Into::into), flags: value.flags.map(|flags| flags as u32), + pinned: value.pinned, } } } diff --git a/crates/core/models/src/lib.rs b/crates/core/models/src/lib.rs index 4e73efed..7aa5d4d1 100644 --- a/crates/core/models/src/lib.rs +++ b/crates/core/models/src/lib.rs @@ -66,3 +66,8 @@ pub fn if_false(t: &bool) -> bool { pub fn if_zero_u32(t: &u32) -> bool { t == &0 } + +/// Utility function to check if an option doesnt contain true +pub fn if_option_false(t: &Option) -> bool { + t != &Some(true) +} diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index 2448c93f..2abf5bf1 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -70,6 +70,9 @@ auto_derived_partial!( /// Name and / or avatar overrides for this message #[serde(skip_serializing_if = "Option::is_none")] pub masquerade: Option, + /// Whether or not the message in pinned + #[serde(skip_serializing_if = "crate::if_option_false")] + pub pinned: Option, /// Bitfield of message flags /// @@ -127,6 +130,10 @@ auto_derived!( ChannelIconChanged { by: String }, #[serde(rename = "channel_ownership_changed")] ChannelOwnershipChanged { from: String, to: String }, + #[serde(rename = "message_pinned")] + MessagePinned { id: String }, + #[serde(rename = "message_unpinned")] + MessageUnpinned { id: String }, } /// Name and / or avatar override information @@ -298,7 +305,9 @@ auto_derived!( /// /// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information. #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))] - pub query: String, + pub query: Option, + /// Whether to only search for pinned messages, cannot be sent with `query`. + pub pinned: Option, /// Maximum number of messages to fetch #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))] @@ -418,6 +427,8 @@ impl From for String { SystemMessage::ChannelOwnershipChanged { .. } => { "Channel ownership changed.".to_string() } + SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(), + SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(), } } } diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index b444d66b..328e0752 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -90,6 +90,8 @@ pub enum ErrorType { }, AlreadyInGroup, NotInGroup, + AlreadyPinned, + NotPinned, // ? Server related errors UnknownServer, diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 272cfc42..3e9e71c1 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -40,6 +40,8 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::GroupTooLarge { .. } => Status::Forbidden, ErrorType::AlreadyInGroup => Status::Conflict, ErrorType::NotInGroup => Status::NotFound, + ErrorType::AlreadyPinned => Status::BadRequest, + ErrorType::NotPinned => Status::BadRequest, ErrorType::UnknownServer => Status::NotFound, ErrorType::InvalidRole => Status::NotFound, diff --git a/crates/delta/src/routes/channels/message_pin.rs b/crates/delta/src/routes/channels/message_pin.rs new file mode 100644 index 00000000..8163556a --- /dev/null +++ b/crates/delta/src/routes/channels/message_pin.rs @@ -0,0 +1,39 @@ +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, PartialMessage, User +}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +/// # Pins a message +/// +/// Pins a message by its id. +#[openapi(tag = "Messaging")] +#[post("//messages//pin")] +pub async fn message_pin( + 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 + .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; + + let mut message = msg.as_message_in_channel(db, &channel.id()).await?; + + if message.pinned.unwrap_or_default() { + return Err(create_error!(AlreadyPinned)) + } + + message.update(db, PartialMessage { + pinned: Some(true), + ..Default::default() + }).await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/channels/message_search.rs b/crates/delta/src/routes/channels/message_search.rs index 74d78540..eb173bc0 100644 --- a/crates/delta/src/routes/channels/message_search.rs +++ b/crates/delta/src/routes/channels/message_search.rs @@ -30,6 +30,10 @@ pub async fn search( }) })?; + if options.query.is_some() && options.pinned.is_some() { + return Err(create_error!(InvalidOperation)) + } + let channel = target.as_channel(db).await?; let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); @@ -39,6 +43,7 @@ pub async fn search( let v0::DataMessageSearch { query, + pinned, limit, before, after, @@ -51,7 +56,8 @@ pub async fn search( MessageQuery { filter: MessageFilter { channel: Some(channel.id().to_string()), - query: Some(query), + query, + pinned, ..Default::default() }, time_period: MessageTimePeriod::Absolute { diff --git a/crates/delta/src/routes/channels/message_unpin.rs b/crates/delta/src/routes/channels/message_unpin.rs new file mode 100644 index 00000000..4271d62e --- /dev/null +++ b/crates/delta/src/routes/channels/message_unpin.rs @@ -0,0 +1,39 @@ +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, PartialMessage, User +}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +/// # Unpins a message +/// +/// Unpins a message by its id. +#[openapi(tag = "Messaging")] +#[post("//messages//unpin")] +pub async fn message_unpin( + 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 + .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; + + let mut message = msg.as_message_in_channel(db, &channel.id()).await?; + + if !message.pinned.unwrap_or_default() { + return Err(create_error!(NotPinned)) + } + + message.update(db, PartialMessage { + pinned: Some(false), + ..Default::default() + }).await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index da6a400f..10c83dc4 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -15,10 +15,12 @@ mod message_clear_reactions; mod message_delete; mod message_edit; mod message_fetch; +mod message_pin; mod message_query; mod message_react; mod message_search; mod message_send; +mod message_unpin; mod message_unreact; mod permissions_set; mod permissions_set_default; @@ -37,10 +39,12 @@ pub fn routes() -> (Vec, OpenApi) { message_send::message_send, message_query::query, message_search::search, + message_pin::message_pin, message_fetch::fetch, message_edit::edit, message_bulk_delete::bulk_delete_messages, message_delete::delete, + message_unpin::message_unpin, group_create::create_group, group_add_member::add_member, group_remove_member::remove_member,