From fbf47283476c590b4c94dbffb7fb466b131e37f4 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 4 Mar 2021 18:51:52 +0100 Subject: [PATCH] Continue refactoring --- src/api_data/account_export_api.rs | 2 +- .../call_peer_interrupted_streaming.rs | 2 +- src/api_data/call_peer_ready.rs | 2 +- src/api_data/conversation_api.rs | 66 +++-- src/api_data/conversation_message_api.rs | 2 +- src/api_data/conversations_refresh_api.rs | 37 --- src/api_data/joined_call_message.rs | 2 +- src/api_data/left_call_message.rs | 2 +- src/api_data/mod.rs | 1 - src/api_data/new_call_signal.rs | 2 +- src/api_data/res_create_conversation.rs | 5 +- .../res_find_private_conversations.rs | 6 +- src/constants.rs | 6 +- src/controllers/calls_controller.rs | 18 +- src/controllers/conversations_controller.rs | 215 +++++++------- src/controllers/user_ws_actions.rs | 5 +- src/data/account_export.rs | 6 +- src/data/base_request_handler.rs | 63 ++++- src/data/conversation.rs | 90 +++++- src/data/conversation_message.rs | 3 +- src/data/new_conversation.rs | 4 + src/data/new_conversation_message.rs | 11 +- src/data/user_membership.rs | 2 +- src/helpers/conversations_helper.rs | 264 ++++++++++-------- src/helpers/database.rs | 50 ++++ src/routes.rs | 2 +- src/utils/string_utils.rs | 14 + 27 files changed, 527 insertions(+), 355 deletions(-) delete mode 100644 src/api_data/conversations_refresh_api.rs diff --git a/src/api_data/account_export_api.rs b/src/api_data/account_export_api.rs index 0422963..96c3103 100644 --- a/src/api_data/account_export_api.rs +++ b/src/api_data/account_export_api.rs @@ -53,7 +53,7 @@ impl AccountExportAPI { conversations_list: ConversationAPI::for_list(&export.conversations), conversations_messages: export.conversation_messages .iter() - .map(|r| (r.0.clone(), ConversationMessageAPI::for_list(r.1))) + .map(|r| (r.0.id(), ConversationMessageAPI::for_list(r.1))) .collect(), friends_list: FriendAPI::from_list(&export.friends_list), groups: export.groups diff --git a/src/api_data/call_peer_interrupted_streaming.rs b/src/api_data/call_peer_interrupted_streaming.rs index 282e695..7f69bf8 100644 --- a/src/api_data/call_peer_interrupted_streaming.rs +++ b/src/api_data/call_peer_interrupted_streaming.rs @@ -17,7 +17,7 @@ pub struct CallPeerInterruptedStreamingAPI { impl CallPeerInterruptedStreamingAPI { pub fn new(call_id: &ConvID, peer_id: &UserID) -> Self { Self { - callID: call_id.clone(), + callID: call_id.id(), peerID: peer_id.id(), } } diff --git a/src/api_data/call_peer_ready.rs b/src/api_data/call_peer_ready.rs index e40dbe1..ffbddd0 100644 --- a/src/api_data/call_peer_ready.rs +++ b/src/api_data/call_peer_ready.rs @@ -16,7 +16,7 @@ pub struct CallPeerReadyAPI { impl CallPeerReadyAPI { pub fn new(call_id: &ConvID, user_id: &UserID) -> Self { Self { - callID: call_id.clone(), + callID: call_id.id(), peerID: user_id.id(), } } diff --git a/src/api_data/conversation_api.rs b/src/api_data/conversation_api.rs index 4ac5d7c..8ed784e 100644 --- a/src/api_data/conversation_api.rs +++ b/src/api_data/conversation_api.rs @@ -1,54 +1,58 @@ //! # Conversation API object //! //! @author Pierre Hubert -use serde::{Serialize, Serializer}; +use serde::Serialize; -use crate::api_data::legacy_api_bool::LegacyBool; use crate::controllers::calls_controller; -use crate::data::conversation::Conversation; +use crate::data::conversation::{Conversation, ConversationMember}; use crate::helpers::calls_helper; -/// Special implementation of conversation name (false if none / the name otherwise) -struct ConvName(Option); - -impl Serialize for ConvName { - fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where - S: Serializer { - match &self.0 { - None => serializer.serialize_bool(false), - Some(n) => serializer.serialize_str(n) - } - } +#[derive(Serialize)] +struct ConversationMembersAPI { + user_id: u64, + last_message_seen: u64, + following: bool, + is_admin: bool, } #[derive(Serialize)] -#[allow(non_snake_case)] pub struct ConversationAPI { - ID: u64, - ID_owner: u64, - last_active: u64, - name: ConvName, - following: LegacyBool, - saw_last_message: LegacyBool, - members: Vec, - canEveryoneAddMembers: bool, + id: u64, + last_activity: u64, + name: Option, + color: Option, + background: Option, + group_id: Option, + members: Vec, + can_everyone_add_members: bool, can_have_call: bool, can_have_video_call: bool, has_call_now: bool, } +impl ConversationMembersAPI { + pub fn new(m: &ConversationMember) -> Self { + Self { + user_id: m.user_id.id(), + last_message_seen: m.last_message_seen, + following: m.following, + is_admin: m.is_admin, + } + } +} + impl ConversationAPI { /// Construct a new Conversation instance pub fn new(conv: &Conversation) -> ConversationAPI { ConversationAPI { - ID: conv.id, - ID_owner: conv.owner_id.id(), - last_active: conv.last_active, - name: ConvName(conv.name.clone()), - following: LegacyBool(conv.following), - saw_last_message: LegacyBool(conv.saw_last_message), - members: conv.members.iter().map(|x| x.id()).collect(), - canEveryoneAddMembers: conv.can_everyone_add_members, + id: conv.id.id(), + last_activity: conv.last_activity, + name: conv.name.clone(), + members: conv.members.iter().map(ConversationMembersAPI::new).collect(), + can_everyone_add_members: conv.can_everyone_add_members, + color: conv.color.clone(), + background: conv.background.clone(), + group_id: conv.group_id.as_ref().map(|i| i.id()), can_have_call: calls_helper::can_have_call(conv), can_have_video_call: calls_helper::can_have_video_calls(conv), diff --git a/src/api_data/conversation_message_api.rs b/src/api_data/conversation_message_api.rs index 99e68d3..cc5f679 100644 --- a/src/api_data/conversation_message_api.rs +++ b/src/api_data/conversation_message_api.rs @@ -79,7 +79,7 @@ impl ConversationMessageAPI { ConversationMessageAPI { id: msg.id, - conv_id: msg.conv_id, + conv_id: msg.conv_id.id(), user_id: msg.user_id.clone().map(|u| u.id()), time_insert: msg.time_sent, message: msg.message.clone(), diff --git a/src/api_data/conversations_refresh_api.rs b/src/api_data/conversations_refresh_api.rs deleted file mode 100644 index 7709e26..0000000 --- a/src/api_data/conversations_refresh_api.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! # Conversation refresh result -//! -//! Note : this structure is now deprecated and should no longer be used for further developments -//! -//! @author Pierre Hubert - -use std::collections::HashMap; - -use serde::Serialize; - -use crate::api_data::conversation_message_api::ConversationMessageAPI; -use crate::data::conversation_message::ConversationMessage; - -#[derive(Serialize)] -#[allow(non_snake_case)] -pub struct ConversationRefreshResultAPI { - #[serde(flatten)] - list: HashMap>, -} - -impl ConversationRefreshResultAPI { - /// Create a new list - pub fn new(list: HashMap>) -> ConversationRefreshResultAPI { - let list = list - .iter() - .map(|v| ( - format!("conversation-{}", v.0), - ConversationMessageAPI::for_list(v.1) - )) - .collect(); - - ConversationRefreshResultAPI { - list - } - } -} - diff --git a/src/api_data/joined_call_message.rs b/src/api_data/joined_call_message.rs index ab114c8..294b0f2 100644 --- a/src/api_data/joined_call_message.rs +++ b/src/api_data/joined_call_message.rs @@ -18,7 +18,7 @@ pub struct JoinedCallMessage { impl JoinedCallMessage { pub fn new(call_id: &ConvID, user_id: &UserID) -> Self { Self { - callID: call_id.clone(), + callID: call_id.id(), userID: user_id.id(), } } diff --git a/src/api_data/left_call_message.rs b/src/api_data/left_call_message.rs index 9d0ea16..57e0f38 100644 --- a/src/api_data/left_call_message.rs +++ b/src/api_data/left_call_message.rs @@ -18,7 +18,7 @@ pub struct LeftCallMessage { impl LeftCallMessage { pub fn new(call_id: &ConvID, user_id: &UserID) -> Self { Self { - callID: call_id.clone(), + callID: call_id.id(), userID: user_id.id(), } } diff --git a/src/api_data/mod.rs b/src/api_data/mod.rs index a0db0cb..dd2b167 100644 --- a/src/api_data/mod.rs +++ b/src/api_data/mod.rs @@ -21,7 +21,6 @@ pub mod conversation_api; mod legacy_api_bool; pub mod res_find_private_conversations; pub mod conversation_message_api; -pub mod conversations_refresh_api; pub mod res_count_unread_conversations; pub mod list_unread_conversations_api; pub mod global_search_result_api; diff --git a/src/api_data/new_call_signal.rs b/src/api_data/new_call_signal.rs index fb8c779..9ae02ce 100644 --- a/src/api_data/new_call_signal.rs +++ b/src/api_data/new_call_signal.rs @@ -19,7 +19,7 @@ pub struct NewCallSignalAPI { impl NewCallSignalAPI { pub fn new(call_id: &ConvID, peer_id: &UserID, data: &str) -> Res { Ok(Self { - callID: call_id.clone(), + callID: call_id.id(), peerID: peer_id.id(), data: serde_json::from_str(data)?, }) diff --git a/src/api_data/res_create_conversation.rs b/src/api_data/res_create_conversation.rs index 3a33445..358e6c3 100644 --- a/src/api_data/res_create_conversation.rs +++ b/src/api_data/res_create_conversation.rs @@ -3,6 +3,7 @@ //! @author Pierre Hubert use serde::{Serialize}; +use crate::data::conversation::ConvID; #[derive(Serialize)] #[allow(non_snake_case)] @@ -13,9 +14,9 @@ pub struct ResCreateConversation { impl ResCreateConversation { /// Construct a new Result instance - pub fn new(conv_id: u64) -> ResCreateConversation { + pub fn new(conv_id: ConvID) -> ResCreateConversation { ResCreateConversation { - conversationID: conv_id, + conversationID: conv_id.id(), success: "The conversation was successfully created!".to_string(), } } diff --git a/src/api_data/res_find_private_conversations.rs b/src/api_data/res_find_private_conversations.rs index a17a1d3..6439a46 100644 --- a/src/api_data/res_find_private_conversations.rs +++ b/src/api_data/res_find_private_conversations.rs @@ -4,6 +4,8 @@ use serde::Serialize; +use crate::data::conversation::ConvID; + #[derive(Serialize)] #[allow(non_snake_case)] pub struct ResFindPrivateConversations { @@ -12,9 +14,9 @@ pub struct ResFindPrivateConversations { impl ResFindPrivateConversations { /// Construct a new instance of this structure - pub fn new(list: Vec) -> ResFindPrivateConversations { + pub fn new(list: Vec) -> ResFindPrivateConversations { ResFindPrivateConversations { - conversationsID: list + conversationsID: list.iter().map(|i| i.id()).collect() } } } \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs index 0efa01d..1f115d3 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -36,7 +36,6 @@ pub mod database_tables_names { pub const CONV_LIST_TABLE: &str = "comunic_conversations_list"; pub const CONV_MEMBERS_TABLE: &str = "comunic_conversations_members"; pub const CONV_MESSAGES_TABLE: &str = "comunic_conversations_messages"; - pub const CONV_INFO_VIEW: &str = "comunic_conversations_info"; /// Posts table pub const POSTS_TABLE: &str = "texte"; @@ -163,4 +162,7 @@ pub const INITIAL_REFRESH_LOAD_INTERVAL: Duration = Duration::from_secs(5); pub const CLEAN_UP_INTERVAL: Duration = Duration::from_secs(60 * 60); /// Minimal mobile version supported -pub const MIN_SUPPORTED_MOBILE_VERSION: &str = "1.1.1"; \ No newline at end of file +pub const MIN_SUPPORTED_MOBILE_VERSION: &str = "1.1.1"; + +/// Minimum message length +pub const MIN_CONVERSATION_MESSAGE_LENGTH: usize = 1; \ No newline at end of file diff --git a/src/controllers/calls_controller.rs b/src/controllers/calls_controller.rs index 5df95e0..c67828c 100644 --- a/src/controllers/calls_controller.rs +++ b/src/controllers/calls_controller.rs @@ -13,7 +13,6 @@ use crate::api_data::joined_call_message::JoinedCallMessage; use crate::api_data::left_call_message::LeftCallMessage; use crate::api_data::new_call_signal::NewCallSignalAPI; use crate::api_data::user_calls_config::UserCallsConfig; -use crate::routes::RequestResult; use crate::controllers::user_ws_controller; use crate::data::base_request_handler::BaseRequestHandler; use crate::data::call_signal::{CallSignal, CloseCallStream, IceCandidate, NewUserCallSignal, SdpType, UserCallOfferRequest}; @@ -27,11 +26,12 @@ use crate::data::user_ws_message::UserWsMessage; use crate::data::user_ws_request_handler::UserWsRequestHandler; use crate::helpers::{calls_helper, conversations_helper, events_helper}; use crate::helpers::events_helper::Event; +use crate::routes::RequestResult; impl UserWsRequestHandler { /// Get the ID of a call included in a WebSocket request fn post_call_id(&mut self, name: &str) -> Res { - let conv_id = self.post_u64(name)?; + let conv_id = ConvID::new(self.post_u64(name)?); if !self.get_conn().is_having_call_with_conversation(&conv_id) { self.forbidden("You do not belong to this call!".to_string())?; @@ -125,7 +125,7 @@ pub fn is_conversation_having_call(conv_id: &ConvID) -> bool { }); if let Err(e) = res { - eprintln!("Failed to check if a conversation is having call! Conversation: {} / Error: {:#?}", conv_id, e); + eprintln!("Failed to check if a conversation is having call! Conversation: {} / Error: {:#?}", conv_id.id(), e); } found @@ -133,10 +133,10 @@ pub fn is_conversation_having_call(conv_id: &ConvID) -> bool { /// Join a call pub fn join_call(r: &mut UserWsRequestHandler) -> RequestResult { - let conv_id = r.post_conv_id("convID")?; + let conv_id = r.post_conv("convID")?.conv_id; // Check if the conversation can have a call - let conv = conversations_helper::get_single(conv_id, r.user_id_ref()?)?; + let conv = conversations_helper::get_single(conv_id)?; if !calls_helper::can_have_call(&conv) { r.forbidden("This conversation can not be used to make calls!".to_string())?; } @@ -177,7 +177,7 @@ pub fn leave_call(r: &mut UserWsRequestHandler) -> RequestResult { // Warning ! For some technical reasons, we do not check if the user // really belongs to the conversation, so be careful when manipulating // conversation ID here - let conv_id = r.post_u64("convID")?; + let conv_id = ConvID::new(r.post_u64("convID")?); // Check if the user was not in the conversation if !r.get_conn().is_having_call_with_conversation(&conv_id) { @@ -208,7 +208,7 @@ pub fn get_members_list(r: &mut UserWsRequestHandler) -> RequestResult { /// Get the hash associated to a call pub fn gen_call_hash(call_id: &ConvID, peer_id: &UserID) -> String { - format!("{}-{}", call_id, peer_id.id()) + format!("{}-{}", call_id.id(), peer_id.id()) } /// Handles client signal @@ -357,7 +357,7 @@ pub fn make_user_leave_call(conv_id: &ConvID, connection: &UserWsConnection) -> // Notify user (if possible) if connection.session.connected() { - user_ws_controller::send_to_client(connection, &UserWsMessage::no_id_message("call_closed", conv_id)?)?; + user_ws_controller::send_to_client(connection, &UserWsMessage::no_id_message("call_closed", conv_id.id())?)?; } // Close main stream (sender) @@ -418,7 +418,7 @@ pub fn handle_event(e: &events_helper::Event) -> Res { return Ok(()); } - let call_id = split[0].parse::()?; + let call_id = ConvID::new(split[0].parse::()?); let peer_id = UserID::new(split[1].parse::()?); let target_user = UserID::new(msg.peer_id.parse::()?); diff --git a/src/controllers/conversations_controller.rs b/src/controllers/conversations_controller.rs index fab12bc..92b2dd7 100644 --- a/src/controllers/conversations_controller.rs +++ b/src/controllers/conversations_controller.rs @@ -2,18 +2,18 @@ //! //! @author Pierre Hubert -use std::collections::HashMap; use crate::api_data::conversation_api::ConversationAPI; use crate::api_data::conversation_message_api::ConversationMessageAPI; -use crate::api_data::conversations_refresh_api::ConversationRefreshResultAPI; use crate::api_data::list_unread_conversations_api::UnreadConversationAPI; use crate::api_data::res_count_unread_conversations::ResultCountUnreadConversations; use crate::api_data::res_create_conversation::ResCreateConversation; use crate::api_data::res_find_private_conversations::ResFindPrivateConversations; -use crate::routes::RequestResult; +use crate::constants::MIN_CONVERSATION_MESSAGE_LENGTH; use crate::controllers::user_ws_controller; use crate::data::base_request_handler::BaseRequestHandler; +use crate::data::conversation::{ConversationMemberSetting, NewConversationSettings}; +use crate::data::conversation_message::ConversationMessageFile; use crate::data::error::Res; use crate::data::http_request_handler::HttpRequestHandler; use crate::data::new_conversation::NewConversation; @@ -23,6 +23,7 @@ use crate::data::user_ws_connection::UserWsConnection; use crate::data::user_ws_message::UserWsMessage; use crate::helpers::{conversations_helper, events_helper, user_helper}; use crate::helpers::events_helper::Event; +use crate::routes::RequestResult; use crate::utils::string_utils::remove_html_nodes; /// Create a new conversation @@ -58,6 +59,9 @@ pub fn create(r: &mut HttpRequestHandler) -> RequestResult { owner_following: r.post_bool("follow")?, members, can_everyone_add_members: r.post_bool_opt("canEveryoneAddMembers", true), + color: r.post_color_opt("color")?, + group_id: None, + background: None,// TODO : add support for background }; // Create the conversation @@ -74,77 +78,94 @@ pub fn get_list(r: &mut HttpRequestHandler) -> RequestResult { /// Get information about a single conversation pub fn get_single(r: &mut HttpRequestHandler) -> RequestResult { - let conversation_id = r.post_conv_id("conversationID")?; - let conv = conversations_helper::get_single(conversation_id, &r.user_id()?)?; + let conv = r.post_conv("conversationID")?; + let conv = conversations_helper::get_single(conv.conv_id)?; r.set_response(ConversationAPI::new(&conv)) } /// Update the settings of a conversation pub fn update_settings(r: &mut HttpRequestHandler) -> RequestResult { - let conv_id = r.post_conv_id("conversationID")?; - let is_moderator = conversations_helper::is_user_moderator(&r.user_id()?, conv_id)?; + let conv_membership = r.post_conv("conversationID")?; + let conv = conversations_helper::get_single(conv_membership.conv_id)?; // Update following state, if required if r.has_post_parameter("following") { conversations_helper::set_following( &r.user_id()?, - conv_id, + conv_membership.member_id, r.post_bool("following")?, )?; } // Update members list - if r.has_post_parameter("members") { - let mut members = r.post_numbers_list("members", 1)? - .iter() - .map(|f| UserID::new(f.clone())) - .collect::>(); + if r.has_post_parameter("members") && !conv.is_managed() { + let mut members = r.post_users_id("members")? + .into_iter() + .map(|user_id| ConversationMemberSetting { user_id, set_admin: false }) + .collect::>(); + let admins = r.post_users_id("admins")?; - let can_everyone_add_members = conversations_helper::can_everyone_add_members(conv_id)?; + let can_everyone_add_members = conversations_helper::can_everyone_add_members(conv_membership.conv_id)?; - if !is_moderator && !can_everyone_add_members { + if !conv_membership.is_admin && !can_everyone_add_members { r.forbidden("You can not update the list of members of this conversation!".to_string())?; } - // Check if the members of the conversation really exists - for member in &members { - if !user_helper::exists(member)? { - r.not_found(format!("User {} not found!", member.id()))?; - } + // Set same admin status as earlier + if !conv_membership.is_admin { + members = members + .into_iter() + .map(|mut n| { + n.set_admin = conv.is_admin(&n.user_id); + n + }) + .collect() } - if !members.contains(&r.user_id()?) { - members.push(r.user_id()?); + // If the user is an admin, use the values he gave + else { + members = members + .into_iter() + .map(|mut n| { + n.set_admin = admins.contains(&n.user_id); + n + }) + .collect() } - conversations_helper::set_members(conv_id, &members, is_moderator)?; + // Current user can not touch his own membership, so revert it back forcefully + members = members + .into_iter() + .filter(|m| !m.user_id.eq(&conv_membership.user_id)) + .collect::>(); + + members.push(ConversationMemberSetting { + user_id: r.user_id()?, + set_admin: conv_membership.is_admin, + }); + + + conversations_helper::set_members(conv_membership.conv_id, &members, conv_membership.is_admin)?; } // Change moderator settings - if r.has_post_parameter("name") || r.has_post_parameter("canEveryoneAddMembers") { - if !is_moderator { + if r.has_post_parameter("name") + && r.has_post_parameter("canEveryoneAddMembers") + && r.has_post_parameter("color") { + if !conv_membership.is_admin { r.forbidden("You are not allowed to perform changes on this conversation!".to_string())?; } - // Change conversation name - if r.has_post_parameter("name") { - let name = r.post_string_opt("name", 0, true)?; - let name = if name.eq("false") || name.is_empty() { - None - } else { - Some(remove_html_nodes(&name)) - }; - conversations_helper::set_name(conv_id, name)?; - } + let new_settings = NewConversationSettings { + conv_id: conv_membership.conv_id, + name: r.post_string_optional("name").map(|s| remove_html_nodes(&s)), + color: r.post_color_opt("color")?, + can_everyone_add_members: r.post_bool("canEveryoneAddMembers")?, + }; - - // Change "canEveryoneAddMembers" parameter - if r.has_post_parameter("canEveryoneAddMembers") { - conversations_helper::set_can_everyone_add_members( - conv_id, r.post_bool("canEveryoneAddMembers")?)?; - } + conversations_helper::set_settings(new_settings)?; } r.success("Conversation information successfully updated!") @@ -169,6 +190,9 @@ pub fn find_private(r: &mut HttpRequestHandler) -> RequestResult { owner_following: true, members: vec![r.user_id()?, other_user], can_everyone_add_members: true, + color: r.post_color_opt("color")?, + background: None, + group_id: None, }; let conv_id = conversations_helper::create(&new_conv)?; list.push(conv_id); @@ -177,80 +201,25 @@ pub fn find_private(r: &mut HttpRequestHandler) -> RequestResult { r.set_response(ResFindPrivateConversations::new(list)) } -/// DEPRECATED : refresh current user conversations -/// -/// This method was used only by ComunicWeb before the introduction of WebSockets -pub fn refresh_list(r: &mut HttpRequestHandler) -> RequestResult { - let mut list = HashMap::new(); - - // Check for new conversations - if r.has_post_parameter("newConversations") { - for conv_id in r.post_numbers_list("newConversations", 0)? { - if !conversations_helper::does_user_belongs_to(&r.user_id()?, conv_id)? { - r.forbidden(format!("Your do not belongs to conversation {} !", conv_id))?; - } - - let list_conv = conversations_helper::get_last_messages(conv_id, 10)?; - list.insert(conv_id as u64, list_conv); - - conversations_helper::mark_user_seen(conv_id as u64, &r.user_id()?)?; - } - } - - // Check for refresh of already initialized conversations - if r.has_post_parameter("toRefresh") { - let v: HashMap> = - serde_json::from_str(r.post_string("toRefresh")?.as_str())?; - - for (k, v) in v { - // Get conversation ID - if !k.starts_with("conversation-") { - return r.bad_request("Entries of 'toRefresh' must start with 'conversation-'!".to_string()); - } - let conv_id = k.replace("conversation-", "").parse::()?; - - // Extract last message id - if !v.contains_key("last_message_id") { - return r.bad_request(format!("Missing 'last_message_id' in conversation {}!", conv_id)); - } - let last_message_id = v["last_message_id"].as_u64().unwrap_or(0); - - // Check user rights - if !conversations_helper::does_user_belongs_to(&r.user_id()?, conv_id)? { - return r.forbidden(format!("You do not belong to conversation {}!", conv_id)); - } - - let list_conv = conversations_helper::get_new_messages(conv_id, last_message_id)?; - list.insert(conv_id, list_conv); - - conversations_helper::mark_user_seen(conv_id as u64, &r.user_id()?)?; - } - } - - r.set_response(ConversationRefreshResultAPI::new(list)) -} - /// Refresh a single conversation pub fn refresh_single(r: &mut HttpRequestHandler) -> RequestResult { - let conv_id = r.post_conv_id("conversationID")?; + let conv = r.post_conv("conversationID")?; let last_message_id = r.post_u64("last_message_id")?; let messages = match last_message_id { // Get latest messages of the conversation - 0 => conversations_helper::get_last_messages(conv_id, 10)?, + 0 => conversations_helper::get_last_messages(conv.member_id, 10)?, // Get new messages - _ => conversations_helper::get_new_messages(conv_id, last_message_id)?, + _ => conversations_helper::get_new_messages(conv.member_id, last_message_id)?, }; - conversations_helper::mark_user_seen(conv_id, &r.user_id()?)?; - r.set_response(ConversationMessageAPI::for_list(&messages)) } /// Get older messages of a conversation pub fn get_older_messages(r: &mut HttpRequestHandler) -> RequestResult { - let conv_id = r.post_conv_id("conversationID")?; + let conv = r.post_conv("conversationID")?; let max_id = r.post_u64("oldest_message_id")? - 1; // Determine limit @@ -261,31 +230,44 @@ pub fn get_older_messages(r: &mut HttpRequestHandler) -> RequestResult { _ => 60 }; - let messages = conversations_helper::get_older_messages(conv_id, max_id, limit)?; + let messages = conversations_helper::get_older_messages(conv.member_id, max_id, limit)?; r.set_response(ConversationMessageAPI::for_list(&messages)) } /// Send a new message pub fn send_message(r: &mut HttpRequestHandler) -> RequestResult { - let conv_id = r.post_conv_id("conversationID")?; - let message = r.post_string_without_html("message", 0, false)?; + let conv = r.post_conv("conversationID")?; + + // TODO : add support for other files type // Get image - let image_path = match r.has_file("image") { + let file = match r.has_file("image") { false => None, - true => Some(r.save_post_image("image", "conversations", 1200, 1200)?) + true => { + let path = r.save_post_image("image", "conversations", 1200, 1200)?; + Some(ConversationMessageFile { + path: path.clone(), + size: std::fs::metadata(&path)?.len(), + name: "picture.png".to_string(), + thumbnail: Some(r.save_post_image("image", "conversations", 50, 50)?), + r#type: "image/png".to_string(), + }) + } }; - if image_path == None && message.len() < 3 { - r.bad_request("Message is empty!".to_string())?; - } + let message = if let None = file { + Some(r.post_string_without_html("message", MIN_CONVERSATION_MESSAGE_LENGTH, true)?) + } else { + None + }; conversations_helper::send_message(&NewConversationMessage { - user_id: r.user_id()?, - conv_id, + user_id: Some(r.user_id()?), + conv_id: conv.conv_id, message, - image_path, + file, + server_message: None, })?; r.success("Conversation message was successfully sent!") @@ -307,9 +289,14 @@ pub fn list_unread(r: &mut HttpRequestHandler) -> RequestResult { /// Delete a conversation pub fn delete_conversation(r: &mut HttpRequestHandler) -> RequestResult { - let conv_id = r.post_conv_id("conversationID")?; + let conv_membership = r.post_conv("conversationID")?; + let conv = conversations_helper::get_single(conv_membership.conv_id)?; - conversations_helper::remove_user_from_conversation(&r.user_id()?, conv_id)?; + if conv.is_managed() { + r.bad_request("This conversation is managed, it can not be deleted by this way!".to_string())?; + } + + conversations_helper::remove_user_from_conversation(&r.user_id()?, &conv, r.user_id_ref()?)?; r.success("The conversation has been deleted") } @@ -362,7 +349,7 @@ pub fn handle_event(e: &events_helper::Event) -> Res { user_ws_controller::send_message_to_specific_connections( |f| f.conversations.contains(&msg.conv_id), |_| UserWsMessage::no_id_message("new_conv_message", ConversationMessageAPI::new(msg)), - Some(|conn: &UserWsConnection| conversations_helper::mark_user_seen(msg.conv_id, conn.user_id())), + Some(|conn: &UserWsConnection| conversations_helper::mark_user_seen(msg.conv_id, conn.user_id(), msg.id)), )?; } diff --git a/src/controllers/user_ws_actions.rs b/src/controllers/user_ws_actions.rs index 8d92cb6..6b1e488 100644 --- a/src/controllers/user_ws_actions.rs +++ b/src/controllers/user_ws_actions.rs @@ -6,6 +6,7 @@ use crate::data::base_request_handler::BaseRequestHandler; use crate::data::error::Res; use crate::data::post::PostAccessLevel; use crate::data::user_ws_request_handler::UserWsRequestHandler; +use crate::data::conversation::ConvID; /// Update incognito status of the connection pub fn set_incognito(r: &mut UserWsRequestHandler) -> Res { @@ -17,14 +18,14 @@ pub fn set_incognito(r: &mut UserWsRequestHandler) -> Res { /// Register a conversation pub fn register_conv(r: &mut UserWsRequestHandler) -> Res { - let conv_id = r.post_conv_id("convID")?; + let conv_id = r.post_conv("convID")?.conv_id; r.update_conn(|c| { c.conversations.insert(conv_id); })?; r.success("ok") } /// Un-register a conversation pub fn unregister_conv(r: &mut UserWsRequestHandler) -> Res { - let conv_id = r.post_u64("convID")?; + let conv_id = ConvID::new(r.post_u64("convID")?); r.update_conn(|c| { c.conversations.remove(&conv_id); })?; r.success("ok") } diff --git a/src/data/account_export.rs b/src/data/account_export.rs index 5c9772a..ebee07e 100644 --- a/src/data/account_export.rs +++ b/src/data/account_export.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; use crate::data::comment::Comment; -use crate::data::conversation::Conversation; +use crate::data::conversation::{Conversation, ConvID}; use crate::data::conversation_message::ConversationMessage; use crate::data::error::ResultBoxError; use crate::data::friend::Friend; @@ -24,7 +24,7 @@ pub struct AccountExport { pub survey_responses: Vec, pub all_conversation_messages: Vec, pub conversations: Vec, - pub conversation_messages: HashMap>, + pub conversation_messages: HashMap>, pub friends_list: Vec, pub groups: Vec, } @@ -56,7 +56,7 @@ impl AccountExport { // Conversation members for conv in &self.conversations { - conv.members.iter().for_each(|f| { set.insert(f.clone()); }); + conv.members_ids().into_iter().for_each(|f| { set.insert(f); }); } // Conversation messages diff --git a/src/data/base_request_handler.rs b/src/data/base_request_handler.rs index 01b2e41..3718870 100644 --- a/src/data/base_request_handler.rs +++ b/src/data/base_request_handler.rs @@ -12,7 +12,7 @@ use serde::Serialize; use crate::api_data::http_error::HttpError; use crate::constants::PASSWORD_MIN_LENGTH; use crate::data::comment::Comment; -use crate::data::conversation::ConvID; +use crate::data::conversation::{ConversationMember, ConvID}; use crate::data::custom_emoji::CustomEmoji; use crate::data::error::{ExecError, Res, ResultBoxError}; use crate::data::group::GroupAccessLevel; @@ -24,9 +24,10 @@ use crate::helpers::{account_helper, comments_helper, conversations_helper, cust use crate::helpers::virtual_directory_helper::VirtualDirType; use crate::routes::RequestResult; use crate::utils::pdf_utils::is_valid_pdf; -use crate::utils::string_utils::{check_emoji_code, check_string_before_insert, check_url, remove_html_nodes}; +use crate::utils::string_utils::{check_emoji_code, check_string_before_insert, check_url, remove_html_nodes, check_html_color}; use crate::utils::user_data_utils::{generate_new_user_data_file_name, prepare_file_creation, user_data_path}; use crate::utils::virtual_directories_utils; +use std::collections::HashSet; #[derive(Serialize)] struct SuccessMessage { @@ -239,6 +240,18 @@ pub trait BaseRequestHandler { } } + /// Get a string included in the request. If none found, or if string is empty, returns [None] + fn post_string_optional(&mut self, name: &str) -> Option { + match self.post_parameter_opt(name) { + Some(RequestValue::String(s)) => { + if s.is_empty() { + None + } else { Some(s.to_string()) } + } + _ => None + } + } + /// Check out whether a file was included in the request or not fn has_file(&self, name: &str) -> bool { self.post_parameter_opt(name) @@ -420,6 +433,22 @@ pub trait BaseRequestHandler { Ok(user_id) } + /// Get a list of users ID included in the request + fn post_users_id(&mut self, name: &str) -> ResultBoxError> { + let users = self.post_numbers_list(name, 1)? + .iter() + .map(|u| UserID::new(u.clone())) + .collect::>(); + + for user in &users { + if !user_helper::exists(user)? { + self.not_found(format!("User {} not found!", user.id()))?; + } + } + + Ok(users) + } + /// Get the ID of a friend included in a POST request /// /// *Note :* This function does not check whether the user exists or not before checking if the @@ -461,14 +490,14 @@ pub trait BaseRequestHandler { } /// Get & return the ID of the conversation included in the POST request - fn post_conv_id(&mut self, name: &str) -> ResultBoxError { - let conv_id = self.post_u64(name)?; + fn post_conv(&mut self, name: &str) -> ResultBoxError { + let conv_id = ConvID::new(self.post_u64(name)?); + let membership = self.ok_or_forbidden( + conversations_helper::get_user_membership(&self.user_id()?, conv_id), + &format!("You do not belong to conversation {} !", conv_id.id()), + )?; - if !conversations_helper::does_user_belongs_to(&self.user_id()?, conv_id)? { - self.forbidden(format!("You do not belong to conversation {} !", conv_id))?; - } - - Ok(conv_id) + Ok(membership) } /// Get the ID @@ -624,4 +653,20 @@ pub trait BaseRequestHandler { Ok(info) } + + /// Get a color included in the request + fn post_color_opt(&mut self, field: &str) -> Res> { + let color = self.post_string_optional(field); + + match color { + None => Ok(None), + Some(color) => { + if !check_html_color(&color) { + self.bad_request(format!("Invalid color specified in '{}' !", field))?; + } + + Ok(Some(color)) + } + } + } } \ No newline at end of file diff --git a/src/data/conversation.rs b/src/data/conversation.rs index 0af2868..a408ea6 100644 --- a/src/data/conversation.rs +++ b/src/data/conversation.rs @@ -2,20 +2,94 @@ //! //! @author Pierre Hubert +use crate::data::group_id::GroupID; use crate::data::user::UserID; -pub type ConvID = u64; +#[derive(Copy, Debug, PartialEq, Eq, Clone, Hash)] +pub struct ConvID(u64); -#[derive(Debug, PartialEq, Eq)] +impl ConvID { + pub fn new(id: u64) -> Self { + ConvID(id) + } + + pub fn id(&self) -> u64 { + self.0.clone() + } +} + +#[derive(Debug)] +pub struct ConversationMember { + pub member_id: u64, + pub conv_id: ConvID, + pub user_id: UserID, + pub added_on: u64, + pub following: bool, + pub is_admin: bool, + pub last_message_seen: u64, +} + +#[derive(Debug)] pub struct Conversation { pub id: ConvID, - pub owner_id: UserID, pub name: Option, - pub members: Vec, + pub color: Option, + pub background: Option, + pub creation_time: u64, + pub group_id: Option, pub can_everyone_add_members: bool, - pub last_active: u64, - pub time_create: u64, + pub last_activity: u64, + pub members: Vec, +} - pub following: bool, - pub saw_last_message: bool, +impl PartialEq for Conversation { + fn eq(&self, other: &Self) -> bool { + self.id.eq(&other.id) + } +} + +impl Eq for Conversation {} + +impl Conversation { + /// Get the IDs of the members in the conversation + pub fn members_ids(&self) -> Vec { + self.members.iter().map(|m| m.user_id.clone()).collect() + } + + /// Check out whether this conversation is managed or not + pub fn is_managed(&self) -> bool { + self.group_id.is_some() + } + + /// Check out whether a given user is an admin of a conversation or not + pub fn is_admin(&self, user_id: &UserID) -> bool { + self + .members + .iter() + .any(|m| m.user_id == user_id && m.is_admin) + } + + /// Check out whether a user is the last administrator of a conversation or not + pub fn is_last_admin(&self, user_id: &UserID) -> bool { + let admins: Vec<&ConversationMember> = self.members + .iter() + .filter(|m| m.is_admin) + .collect(); + + admins.len() == 1 && admins[0].user_id == user_id + } +} + +/// Structure used to update the list of members of the conversation +pub struct ConversationMemberSetting { + pub user_id: UserID, + pub set_admin: bool, +} + +/// Structure used to update conversation settings +pub struct NewConversationSettings { + pub conv_id: ConvID, + pub color: Option, + pub name: Option, + pub can_everyone_add_members: bool, } \ No newline at end of file diff --git a/src/data/conversation_message.rs b/src/data/conversation_message.rs index b9183ea..be469fb 100644 --- a/src/data/conversation_message.rs +++ b/src/data/conversation_message.rs @@ -6,6 +6,7 @@ use std::collections::HashSet; use crate::data::error::{ExecError, Res}; use crate::data::user::UserID; +use crate::data::conversation::ConvID; pub type ConvMessageID = u64; @@ -101,7 +102,7 @@ impl ConversationServerMessageType { pub struct ConversationMessage { pub id: ConvMessageID, pub time_sent: u64, - pub conv_id: u64, + pub conv_id: ConvID, pub user_id: Option, pub message: Option, pub server_message: Option, diff --git a/src/data/new_conversation.rs b/src/data/new_conversation.rs index 96e2fce..074543c 100644 --- a/src/data/new_conversation.rs +++ b/src/data/new_conversation.rs @@ -3,11 +3,15 @@ //! @author Pierre Huber use crate::data::user::UserID; +use crate::data::group_id::GroupID; #[derive(Debug)] pub struct NewConversation { pub owner_id: UserID, pub name: Option, + pub group_id: Option, + pub color: Option, + pub background: Option, pub owner_following: bool, pub members: Vec, pub can_everyone_add_members: bool diff --git a/src/data/new_conversation_message.rs b/src/data/new_conversation_message.rs index c300de5..0a54338 100644 --- a/src/data/new_conversation_message.rs +++ b/src/data/new_conversation_message.rs @@ -3,11 +3,14 @@ //! @author Pierre Hubert use crate::data::user::UserID; +use crate::data::conversation::ConvID; +use crate::data::conversation_message::{ConversationMessageFile, ConversationServerMessageType}; /// Information about a new conversation message pub struct NewConversationMessage { - pub user_id: UserID, - pub conv_id: u64, - pub message: String, - pub image_path: Option + pub user_id: Option, + pub conv_id: ConvID, + pub message: Option, + pub file: Option, + pub server_message: Option } \ No newline at end of file diff --git a/src/data/user_membership.rs b/src/data/user_membership.rs index 4be6ac3..20c389f 100644 --- a/src/data/user_membership.rs +++ b/src/data/user_membership.rs @@ -20,7 +20,7 @@ impl UserMembership { match self { UserMembership::Group(_, last_active) => *last_active, UserMembership::Friend(f) => f.last_activity_time, - UserMembership::Conversation(c) => c.last_active, + UserMembership::Conversation(c) => c.last_activity, } } } diff --git a/src/helpers/conversations_helper.rs b/src/helpers/conversations_helper.rs index b6c57fe..61b8552 100644 --- a/src/helpers/conversations_helper.rs +++ b/src/helpers/conversations_helper.rs @@ -3,7 +3,7 @@ //! @author Pierre Hubert use crate::constants::database_tables_names::{CONV_LIST_TABLE, CONV_MEMBERS_TABLE, CONV_MESSAGES_TABLE}; -use crate::data::conversation::Conversation; +use crate::data::conversation::{Conversation, ConversationMember, ConversationMemberSetting, ConvID, NewConversationSettings}; use crate::data::conversation_message::{ConversationMessage, ConversationMessageFile, ConversationServerMessageType}; use crate::data::error::{ExecError, Res, ResultBoxError}; use crate::data::new_conversation::NewConversation; @@ -11,57 +11,81 @@ use crate::data::new_conversation_message::NewConversationMessage; use crate::data::unread_conversation::UnreadConversation; use crate::data::user::{User, UserID}; use crate::helpers::{database, events_helper}; -use crate::helpers::database::InsertQuery; +use crate::helpers::database::{InsertQuery, UpdateInfo}; use crate::helpers::events_helper::Event; use crate::utils::date_utils::time; use crate::utils::user_data_utils::delete_user_data_file_if_exists; /// Create a new conversation. This method returns the ID of the created conversation -pub fn create(conv: &NewConversation) -> ResultBoxError { +pub fn create(conv: &NewConversation) -> Res { // Create the conversation in the main table let conv_id = InsertQuery::new(CONV_LIST_TABLE) .add_user_id("user_id", &conv.owner_id) .add_str("name", conv.name.clone().unwrap_or(String::new()).as_str()) - .add_u64("last_active", time()) + .add_u64("last_activity", time()) .add_u64("creation_time", time()) + .add_opt_str("color", Option::from(&conv.color)) + .add_opt_str("background", Option::from(&conv.background)) .add_legacy_bool("can_everyone_add_members", conv.can_everyone_add_members) - .insert()?.ok_or(ExecError::new("missing result conv id!"))?; + .add_opt_group_id("group_id", conv.group_id.clone()) + .insert()? + .map(|i| ConvID::new(i)) + .ok_or(ExecError::new("missing result conv id!"))?; // Add the members to the conversation for member in &conv.members { + // Check following status of the member let mut follow = true; + let mut admin = false; if member.eq(&conv.owner_id) { follow = conv.owner_following; + admin = true; } - add_member(conv_id, member, follow)?; + add_member(conv_id, member, follow, admin)?; } Ok(conv_id) } /// Add a member to a conversation -pub fn add_member(conv_id: u64, user_id: &UserID, following: bool) -> ResultBoxError<()> { +pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool) -> Res { InsertQuery::new(CONV_MEMBERS_TABLE) - .add_u64("conv_id", conv_id) + .add_conv_id("conv_id", conv_id) .add_user_id("user_id", user_id) - .add_u64("time_add", time()) + .add_u64("added_on", time()) .add_legacy_bool("following", following) - .add_legacy_bool("saw_last_message", true) + .add_legacy_bool("is_admin", admin) + .add_u64("last_message_seen", 0) .insert()?; + // TODO : create a message + Ok(()) } -/// Remove a member from a conversation -pub fn remove_member(conv_id: u64, user_id: &UserID) -> ResultBoxError<()> { - database::DeleteQuery::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", conv_id) +/// Update admin status of a member for a conversation +pub fn set_admin(conv_id: &ConvID, user_id: &UserID, admin: bool) -> Res { + UpdateInfo::new(CONV_MEMBERS_TABLE) .cond_user_id("user_id", user_id) + .cond_conv_id("conv_id", conv_id.clone()) + .set_legacy_bool("is_admin", admin) .exec() } +/// Remove a member from a conversation +pub fn remove_member(conv_id: ConvID, user_id: &UserID) -> ResultBoxError<()> { + database::DeleteQuery::new(CONV_MEMBERS_TABLE) + .cond_conv_id("conv_id", conv_id) + .cond_user_id("user_id", user_id) + .exec()?; + + // TODO : create a message + + Ok(()) +} + /// Get the list of conversations of a specific user pub fn get_list_user(user_id: &UserID) -> ResultBoxError> { database::QueryInfo::new(CONV_LIST_TABLE) @@ -71,67 +95,46 @@ pub fn get_list_user(user_id: &UserID) -> ResultBoxError> { .join(CONV_MEMBERS_TABLE, "u", "l.id = u.conv_id") // Specify selected fields - .add_field("*") - .add_field("l.id as id") - .add_field("l.user_id as owner_id") + .add_field("l.*") // Filter query .cond_user_id("u.user_id", user_id) // Sort results - .set_order("l.last_active DESC") + .set_order("l.last_activity DESC") // Execute query .exec(db_to_conversation_info) } /// Get information about a single conversation -pub fn get_single(conv_id: u64, user_id: &UserID) -> ResultBoxError { +pub fn get_single(conv_id: ConvID) -> ResultBoxError { // Tables database::QueryInfo::new(CONV_LIST_TABLE) - .alias("l") - .join(CONV_MEMBERS_TABLE, "u", "l.id = u.conv_id") - - // Fields - .add_field("*") - .add_field("l.id as id") - .add_field("l.user_id as owner_id") - - // Conditions - .cond_u64("l.id", conv_id) - .cond_user_id("u.user_id", user_id) - + .cond_conv_id("id", conv_id) .query_row(db_to_conversation_info) } /// Get the list of members of a conversation -pub fn get_list_members(conv_id: u64) -> ResultBoxError> { +pub fn get_list_members(conv_id: ConvID) -> Res> { database::QueryInfo::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", conv_id) - .add_field("user_id") - .exec(|res| res.get_user_id("user_id")) + .cond_conv_id("conv_id", conv_id) + .exec(db_to_conversation_member) } /// Check if a user belongs to a conversation or not -pub fn does_user_belongs_to(user_id: &UserID, conv_id: u64) -> ResultBoxError { - Ok(database::QueryInfo::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", conv_id) +pub fn get_user_membership(user_id: &UserID, conv_id: ConvID) -> Res { + database::QueryInfo::new(CONV_MEMBERS_TABLE) + .cond_conv_id("conv_id", conv_id) .cond_user_id("user_id", user_id) - .exec_count()? > 0) + .query_row(db_to_conversation_member) } -/// Check out wheter a user is the moderator of a conversation or not -pub fn is_user_moderator(user_id: &UserID, conv_id: u64) -> ResultBoxError { - Ok(database::QueryInfo::new(CONV_LIST_TABLE) - .cond_u64("id", conv_id) - .cond_user_id("user_id", user_id) - .exec_count()? > 0) -} /// Check out whether all the members of a conversation can add members to it or not -pub fn can_everyone_add_members(conv_id: u64) -> ResultBoxError { +pub fn can_everyone_add_members(conv_id: ConvID) -> ResultBoxError { database::QueryInfo::new(CONV_LIST_TABLE) - .cond_u64("id", conv_id) + .cond_conv_id("id", conv_id) .add_field("can_everyone_add_members") .query_row(|f| f.get_legacy_bool("can_everyone_add_members")) } @@ -146,25 +149,28 @@ pub fn set_following(user_id: &UserID, conv_id: u64, following: bool) -> ResultB } /// Set a new list of members for a given conversation -pub fn set_members(conv_id: u64, new_list: &Vec, can_delete: bool) -> ResultBoxError<()> { +pub fn set_members(conv_id: ConvID, new_list: &Vec, can_delete: bool) -> ResultBoxError<()> { let curr_list = get_list_members(conv_id)?; // Add new members for member in new_list { - if curr_list.contains(member) { - continue; + if let Some(user) = curr_list.iter().filter(|m| m.user_id == member.user_id).next() { + // Check if we have to update admin state + if user.is_admin != member.set_admin { + set_admin(&conv_id, &member.user_id, member.set_admin)?; + } + } else { + add_member(conv_id, &member.user_id, true, member.set_admin)?; } - - add_member(conv_id, member, true)?; } // Remove a member if can_delete { for member in curr_list { - if new_list.contains(&member) { + if new_list.iter().any(|m| m.user_id.eq(&member.user_id)) { continue; } - remove_member(conv_id, &member)?; + remove_member(conv_id, &member.user_id)?; } } @@ -173,23 +179,17 @@ pub fn set_members(conv_id: u64, new_list: &Vec, can_delete: bool) -> Re } /// Set a new name to the conversation -pub fn set_name(conv_id: u64, name: Option) -> ResultBoxError<()> { +pub fn set_settings(settings: NewConversationSettings) -> Res { database::UpdateInfo::new(CONV_LIST_TABLE) - .cond_u64("id", conv_id) - .set_opt_str("name", name) - .exec() -} - -/// Specify whether any member of this conversation can invite other users to join it -pub fn set_can_everyone_add_members(conv_id: u64, allow: bool) -> ResultBoxError<()> { - database::UpdateInfo::new(CONV_LIST_TABLE) - .cond_u64("id", conv_id) - .set_legacy_bool("can_everyone_add_members", allow) + .cond_conv_id("id", settings.conv_id) + .set_opt_str("name", settings.name) + .set_opt_str("color", settings.color) + .set_legacy_bool("can_everyone_add_members", settings.can_everyone_add_members) .exec() } /// Search for private conversation between two users -pub fn find_private(user_1: &UserID, user_2: &UserID) -> ResultBoxError> { +pub fn find_private(user_1: &UserID, user_2: &UserID) -> ResultBoxError> { database::QueryInfo::new(CONV_MEMBERS_TABLE) .alias("t1") @@ -201,7 +201,7 @@ pub fn find_private(user_1: &UserID, user_2: &UserID) -> ResultBoxError .cond_user_id("t2.user_id", user_2) .set_custom_where(format!("(SELECT COUNT(*) FROM {} WHERE conv_id = t1.conv_id) = 2", CONV_MEMBERS_TABLE).as_ref()) .add_field("t1.conv_id AS conv_id") - .exec(|f| f.get_u64("conv_id")) + .exec(|f| f.get_conv_id("conv_id")) } /// Get the last messages posted in a conversation @@ -247,9 +247,9 @@ pub fn get_older_messages(conv_id: u64, start_id: u64, limit: u64) -> ResultBoxE } /// Get all the messages of a single user for a conversation -pub fn get_user_messages_for_conversations(conv_id: u64, user_id: &UserID) -> ResultBoxError> { +pub fn get_user_messages_for_conversations(conv_id: ConvID, user_id: &UserID) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) - .cond_u64("conv_id", conv_id) + .cond_conv_id("conv_id", conv_id) .cond_user_id("user_id", user_id) .exec(db_to_conversation_message) } @@ -293,16 +293,16 @@ pub fn delete_all_user_messages(user_id: &UserID) -> ResultBoxError { /// Remove the user from all the conversations he belongs to pub fn delete_all_user_conversations(user_id: &UserID) -> ResultBoxError { for conversation in &get_list_user(user_id)? { - remove_user_from_conversation(user_id, conversation.id)?; + remove_user_from_conversation(user_id, conversation, user_id)?; } Ok(()) } /// Get the entire list of messages of a given conversation -pub fn get_all_messages(conv_id: u64) -> ResultBoxError> { +pub fn get_all_messages(conv_id: ConvID) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) - .cond_u64("conv_id", conv_id) + .cond_conv_id("conv_id", conv_id) .exec(db_to_conversation_message) } @@ -318,39 +318,45 @@ pub fn send_message(msg: &NewConversationMessage) -> ResultBoxError<()> { let t = time(); // Insert the message in the database - let msg_id = database::InsertQuery::new(CONV_MESSAGES_TABLE) - .add_u64("conv_id", msg.conv_id) - .add_user_id("user_id", &msg.user_id) - .add_u64("time_insert", t) - .add_str("message", msg.message.as_str()) - .add_opt_str("image_path", msg.image_path.as_ref()) - .insert_expect_result()?; + let mut msg_request = database::InsertQuery::new(CONV_MESSAGES_TABLE) + .add_conv_id("conv_id", msg.conv_id) + .add_u64("user_id", msg.user_id.as_ref().map(|u| u.id()).unwrap_or(0)) + .add_u64("time_sent", t); + + if let Some(server_msg) = &msg.server_message { + msg_request = msg_request.add_str("message", &server_msg.to_db()); + } else if let Some(message) = &msg.message { + msg_request = msg_request.add_str("message", message); + } + + if let Some(file) = &msg.file { + msg_request = msg_request.add_str("file_path", &file.path) + .add_u64("file_size", file.size) + .add_str("file_name", &file.name) + .add_str("file_type", &file.r#type) + .add_opt_str("file_thumbnail", Option::from(&file.thumbnail)); + } + + let msg_id = msg_request.insert_expect_result()?; // Update the last activity of the conversation database::UpdateInfo::new(CONV_LIST_TABLE) - .cond_u64("id", msg.conv_id) - .set_u64("last_active", t) + .cond_conv_id("id", msg.conv_id) + .set_u64("last_activity", t) .exec()?; // Get the list of users to notify after the update let list_to_notify = database::QueryInfo::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", msg.conv_id) - .cond_legacy_bool("saw_last_message", true) + .cond_conv_id("conv_id", msg.conv_id) .cond_legacy_bool("following", true) .set_custom_where("user_id != ?") - .add_custom_where_argument_user_id(&msg.user_id) + .add_custom_where_argument_user_id(msg.user_id.as_ref().unwrap_or(&UserID::invalid())) .exec(|r| r.get_user_id("user_id"))?; - // Mark all the users of the conversation as unread - database::UpdateInfo::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", msg.conv_id) - .cond_legacy_bool("saw_last_message", true) - - .custom_where("user_id != ?") - .add_custom_where_arg_u64(msg.user_id.id()) - - .set_legacy_bool("saw_last_message", false) - .exec()?; + // Mark the user has seen his message + if let Some(user_id) = &msg.user_id { + mark_user_seen(msg.conv_id, user_id, msg_id)?; + } // Send an event (updated_number_unread_conversations) events_helper::propagate_event(&Event::UpdatedNumberUnreadConversations(&list_to_notify))?; @@ -419,34 +425,32 @@ pub fn get_list_unread(user_id: &UserID) -> ResultBoxError ResultBoxError<()> { +pub fn mark_user_seen(conv_id: ConvID, user_id: &UserID, last_msg: u64) -> ResultBoxError<()> { database::UpdateInfo::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", conv_id) + .cond_conv_id("conv_id", conv_id) .cond_user_id("user_id", user_id) - .cond_legacy_bool("saw_last_message", false) - .set_legacy_bool("saw_last_message", true) + .set_u64("last_message_seen", last_msg) .exec()?; // Push an event (updated_number_unread_conversations) @@ -456,36 +460,41 @@ pub fn mark_user_seen(conv_id: u64, user_id: &UserID) -> ResultBoxError<()> { } /// Remove a user from a conversation -pub fn remove_user_from_conversation(user_id: &UserID, conv_id: u64) -> ResultBoxError<()> { - if is_user_moderator(user_id, conv_id)? { - delete_conversation(conv_id) +pub fn remove_user_from_conversation(user_id: &UserID, conv: &Conversation, remover: &UserID) -> ResultBoxError<()> { + if conv.is_last_admin(user_id) { + delete_conversation(conv) } else { - delete_member(user_id, conv_id) + delete_member(user_id, conv.id, remover) } } /// Remove permanently a conversation -pub fn delete_conversation(conv_id: u64) -> ResultBoxError<()> { +pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> { // Delete all the messages of the conversations - for message in get_all_messages(conv_id)? { + for message in get_all_messages(conv.id)? { delete_message(&message)?; } // Delete all the members of the conversation database::DeleteQuery::new(CONV_MEMBERS_TABLE) - .cond_u64("conv_id", conv_id) + .cond_conv_id("conv_id", conv.id) .exec()?; + // Delete associated background image, if any + if let Some(image) = &conv.background { + delete_user_data_file_if_exists(image)?; + } + // Delete the conversation entry itself database::DeleteQuery::new(CONV_LIST_TABLE) - .cond_u64("id", conv_id) + .cond_conv_id("id", conv.id) .exec()?; Ok(()) } /// Delete a conversation membership -pub fn delete_member(user_id: &UserID, conv_id: u64) -> ResultBoxError<()> { +pub fn delete_member(user_id: &UserID, conv_id: ConvID, _remover: &UserID) -> ResultBoxError<()> { for msg in get_user_messages_for_conversations(conv_id, user_id)? { delete_message(&msg)?; } @@ -507,17 +516,30 @@ pub fn is_message_owner(user_id: &UserID, message_id: u64) -> ResultBoxError ResultBoxError { - let conv_id = row.get_u64("id")?; + let conv_id = row.get_conv_id("id")?; Ok(Conversation { id: conv_id, - owner_id: row.get_user_id("owner_id")?, + color: row.get_optional_str("color")?, + background: row.get_optional_str("background")?, name: row.get_optional_str("name")?, members: get_list_members(conv_id)?, can_everyone_add_members: row.get_legacy_bool("can_everyone_add_members")?, - last_active: row.get_u64("last_active")?, - time_create: row.get_u64("time_add")?, + last_activity: row.get_u64("last_activity")?, + creation_time: row.get_u64("creation_time")?, + group_id: row.get_optional_group_id("group_id")?, + }) +} + +/// Turn a database entry into a ConversationMember object +fn db_to_conversation_member(row: &database::RowResult) -> Res { + Ok(ConversationMember { + member_id: row.get_u64("id")?, + conv_id: row.get_conv_id("conv_id")?, + user_id: row.get_user_id("user_id")?, + added_on: row.get_u64("added_on")?, following: row.get_legacy_bool("following")?, - saw_last_message: row.get_legacy_bool("saw_last_message")?, + is_admin: row.get_legacy_bool("is_admin")?, + last_message_seen: row.get_u64("last_message_seen")?, }) } @@ -529,10 +551,10 @@ fn db_to_conversation_message(row: &database::RowResult) -> ResultBoxError Some(row.get_user_id("user_id")?) }; - let file = match row.is_null_or_empty("filepath")? { + let file = match row.is_null_or_empty("file_path")? { true => None, false => Some(ConversationMessageFile { - path: row.get_str("filepath")?, + path: row.get_str("file_path")?, size: row.get_u64("file_size")?, name: row.get_str("file_name")?, thumbnail: row.get_optional_str("file_thumbnail")?, @@ -553,7 +575,7 @@ fn db_to_conversation_message(row: &database::RowResult) -> ResultBoxError QueryInfo { + self.conditions.insert(key.to_string(), mysql::Value::from(val.id())); + self + } + pub fn cond_legacy_bool(mut self, key: &str, val: bool) -> QueryInfo { let val = match val { true => 1, @@ -358,6 +364,19 @@ impl<'a> RowResult<'a> { Ok(GroupID::new(self.get_u64(name)?)) } + /// Get the optional ID of a group included in the response + pub fn get_optional_group_id(&self, name: &str) -> ResultBoxError> { + Ok(match self.get_optional_u64(name)? { + None | Some(0) => None, + Some(id) => Some(GroupID::new(id)) + }) + } + + /// Get the ID of a conversation included in the response + pub fn get_conv_id(&self, name: &str) -> ResultBoxError { + Ok(ConvID::new(self.get_u64(name)?)) + } + /// Find a string included in the request pub fn get_str(&self, name: &str) -> Result> { let value = self.row.get_opt(self.find_col(name)?); @@ -588,6 +607,19 @@ impl InsertQuery { self } + /// Add an optional number. If None, Null will be inserted + pub fn add_opt_u64(mut self, key: &str, value: Option) -> InsertQuery { + self.values.insert(key.to_string(), value + .map(|u| Value::UInt(u)) + .unwrap_or(Value::NULL)); + self + } + + /// Add an optional number. If None, Null will be inserted + pub fn add_opt_group_id(self, key: &str, value: Option) -> InsertQuery { + self.add_opt_u64(key, value.map(|u| u.id())) + } + /// Add an integer pub fn add_i64(mut self, key: &str, value: i64) -> InsertQuery { self.values.insert(key.to_string(), Value::from(value)); @@ -619,6 +651,12 @@ impl InsertQuery { self } + pub fn add_conv_id(mut self, key: &str, value: ConvID) -> InsertQuery { + self.values.insert(key.to_string(), Value::from(value.id())); + self + } + + /// Legacy database boolean (1 = true / 0 = false) pub fn add_legacy_bool(mut self, key: &str, value: bool) -> InsertQuery { @@ -758,6 +796,11 @@ impl DeleteQuery { self } + pub fn cond_conv_id(mut self, key: &str, value: ConvID) -> DeleteQuery { + self.conditions.insert(key.to_string(), Value::from(value.id())); + self + } + /// Execute the delete query pub fn exec(self) -> ResultBoxError<()> { delete(self) @@ -832,6 +875,13 @@ impl UpdateInfo { self } + /// Filter with a conversation id + pub fn cond_conv_id(mut self, name: &str, val: ConvID) -> UpdateInfo { + self.cond.insert(name.to_string(), Value::UInt(val.id())); + self + } + + /// Filter with an unsigned integer pub fn cond_u64(mut self, name: &str, val: u64) -> UpdateInfo { self.cond.insert(name.to_string(), Value::UInt(val)); diff --git a/src/routes.rs b/src/routes.rs index 35b2d24..6c85250 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -196,7 +196,6 @@ pub fn get_routes() -> Vec { Route::post("/conversations/getInfosOne", Box::new(conversations_controller::get_single)), Route::post("/conversations/updateSettings", Box::new(conversations_controller::update_settings)), Route::post("/conversations/getPrivate", Box::new(conversations_controller::find_private)), - Route::post("/conversations/refresh", Box::new(conversations_controller::refresh_list)), Route::post("/conversations/refresh_single", Box::new(conversations_controller::refresh_single)), Route::post("/conversations/get_older_messages", Box::new(conversations_controller::get_older_messages)), Route::post("/conversations/sendMessage", Box::new(conversations_controller::send_message)), @@ -205,6 +204,7 @@ pub fn get_routes() -> Vec { Route::post("/conversations/delete", Box::new(conversations_controller::delete_conversation)), Route::post("/conversations/updateMessage", Box::new(conversations_controller::update_message)), Route::post("/conversations/deleteMessage", Box::new(conversations_controller::delete_message)), + // TODO : add a route to mark messages seen // Search controller diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs index 125f737..33d1dbb 100644 --- a/src/utils/string_utils.rs +++ b/src/utils/string_utils.rs @@ -92,4 +92,18 @@ pub fn check_youtube_id(id: &str) -> bool { pub fn check_emoji_code(shortcut: &str) -> bool { let r = Regex::new(r"^:[a-zA-Z0-9]+:$").unwrap(); r.is_match(shortcut) +} + +/// Check the validity of an HTML color +/// +/// ``` +/// use comunic_server::utils::string_utils::check_emoji_code; +/// +/// assert_eq!(check_emoji_code("AAFF00"), true); +/// assert_eq!(check_emoji_code("ABC"), false); +/// assert_eq!(check_emoji_code("UUFF00"), false); +/// ``` +pub fn check_html_color(color: &str) -> bool { + let r = Regex::new(r"^:[A-F0-9]{6}:$").unwrap(); + r.is_match(color) } \ No newline at end of file