diff --git a/src/api_data/mod.rs b/src/api_data/mod.rs index 1f43be5..883a8be 100644 --- a/src/api_data/mod.rs +++ b/src/api_data/mod.rs @@ -67,4 +67,5 @@ pub mod call_peer_ready; pub mod call_peer_interrupted_streaming; pub mod res_check_password_token; pub mod removed_user_from_conv_message; -pub mod user_is_writing_message_in_conversation; \ No newline at end of file +pub mod user_is_writing_message_in_conversation; +pub mod res_create_conversation_for_group; \ No newline at end of file diff --git a/src/api_data/res_create_conversation_for_group.rs b/src/api_data/res_create_conversation_for_group.rs new file mode 100644 index 0000000..f93a074 --- /dev/null +++ b/src/api_data/res_create_conversation_for_group.rs @@ -0,0 +1,20 @@ +//! # Conversation for group creation result +//! +//! This structure returns the ID of the create conversation + +use serde::Serialize; + +use crate::data::conversation::ConvID; + +#[derive(Serialize)] +pub struct ResCreateConversationForGroup { + conv_id: u64 +} + +impl ResCreateConversationForGroup { + pub fn new(conv_id: ConvID) -> Self { + Self { + conv_id: conv_id.id() + } + } +} \ No newline at end of file diff --git a/src/controllers/conversations_controller.rs b/src/controllers/conversations_controller.rs index 4abebeb..8d4964c 100644 --- a/src/controllers/conversations_controller.rs +++ b/src/controllers/conversations_controller.rs @@ -45,6 +45,7 @@ pub fn create(r: &mut HttpRequestHandler) -> RequestResult { can_everyone_add_members: r.post_bool_opt("canEveryoneAddMembers", true), color: r.post_color_opt("color")?, group_id: None, + group_min_membership_level: None, logo: None, }; @@ -146,7 +147,7 @@ pub fn add_member(r: &mut HttpRequestHandler) -> RequestResult { r.bad_request("This user is already a member of this conversation!".to_string())?; } - conversations_helper::add_member(conv.id, &user_to_add, true, false, r.user_id_ref()?)?; + conversations_helper::add_member(conv.id, &user_to_add, true, false, Some(r.user_id_ref()?))?; r.success("The user was added to the conversation!") } @@ -194,7 +195,7 @@ pub fn remove_member(r: &mut HttpRequestHandler) -> RequestResult { r.bad_request("This user is not a member of this conversation!".to_string())?; } - conversations_helper::remove_member(&user_to_remove, conv.id, r.user_id_ref()?)?; + conversations_helper::remove_member(&user_to_remove, conv.id, Some(r.user_id_ref()?))?; r.ok() } @@ -225,6 +226,7 @@ pub fn find_private(r: &mut HttpRequestHandler) -> RequestResult { color: None, logo: None, group_id: None, + group_min_membership_level: None, }; let conv_id = conversations_helper::create(&new_conv)?; list.push(conv_id); diff --git a/src/controllers/groups_controller.rs b/src/controllers/groups_controller.rs index 65ff0d0..ae77ffd 100644 --- a/src/controllers/groups_controller.rs +++ b/src/controllers/groups_controller.rs @@ -8,20 +8,35 @@ use crate::api_data::advanced_group_api::AdvancedGroupApi; use crate::api_data::group_api::GroupApi; use crate::api_data::group_member_api::GroupMemberAPI; use crate::api_data::res_change_group_logo::ResChangeGroupLogo; +use crate::api_data::res_create_conversation_for_group::ResCreateConversationForGroup; use crate::api_data::res_create_group::GroupCreationResult; use crate::constants::{DEFAULT_GROUP_LOGO, PATH_GROUPS_LOGOS}; use crate::data::base_request_handler::BaseRequestHandler; +use crate::data::error::Res; use crate::data::group::{Group, GroupAccessLevel, GroupPostsCreationLevel, GroupRegistrationLevel, GroupVisibilityLevel}; use crate::data::group_id::GroupID; use crate::data::group_member::{GroupMember, GroupMembershipLevel}; use crate::data::http_request_handler::HttpRequestHandler; use crate::data::new_group::NewGroup; use crate::data::notification::NotifEventType; -use crate::helpers::{groups_helper, notifications_helper, virtual_directory_helper}; +use crate::helpers::{conversations_helper, groups_helper, notifications_helper, virtual_directory_helper}; use crate::helpers::virtual_directory_helper::VirtualDirType; use crate::routes::RequestResult; use crate::utils::date_utils::time; +impl HttpRequestHandler { + /// Get membership level for a conversation + pub fn post_group_membership_level_for_conversation(&mut self, name: &str) -> Res { + let level = GroupMembershipLevel::from_api(&self.post_string(name)?); + + if !level.is_at_least_member() { + self.bad_request("Specified membership level is not enough!".to_string())?; + } + + Ok(level) + } +} + /// Create a new group pub fn create(r: &mut HttpRequestHandler) -> RequestResult { let new_group = NewGroup { @@ -153,6 +168,17 @@ pub fn delete_logo(r: &mut HttpRequestHandler) -> RequestResult { r.set_response(ResChangeGroupLogo::new(DEFAULT_GROUP_LOGO)) } +/// Create a new group's conversation +pub fn create_conversation(r: &mut HttpRequestHandler) -> RequestResult { + let group = r.post_group_id_with_access("group_id", GroupAccessLevel::ADMIN_ACCESS)?; + let min_membership_level = r.post_group_membership_level_for_conversation("min_membership_level")?; + let name = r.post_string("name")?; + + let conv_id = conversations_helper::create_conversation_for_group(group, min_membership_level, &name)?; + + r.set_response(ResCreateConversationForGroup::new(conv_id)) +} + /// Get the list of members of a group pub fn get_members(r: &mut HttpRequestHandler) -> RequestResult { let group_id = r.post_group_id("id")?; diff --git a/src/data/conversation.rs b/src/data/conversation.rs index 9064f99..c53ed4b 100644 --- a/src/data/conversation.rs +++ b/src/data/conversation.rs @@ -4,6 +4,7 @@ use crate::data::group_id::GroupID; use crate::data::user::UserID; +use crate::data::group_member::GroupMembershipLevel; #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash)] pub struct ConvID(u64); @@ -38,6 +39,7 @@ pub struct Conversation { pub logo: Option, pub creation_time: u64, pub group_id: Option, + pub min_group_membership_level: Option, pub can_everyone_add_members: bool, pub last_activity: u64, pub members: Vec, @@ -59,6 +61,11 @@ impl Conversation { /// Check out whether this conversation is managed or not pub fn is_managed(&self) -> bool { + self.is_linked_to_group() + } + + /// Check if this conversation is linked to a group + pub fn is_linked_to_group(&self) -> bool { self.group_id.is_some() } diff --git a/src/data/group_member.rs b/src/data/group_member.rs index 711ed72..ede51fc 100644 --- a/src/data/group_member.rs +++ b/src/data/group_member.rs @@ -5,7 +5,7 @@ use crate::data::group_id::GroupID; use crate::data::user::UserID; -#[derive(PartialEq, Eq, PartialOrd)] +#[derive(PartialEq, Eq, PartialOrd, Debug)] pub enum GroupMembershipLevel { ADMINISTRATOR = 0, MODERATOR = 1, @@ -20,6 +20,13 @@ pub enum GroupMembershipLevel { } impl GroupMembershipLevel { + pub fn is_at_least_member(&self) -> bool { + matches!( + &self, + GroupMembershipLevel::ADMINISTRATOR | GroupMembershipLevel::MODERATOR | GroupMembershipLevel::MEMBER + ) + } + pub fn to_api(&self) -> String { match self { GroupMembershipLevel::ADMINISTRATOR => "administrator", @@ -54,6 +61,10 @@ pub struct GroupMember { } impl GroupMember { + pub fn is_admin(&self) -> bool { + self.level == GroupMembershipLevel::ADMINISTRATOR + } + /// Check if a member of a group is a least a moderator of this group pub fn is_moderator(&self) -> bool { self.level <= GroupMembershipLevel::MODERATOR diff --git a/src/data/new_conversation.rs b/src/data/new_conversation.rs index 7175faa..e05797a 100644 --- a/src/data/new_conversation.rs +++ b/src/data/new_conversation.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; use crate::data::group_id::GroupID; +use crate::data::group_member::GroupMembershipLevel; use crate::data::user::UserID; #[derive(Debug)] @@ -12,6 +13,7 @@ pub struct NewConversation { pub owner_id: UserID, pub name: Option, pub group_id: Option, + pub group_min_membership_level: Option, pub color: Option, pub logo: Option, pub owner_following: bool, diff --git a/src/helpers/conversations_helper.rs b/src/helpers/conversations_helper.rs index d8c3eeb..90ac08e 100644 --- a/src/helpers/conversations_helper.rs +++ b/src/helpers/conversations_helper.rs @@ -6,10 +6,12 @@ use crate::constants::database_tables_names::{CONV_LIST_TABLE, CONV_MEMBERS_TABL use crate::data::conversation::{Conversation, ConversationMember, ConvID, NewConversationSettings}; use crate::data::conversation_message::{ConversationMessage, ConversationMessageFile, ConversationServerMessageType, UserAddedAnotherUserToConversation, UserRemovedAnotherUserToConversation}; use crate::data::error::{ExecError, Res, ResultBoxError}; +use crate::data::group_id::GroupID; +use crate::data::group_member::GroupMembershipLevel; use crate::data::new_conversation::NewConversation; use crate::data::new_conversation_message::NewConversationMessage; use crate::data::user::{User, UserID}; -use crate::helpers::{database, events_helper}; +use crate::helpers::{database, events_helper, groups_helper}; use crate::helpers::database::{InsertQuery, UpdateInfo}; use crate::helpers::events_helper::Event; use crate::utils::date_utils::time; @@ -26,25 +28,47 @@ pub fn create(conv: &NewConversation) -> Res { .add_opt_str("logo", Option::from(&conv.logo)) .add_legacy_bool("can_everyone_add_members", conv.can_everyone_add_members) .add_opt_group_id("group_id", conv.group_id.clone()) + .add_opt_u32("min_group_membership_level", + conv.group_min_membership_level.as_ref().map(|s| s.to_db())) .insert()? .map(|i| ConvID::new(i)) .ok_or(ExecError::new("missing result conv id!"))?; - // Add the creator of the conversation - add_member(conv_id, &conv.owner_id, conv.owner_following, true, &conv.owner_id)?; + // Initialize the list of members of the group + if conv.group_id.is_some() { + update_members_list_for_group_conversation(conv_id)?; + } else { + // Add the creator of the conversation + add_member(conv_id, &conv.owner_id, conv.owner_following, true, Some(&conv.owner_id))?; - // Add other members to the conversation - for member in &conv.members { - if !member.eq(&conv.owner_id) { - add_member(conv_id, member, true, false, &conv.owner_id)?; + // Add other members to the conversation + for member in &conv.members { + if !member.eq(&conv.owner_id) { + add_member(conv_id, member, true, false, Some(&conv.owner_id))?; + } } } Ok(conv_id) } +/// Create a conversation for a group +pub fn create_conversation_for_group(group_id: GroupID, min_membership_level: GroupMembershipLevel, name: &String) -> Res { + create(&NewConversation { + owner_id: UserID::invalid(), + name: Some(name.to_string()), + group_id: Some(group_id), + group_min_membership_level: Some(min_membership_level), + color: None, + logo: None, + owner_following: false, + members: Default::default(), + can_everyone_add_members: false, + }) +} + /// Add a member to a conversation -pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool, adder: &UserID) -> Res { +pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool, adder: Option<&UserID>) -> Res { InsertQuery::new(CONV_MEMBERS_TABLE) .add_conv_id("conv_id", conv_id) .add_user_id("user_id", user_id) @@ -54,21 +78,25 @@ pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: boo .add_u64("last_message_seen", 0) .insert()?; - // Create a message - if adder != user_id { - send_message( - &NewConversationMessage::new_server_message( - conv_id, - ConversationServerMessageType::UserAddedAnotherUserToConversation(UserAddedAnotherUserToConversation { - user_who_added: adder.clone(), - user_added: user_id.clone(), - }), - ) - )?; - } else { - send_message(&NewConversationMessage::new_server_message( - conv_id, ConversationServerMessageType::UserCreatedConversation(user_id.clone()), - ))?; + // Send the messages (if possible, no messages are created for groups conversations) + if let Some(adder) = adder { + + // Create a message + if adder != user_id { + send_message( + &NewConversationMessage::new_server_message( + conv_id, + ConversationServerMessageType::UserAddedAnotherUserToConversation(UserAddedAnotherUserToConversation { + user_who_added: adder.clone(), + user_added: user_id.clone(), + }), + ) + )?; + } else { + send_message(&NewConversationMessage::new_server_message( + conv_id, ConversationServerMessageType::UserCreatedConversation(user_id.clone()), + ))?; + } } Ok(()) @@ -427,10 +455,50 @@ pub fn remove_user_from_conversation(user_id: &UserID, conv: &Conversation, remo if conv.is_last_admin(user_id) { delete_conversation(conv) } else { - remove_member(user_id, conv.id, remover) + remove_member(user_id, conv.id, Some(remover)) } } +/// Update the list of members for a group conversation +pub fn update_members_list_for_group_conversation(conv_id: ConvID) -> Res { + let conv = get_single(conv_id)?; + + if !conv.is_linked_to_group() { + return Err(ExecError::boxed_new("Attempted to update members list for a non-group conversation!")); + } + + let group_members = groups_helper::get_list_members(conv.group_id.as_ref().unwrap())?; + + // Add missing memberships / Update existing invalid memberships + for member in &group_members { + let conv_member = conv.members.iter().filter(|f| f.user_id == member.user_id).next(); + + if let Some(conv_member) = conv_member { + // Update admin status, if required + if conv_member.is_admin != member.is_admin() { + set_admin(&conv_id, &member.user_id, member.is_admin())?; + } + } + + // Create the member + else if conv.min_group_membership_level.as_ref().unwrap() >= &member.level { + add_member(conv_id, &member.user_id, true, member.is_admin(), None)?; + } + } + + // Remove memberships that have to be removed + for conv_member in &conv.members { + let member = group_members.iter().filter(|m| m.user_id == conv_member.user_id).next(); + + // Remove the member, if required + if member.is_none() || conv.min_group_membership_level.as_ref().unwrap() < &member.unwrap().level { + remove_member(&conv_member.user_id, conv_id, None)?; + } + } + + Ok(()) +} + /// Remove permanently a conversation pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> { // Delete all the messages of the conversations @@ -460,7 +528,7 @@ pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> { } /// Delete a conversation membership -pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: &UserID) -> ResultBoxError<()> { +pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: Option<&UserID>) -> ResultBoxError<()> { for msg in get_user_messages_for_conversations(conv_id, user_id)? { delete_message(&msg)?; } @@ -472,19 +540,21 @@ pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: &UserID) -> Res .exec()?; // Create a message - if remover == user_id { - send_message(&NewConversationMessage::new_server_message( - conv_id, - ConversationServerMessageType::UserLeftConversation(user_id.clone()), - ))?; - } else { - send_message(&NewConversationMessage::new_server_message( - conv_id, - ConversationServerMessageType::UserRemovedFromConversation(UserRemovedAnotherUserToConversation { - user_who_removed: remover.clone(), - user_removed: user_id.clone(), - }), - ))?; + if let Some(remover) = remover { + if remover == user_id { + send_message(&NewConversationMessage::new_server_message( + conv_id, + ConversationServerMessageType::UserLeftConversation(user_id.clone()), + ))?; + } else { + send_message(&NewConversationMessage::new_server_message( + conv_id, + ConversationServerMessageType::UserRemovedFromConversation(UserRemovedAnotherUserToConversation { + user_who_removed: remover.clone(), + user_removed: user_id.clone(), + }), + ))?; + } } // Propagate event @@ -537,6 +607,8 @@ fn db_to_conversation_info(row: &database::RowResult) -> ResultBoxError RowResult<'a> { } } + pub fn get_optional_u32(&self, name: &str) -> ResultBoxError> { + match self.is_null(name)? { + true => Ok(None), + false => Ok(Some(self.get_u32(name)?)) + } + } + /// Get an optional unsigned number => Set to None if value is null / empty / 0 pub fn get_optional_positive_u64(&self, name: &str) -> ResultBoxError> { Ok(match self.get_optional_u64(name)? { @@ -615,6 +622,14 @@ impl InsertQuery { self } + /// Add an optional number. If None, Null will be inserted + pub fn add_opt_u32(mut self, key: &str, value: Option) -> InsertQuery { + self.values.insert(key.to_string(), value + .map(|u| Value::UInt(u as u64)) + .unwrap_or(Value::NULL)); + self + } + /// Add an optional user ID. If None, Null will be inserted pub fn add_opt_user_id(self, key: &str, value: Option) -> InsertQuery { self.add_opt_u64(key, value.map(|u| u.id())) diff --git a/src/helpers/groups_helper.rs b/src/helpers/groups_helper.rs index 9f5e61f..591a64c 100644 --- a/src/helpers/groups_helper.rs +++ b/src/helpers/groups_helper.rs @@ -487,6 +487,8 @@ pub fn delete(group_id: &GroupID) -> ResultBoxError { // Delete all group related notifications notifications_helper::delete_all_related_with_group(group_id)?; + // TODO : delete all conversations related with the group + // Delete all group members database::DeleteQuery::new(GROUPS_MEMBERS_TABLE) .cond_group_id("groups_id", group_id) diff --git a/src/routes.rs b/src/routes.rs index 4166f70..f9c5f7a 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -227,6 +227,7 @@ pub fn get_routes() -> Vec { Route::post("/groups/checkVirtualDirectory", Box::new(groups_controller::check_virtual_dir)), Route::post("/groups/upload_logo", Box::new(groups_controller::upload_logo)), Route::post("/groups/delete_logo", Box::new(groups_controller::delete_logo)), + Route::post("/groups/create_conversation", Box::new(groups_controller::create_conversation)), Route::post("/groups/get_members", Box::new(groups_controller::get_members)), Route::post("/groups/invite", Box::new(groups_controller::invite_user)), Route::post("/groups/cancel_invitation", Box::new(groups_controller::cancel_invitation)),