From 9a417aa6ad42370c889eaab49a4da2e358a41891 Mon Sep 17 00:00:00 2001 From: Paul <paulmakles@gmail.com> Date: Wed, 23 Jun 2021 13:50:18 +0100 Subject: [PATCH] Messaging: Add message replies. Servers: Add VoiceChannel. --- set_version.sh | 2 +- src/database/entities/channel.rs | 170 ++++++++++++--------- src/database/entities/message.rs | 5 + src/database/permissions/channel.rs | 4 +- src/notifications/events.rs | 3 +- src/routes/channels/delete_channel.rs | 3 +- src/routes/channels/invite_create.rs | 3 +- src/routes/channels/message_delete.rs | 1 + src/routes/channels/message_edit.rs | 2 + src/routes/channels/message_fetch.rs | 1 + src/routes/channels/message_query.rs | 1 + src/routes/channels/message_query_stale.rs | 1 + src/routes/channels/message_send.rs | 64 ++++++-- src/routes/servers/channel_create.rs | 43 ++++-- src/util/result.rs | 4 + src/version.rs | 2 +- 16 files changed, 208 insertions(+), 101 deletions(-) diff --git a/set_version.sh b/set_version.sh index d003f77..e84cc49 100755 --- a/set_version.sh +++ b/set_version.sh @@ -1,3 +1,3 @@ #!/bin/bash -export version=0.5.0-alpha.5 +export version=0.5.1-alpha.0 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index 5f102b2..97cee87 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -66,6 +66,19 @@ pub enum Channel { #[serde(skip_serializing_if = "Option::is_none")] last_message: Option<String>, }, + VoiceChannel { + #[serde(rename = "_id")] + id: String, + server: String, + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option<String>, + + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + icon: Option<File>, + }, } impl Channel { @@ -74,7 +87,17 @@ impl Channel { Channel::SavedMessages { id, .. } | Channel::DirectMessage { id, .. } | Channel::Group { id, .. } - | Channel::TextChannel { id, .. } => id, + | Channel::TextChannel { id, .. } + | Channel::VoiceChannel { id, .. } => id, + } + } + pub fn has_messaging(&self) -> Result<()> { + match self { + Channel::SavedMessages { .. } + | Channel::DirectMessage { .. } + | Channel::Group { .. } + | Channel::TextChannel { .. } => Ok(()), + Channel::VoiceChannel { .. } => Err(Error::InvalidOperation) } } @@ -129,79 +152,84 @@ impl Channel { with: "channel_invites", })?; - // Delete any unreads. - get_collection("channel_unreads") - .delete_many( - doc! { - "_id.channel": id - }, - None, - ) - .await - .map_err(|_| Error::DatabaseError { - operation: "delete_many", - with: "channel_unreads", - })?; + match &self { + Channel::VoiceChannel { .. } => {}, + _ => { + // Delete any unreads. + get_collection("channel_unreads") + .delete_many( + doc! { + "_id.channel": id + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "delete_many", + with: "channel_unreads", + })?; - // Check if there are any attachments we need to delete. - let message_ids = messages - .find( - doc! { - "channel": id, - "attachment": { - "$exists": 1 - } - }, - FindOptions::builder().projection(doc! { "_id": 1 }).build(), - ) - .await - .map_err(|_| Error::DatabaseError { - operation: "fetch_many", - with: "messages", - })? - .filter_map(async move |s| s.ok()) - .collect::<Vec<Document>>() - .await - .into_iter() - .filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string())) - .collect::<Vec<String>>(); + // Check if there are any attachments we need to delete. + let message_ids = messages + .find( + doc! { + "channel": id, + "attachment": { + "$exists": 1 + } + }, + FindOptions::builder().projection(doc! { "_id": 1 }).build(), + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "fetch_many", + with: "messages", + })? + .filter_map(async move |s| s.ok()) + .collect::<Vec<Document>>() + .await + .into_iter() + .filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string())) + .collect::<Vec<String>>(); - // If we found any, mark them as deleted. - if message_ids.len() > 0 { - get_collection("attachments") - .update_many( - doc! { - "message_id": { - "$in": message_ids - } - }, - doc! { - "$set": { - "deleted": true - } - }, - None, - ) - .await - .map_err(|_| Error::DatabaseError { - operation: "update_many", - with: "attachments", - })?; - } + // If we found any, mark them as deleted. + if message_ids.len() > 0 { + get_collection("attachments") + .update_many( + doc! { + "message_id": { + "$in": message_ids + } + }, + doc! { + "$set": { + "deleted": true + } + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "update_many", + with: "attachments", + })?; + } - // And then delete said messages. - messages - .delete_many( - doc! { - "channel": id - }, - None, - ) - .await - .map_err(|_| Error::DatabaseError { - operation: "delete_many", - with: "messages", - })?; + // And then delete said messages. + messages + .delete_many( + doc! { + "channel": id + }, + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "delete_many", + with: "messages", + })?; + } + } // Remove from server object. if let Channel::TextChannel { server, .. } = &self { diff --git a/src/database/entities/message.rs b/src/database/entities/message.rs index 675ad35..b733515 100644 --- a/src/database/entities/message.rs +++ b/src/database/entities/message.rs @@ -57,6 +57,7 @@ impl Content { target.id().to_string(), self, None, + None ) .publish(&target) .await @@ -81,6 +82,8 @@ pub struct Message { pub embeds: Option<Vec<Embed>>, #[serde(skip_serializing_if = "Option::is_none")] pub mentions: Option<Vec<String>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub replies: Option<Vec<String>> } impl Message { @@ -89,6 +92,7 @@ impl Message { channel: String, content: Content, mentions: Option<Vec<String>>, + replies: Option<Vec<String>>, ) -> Message { Message { id: Ulid::new().to_string(), @@ -101,6 +105,7 @@ impl Message { edited: None, embeds: None, mentions, + replies } } diff --git a/src/database/permissions/channel.rs b/src/database/permissions/channel.rs index c47a21b..e05f4ce 100644 --- a/src/database/permissions/channel.rs +++ b/src/database/permissions/channel.rs @@ -84,7 +84,8 @@ impl<'a> PermissionCalculator<'a> { Ok(0) } } - Channel::TextChannel { server, .. } => { + Channel::TextChannel { server, .. } + | Channel::VoiceChannel { server, .. } => { let server = Ref::from_unchecked(server.clone()).fetch_server().await?; if self.perspective.id == server.owner { @@ -92,6 +93,7 @@ impl<'a> PermissionCalculator<'a> { } else { Ok(ChannelPermission::View + ChannelPermission::SendMessage + + ChannelPermission::VoiceCall + ChannelPermission::InviteOthers) } } diff --git a/src/notifications/events.rs b/src/notifications/events.rs index b26b7c5..3cc091c 100644 --- a/src/notifications/events.rs +++ b/src/notifications/events.rs @@ -178,7 +178,8 @@ pub async fn prehandle_hook(notification: &ClientboundNotification) -> Result<() subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok(); } } - Channel::TextChannel { server, .. } => { + Channel::TextChannel { server, .. } + | Channel::VoiceChannel { server, .. } => { // ! FIXME: write a better algorithm? let members = Server::fetch_member_ids(server).await?; for member in members { diff --git a/src/routes/channels/delete_channel.rs b/src/routes/channels/delete_channel.rs index d11d8fe..6bff3df 100644 --- a/src/routes/channels/delete_channel.rs +++ b/src/routes/channels/delete_channel.rs @@ -105,7 +105,8 @@ pub async fn req(user: User, target: Ref) -> Result<()> { Ok(()) } - Channel::TextChannel { .. } => { + Channel::TextChannel { .. } | + Channel::VoiceChannel { .. } => { if perm.get_manage_channel() { target.delete().await } else { diff --git a/src/routes/channels/invite_create.rs b/src/routes/channels/invite_create.rs index eaef595..0018b3b 100644 --- a/src/routes/channels/invite_create.rs +++ b/src/routes/channels/invite_create.rs @@ -30,7 +30,8 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { Channel::Group { .. } => { unimplemented!() } - Channel::TextChannel { id, server, .. } => { + Channel::TextChannel { id, server, .. } + | Channel::VoiceChannel { id, server, .. } => { Invite::Server { code: code.clone(), creator: user.id, diff --git a/src/routes/channels/message_delete.rs b/src/routes/channels/message_delete.rs index fa794d7..c0f3ac5 100644 --- a/src/routes/channels/message_delete.rs +++ b/src/routes/channels/message_delete.rs @@ -6,6 +6,7 @@ use mongodb::bson::doc; #[delete("/<target>/messages/<msg>")] pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> { let channel = target.fetch_channel().await?; + channel.has_messaging()?; let perm = permissions::PermissionCalculator::new(&user) .with_channel(&channel) diff --git a/src/routes/channels/message_edit.rs b/src/routes/channels/message_edit.rs index 19d95be..a356a68 100644 --- a/src/routes/channels/message_edit.rs +++ b/src/routes/channels/message_edit.rs @@ -19,6 +19,8 @@ pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result< .map_err(|error| Error::FailedValidation { error })?; let channel = target.fetch_channel().await?; + channel.has_messaging()?; + let perm = permissions::PermissionCalculator::new(&user) .with_channel(&channel) .for_channel() diff --git a/src/routes/channels/message_fetch.rs b/src/routes/channels/message_fetch.rs index d308c84..c87a333 100644 --- a/src/routes/channels/message_fetch.rs +++ b/src/routes/channels/message_fetch.rs @@ -6,6 +6,7 @@ use rocket_contrib::json::JsonValue; #[get("/<target>/messages/<msg>")] pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> { let channel = target.fetch_channel().await?; + channel.has_messaging()?; let perm = permissions::PermissionCalculator::new(&user) .with_channel(&channel) diff --git a/src/routes/channels/message_query.rs b/src/routes/channels/message_query.rs index e1d67b8..4492e83 100644 --- a/src/routes/channels/message_query.rs +++ b/src/routes/channels/message_query.rs @@ -38,6 +38,7 @@ pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<Json .map_err(|error| Error::FailedValidation { error })?; let target = target.fetch_channel().await?; + target.has_messaging()?; let perm = permissions::PermissionCalculator::new(&user) .with_channel(&target) diff --git a/src/routes/channels/message_query_stale.rs b/src/routes/channels/message_query_stale.rs index 2930a42..5f83081 100644 --- a/src/routes/channels/message_query_stale.rs +++ b/src/routes/channels/message_query_stale.rs @@ -18,6 +18,7 @@ pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonVal } let target = target.fetch_channel().await?; + target.has_messaging()?; let perm = permissions::PermissionCalculator::new(&user) .with_channel(&target) diff --git a/src/routes/channels/message_send.rs b/src/routes/channels/message_send.rs index 9bf0f9c..18259d7 100644 --- a/src/routes/channels/message_send.rs +++ b/src/routes/channels/message_send.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use crate::database::*; use crate::util::result::{Error, Result}; @@ -8,6 +10,12 @@ use serde::{Deserialize, Serialize}; use ulid::Ulid; use validator::Validate; +#[derive(Serialize, Deserialize)] +pub struct Reply { + id: String, + mention: bool +} + #[derive(Validate, Serialize, Deserialize)] pub struct Data { #[validate(length(min = 0, max = 2000))] @@ -17,6 +25,7 @@ pub struct Data { nonce: String, #[validate(length(min = 1, max = 128))] attachments: Option<Vec<String>>, + replies: Option<Vec<Reply>>, } lazy_static! { @@ -25,6 +34,7 @@ lazy_static! { #[post("/<target>/messages", data = "<message>")] pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonValue> { + let message = message.into_inner(); message .validate() .map_err(|error| Error::FailedValidation { error })?; @@ -36,6 +46,8 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal } let target = target.fetch_channel().await?; + target.has_messaging()?; + let perm = permissions::PermissionCalculator::new(&user) .with_channel(&target) .for_channel() @@ -65,42 +77,64 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal } let id = Ulid::new().to_string(); - let attachments = if let Some(ids) = &message.attachments { + + let mut mentions = HashSet::new(); + if let Some(captures) = RE_ULID.captures_iter(&message.content).next() { + // ! FIXME: in the future, verify in group so we can send out push + mentions.insert(captures[1].to_string()); + } + + let mut replies = HashSet::new(); + if let Some(entries) = message.replies { + // ! FIXME: move this to app config + if entries.len() >= 5 { + return Err(Error::TooManyReplies) + } + + for Reply { id, mention } in entries { + let message = Ref::from_unchecked(id) + .fetch_message(&target) + .await?; + + replies.insert(message.id); + + if mention { + mentions.insert(message.author); + } + } + } + + let mut attachments = vec![]; + if let Some(ids) = &message.attachments { // ! FIXME: move this to app config if ids.len() >= 5 { return Err(Error::TooManyAttachments) } - let mut attachments = vec![]; for attachment_id in ids { attachments .push(File::find_and_use(attachment_id, "attachments", "message", &id).await?); } - - Some(attachments) - } else { - None - }; - - let mut mentions = vec![]; - if let Some(captures) = RE_ULID.captures_iter(&message.content).next() { - // ! FIXME: in the future, verify in group so we can send out push - mentions.push(captures[1].to_string()); } - let msg = Message { id, channel: target.id().to_string(), author: user.id, content: Content::Text(message.content.clone()), - attachments, nonce: Some(message.nonce.clone()), edited: None, embeds: None, + + attachments: if attachments.len() > 0 { Some(attachments) } else { None }, mentions: if mentions.len() > 0 { - Some(mentions) + Some(mentions.into_iter().collect::<Vec<String>>()) + } else { + None + }, + replies: if replies.len() > 0 { + Some(replies.into_iter().collect::<Vec<String>>()) } else { None }, diff --git a/src/routes/servers/channel_create.rs b/src/routes/servers/channel_create.rs index d98bec0..c96d1cd 100644 --- a/src/routes/servers/channel_create.rs +++ b/src/routes/servers/channel_create.rs @@ -7,8 +7,22 @@ use serde::{Deserialize, Serialize}; use ulid::Ulid; use validator::Validate; +#[derive(Serialize, Deserialize)] +enum ChannelType { + Text, + Voice +} + +impl Default for ChannelType { + fn default() -> Self { + ChannelType::Text + } +} + #[derive(Validate, Serialize, Deserialize)] pub struct Data { + #[serde(rename = "type", default = "ChannelType::default")] + channel_type: ChannelType, #[validate(length(min = 1, max = 32))] name: String, #[validate(length(min = 0, max = 1024))] @@ -30,7 +44,7 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<JsonValue> .for_server() .await?; - if !perm.get_manage_server() { + if !perm.get_manage_channels() { Err(Error::MissingPermission)? } @@ -52,15 +66,26 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<JsonValue> } let id = Ulid::new().to_string(); - let channel = Channel::TextChannel { - id: id.clone(), - server: target.id.clone(), - nonce: Some(info.nonce), + let channel = match info.channel_type { + ChannelType::Text => Channel::TextChannel { + id: id.clone(), + server: target.id.clone(), + nonce: Some(info.nonce), + + name: info.name, + description: info.description, + icon: None, + last_message: None, + }, + ChannelType::Voice => Channel::VoiceChannel { + id: id.clone(), + server: target.id.clone(), + nonce: Some(info.nonce), - name: info.name, - description: info.description, - icon: None, - last_message: None, + name: info.name, + description: info.description, + icon: None + } }; channel.clone().publish().await?; diff --git a/src/util/result.rs b/src/util/result.rs index e0df975..218a536 100644 --- a/src/util/result.rs +++ b/src/util/result.rs @@ -26,9 +26,11 @@ pub enum Error { // ? Channel related errors. UnknownChannel, UnknownAttachment, + UnknownMessage, CannotEditMessage, CannotJoinCall, TooManyAttachments, + TooManyReplies, EmptyMessage, CannotRemoveYourself, GroupTooLarge { @@ -79,10 +81,12 @@ impl<'r> Responder<'r, 'static> for Error { Error::NotFriends => Status::Forbidden, Error::UnknownChannel => Status::NotFound, + Error::UnknownMessage => Status::NotFound, Error::UnknownAttachment => Status::BadRequest, Error::CannotEditMessage => Status::Forbidden, Error::CannotJoinCall => Status::BadRequest, Error::TooManyAttachments => Status::BadRequest, + Error::TooManyReplies => Status::BadRequest, Error::EmptyMessage => Status::UnprocessableEntity, Error::CannotRemoveYourself => Status::BadRequest, Error::GroupTooLarge { .. } => Status::Forbidden, diff --git a/src/version.rs b/src/version.rs index 0c8c63d..0625890 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "0.5.0-alpha.5"; +pub const VERSION: &str = "0.5.1-alpha.0"; -- GitLab