From 4270f0c5d4b37a4bd622ce3d3bee936d1cfc7438 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 3 Sep 2023 18:12:08 +0100 Subject: [PATCH] feat(core): implement create group --- Cargo.lock | 1 + .../database/src/models/channels/model.rs | 37 ++++- .../core/database/src/models/users/model.rs | 15 ++ crates/core/models/src/v0/channels.rs | 27 +++- crates/delta/Cargo.toml | 1 + crates/delta/src/routes/bots/invite.rs | 24 ++-- .../delta/src/routes/channels/group_create.rs | 134 ++++++++++-------- crates/delta/src/routes/channels/mod.rs | 2 +- 8 files changed, 161 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff7130cb..dcceb406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3591,6 +3591,7 @@ dependencies = [ "redis-kiss", "regex", "reqwest", + "revolt-config", "revolt-database", "revolt-models", "revolt-permissions", diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index ac07e16d..ad92cb2e 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use revolt_models::v0::MessageAuthor; +use revolt_models::v0::{self, MessageAuthor}; use revolt_permissions::OverrideField; use revolt_result::Result; use serde::{Deserialize, Serialize}; @@ -168,7 +168,7 @@ auto_derived!( #[allow(clippy::disallowed_methods)] impl Channel { - /// Create a channel + /* /// Create a channel pub async fn create(&self, db: &Database) -> Result<()> { db.insert_channel(self).await?; @@ -186,6 +186,39 @@ impl Channel { } Ok(()) + }*/ + + /// Create a group + pub async fn create_group( + db: &Database, + data: v0::DataCreateGroup, + owner_id: String, + ) -> Result { + let recipients = data.users.into_iter().collect::>(); + let channel = Channel::Group { + id: ulid::Ulid::new().to_string(), + + name: data.name, + owner: owner_id, + description: data.description, + recipients: recipients.clone(), + + icon: None, + last_message_id: None, + + permissions: None, + + nsfw: data.nsfw.unwrap_or(false), + }; + + db.insert_channel(&channel).await?; + + let event = EventV1::ChannelCreate(channel.clone().into()); + for recipient in recipients { + event.clone().private(recipient).await; + } + + Ok(channel) } /// Add user to a group diff --git a/crates/core/database/src/models/users/model.rs b/crates/core/database/src/models/users/model.rs index c32e374e..89a10949 100644 --- a/crates/core/database/src/models/users/model.rs +++ b/crates/core/database/src/models/users/model.rs @@ -182,6 +182,21 @@ impl User { Ok(user) } + /// Get the relationship with another user + pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus { + if self.id == user_b { + return RelationshipStatus::User; + } + + if let Some(relations) = &self.relations { + if let Some(relationship) = relations.iter().find(|x| x.id == user_b) { + return relationship.status.clone(); + } + } + + RelationshipStatus::None + } + /// Check whether two users have a mutual connection /// /// This will check if user and user_b share a server or a group. diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index b2f7d3c6..c039802a 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -1,10 +1,11 @@ use super::File; use revolt_permissions::OverrideField; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; auto_derived!( /// Channel + #[serde(tag = "channel_type")] pub enum Channel { /// Personal "Saved Notes" channel which allows users to save messages SavedMessages { @@ -205,4 +206,28 @@ auto_derived!( #[cfg_attr(feature = "serde", serde(default))] pub remove: Option>, } + + /// Create new group + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct DataCreateGroup { + /// Group name + #[validate(length(min = 1, max = 32))] + pub name: String, + /// Group description + #[validate(length(min = 0, max = 1024))] + pub description: Option, + /// Group icon + #[validate(length(min = 1, max = 128))] + pub icon: Option, + /// Array of user IDs to add to the group + /// + /// Must be friends with these users. + #[validate(length(min = 0, max = 49))] + #[serde(default)] + pub users: HashSet, + /// Whether this group is age-restricted + #[serde(skip_serializing_if = "Option::is_none")] + pub nsfw: Option, + } ); diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index ea5aaa81..bc18e98b 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -68,6 +68,7 @@ revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] } revolt-quark = { path = "../quark" } # core +revolt-config = { path = "../core/config" } revolt-database = { path = "../core/database", features = [ "rocket-impl", "redis-is-patched", diff --git a/crates/delta/src/routes/bots/invite.rs b/crates/delta/src/routes/bots/invite.rs index 9677881d..40af6eda 100644 --- a/crates/delta/src/routes/bots/invite.rs +++ b/crates/delta/src/routes/bots/invite.rs @@ -78,20 +78,16 @@ mod test { .await .expect("`Bot`"); - // FIXME: Channel::create_group - let group = Channel::Group { - id: ulid::Ulid::new().to_string(), - name: TestHarness::rand_string(), - owner: user.id.to_string(), - description: None, - last_message_id: None, - icon: None, - nsfw: false, - permissions: None, - recipients: vec![user.id.to_string()], - }; - - group.create(&harness.db).await.unwrap(); + let group = Channel::create_group( + &harness.db, + v0::DataCreateGroup { + name: TestHarness::rand_string(), + ..Default::default() + }, + user.id.to_string(), + ) + .await + .unwrap(); let response = harness .client diff --git a/crates/delta/src/routes/channels/group_create.rs b/crates/delta/src/routes/channels/group_create.rs index b9deeec9..cf4b3d43 100644 --- a/crates/delta/src/routes/channels/group_create.rs +++ b/crates/delta/src/routes/channels/group_create.rs @@ -1,84 +1,94 @@ -use std::{collections::HashSet, iter::FromIterator}; - -use revolt_quark::{ - get_relationship, - models::{user::RelationshipStatus, Channel, User}, - variables::delta::MAX_GROUP_SIZE, - Db, Error, Result, -}; +use revolt_config::config; +use revolt_database::{Channel, Database, RelationshipStatus, User}; +use revolt_models::v0; +use revolt_result::{create_error, Result}; use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; -use ulid::Ulid; +use rocket::State; use validator::Validate; -/// # Group Data -#[derive(Validate, Serialize, Deserialize, JsonSchema)] -pub struct DataCreateGroup { - /// Group name - #[validate(length(min = 1, max = 32))] - name: String, - /// Group description - #[validate(length(min = 0, max = 1024))] - description: Option, - /// Array of user IDs to add to the group - /// - /// Must be friends with these users. - #[validate(length(min = 0, max = 49))] - users: Vec, - /// Whether this group is age-restricted - #[serde(skip_serializing_if = "Option::is_none")] - nsfw: Option, -} - /// # Create Group /// /// Create a new group channel. #[openapi(tag = "Groups")] -#[post("/create", data = "")] -pub async fn req(db: &Db, user: User, info: Json) -> Result> { +#[post("/create", data = "")] +pub async fn create_group( + db: &State, + user: User, + data: Json, +) -> Result> { + let config = config().await; if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } - let info = info.into_inner(); - info.validate() - .map_err(|error| Error::FailedValidation { error })?; + let mut data = data.into_inner(); + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; - let mut set: HashSet = HashSet::from_iter(info.users.into_iter()); - set.insert(user.id.clone()); + data.users.insert(user.id.to_string()); - if set.len() > *MAX_GROUP_SIZE { - return Err(Error::GroupTooLarge { - max: *MAX_GROUP_SIZE, - }); + if data.users.len() > config.features.limits.default.group_size { + return Err(create_error!(GroupTooLarge { + max: config.features.limits.default.group_size, + })); } - for target in &set { - match get_relationship(&user, target) { + for target in &data.users { + match user.relationship_with(target) { RelationshipStatus::Friend | RelationshipStatus::User => {} _ => { - return Err(Error::NotFriends); + return Err(create_error!(NotFriends)); } } } - let group = Channel::Group { - id: Ulid::new().to_string(), - - name: info.name, - owner: user.id, - description: info.description, - recipients: set.into_iter().collect::>(), - - icon: None, - last_message_id: None, - - permissions: None, - - nsfw: info.nsfw.unwrap_or(false), - }; - - group.create(db).await?; - Ok(Json(group)) + Ok(Json(Channel::create_group(db, data, user.id).await?.into())) +} + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn create_group() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let response = harness + .client + .post("/channels/create") + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataCreateBot { + name: TestHarness::rand_string(), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let channel: v0::Channel = response.into_json().await.expect("`Channel`"); + match channel { + v0::Channel::Group { + id, + owner, + recipients, + .. + } => { + assert_eq!(owner, user.id); + assert_eq!(recipients.len(), 1); + assert!(harness.db.fetch_channel(&id).await.is_ok()); + } + _ => unreachable!(), + } + } } diff --git a/crates/delta/src/routes/channels/mod.rs b/crates/delta/src/routes/channels/mod.rs index 6aa6aecd..8dd0b69c 100644 --- a/crates/delta/src/routes/channels/mod.rs +++ b/crates/delta/src/routes/channels/mod.rs @@ -43,7 +43,7 @@ pub fn routes() -> (Vec, OpenApi) { message_edit::req, message_bulk_delete::req, message_delete::req, - group_create::req, + group_create::create_group, group_add_member::req, group_remove_member::req, voice_join::req,