//! # Conversations controller //! //! @author Pierre Hubert use std::collections::HashSet; use crate::api_data::conversation_api::ConversationAPI; use crate::api_data::conversation_message_api::ConversationMessageAPI; use crate::api_data::list_unread_conversations_api::UnreadConversationAPI; use crate::api_data::remove_user_from_conv_message::RemovedUserFromConversationMessage; 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::constants::{ALLOWED_CONVERSATION_FILES_TYPES, CONVERSATION_FILES_MAX_SIZE, MAX_CONVERSATION_MESSAGE_LENGTH, MIN_CONVERSATION_MESSAGE_LENGTH}; use crate::controllers::user_ws_controller; use crate::data::base_request_handler::{BaseRequestHandler, RequestValue}; use crate::data::conversation::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; use crate::data::new_conversation_message::NewConversationMessage; use crate::data::user_ws_connection::UserWsConnection; use crate::data::user_ws_message::UserWsMessage; use crate::helpers::{conversations_helper, events_helper}; use crate::helpers::events_helper::Event; use crate::routes::RequestResult; use crate::utils::string_utils::remove_html_nodes; use crate::utils::user_data_utils::{delete_user_data_file_if_exists, user_data_path}; /// Create a new conversation pub fn create(r: &mut HttpRequestHandler) -> RequestResult { let name = r.post_string("name")?; let mut members = r.post_users_id("users")?; // Adapt name let name = match name.as_str() { "false" => None, s => Some(s.to_string()) }; // Add current user ID if required members.insert(r.user_id()?); let conv = NewConversation { owner_id: r.user_id()?, name, 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, logo: None, }; // Create the conversation let conv_id = conversations_helper::create(&conv)?; r.set_response(ResCreateConversation::new(conv_id)) } /// Get the list of conversations of a user pub fn get_list(r: &mut HttpRequestHandler) -> RequestResult { let list = conversations_helper::get_list_user(&r.user_id()?)?; r.set_response(list.iter().map(|c| ConversationAPI::new(c)).collect::>()) } /// Get information about a single conversation pub fn get_single(r: &mut HttpRequestHandler) -> RequestResult { 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_membership = r.post_conv("conversationID")?; // Update following state, if required if r.has_post_parameter("following") { conversations_helper::set_following( &r.user_id()?, conv_membership.conv_id, r.post_bool("following")?, )?; } // Change moderator settings 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())?; } 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")?, }; conversations_helper::set_settings(new_settings)?; } r.success("Conversation information successfully updated!") } /// Change conversation image pub fn change_image(r: &mut HttpRequestHandler) -> RequestResult { let conv_membership = r.post_conv_admin("convID")?; let conv = conversations_helper::get_single(conv_membership.conv_id)?; let new_image = r.save_post_image("file", "conv-image", 200, 200)?; conversations_helper::remove_conversation_image(&conv)?; conversations_helper::set_conversation_image(&conv, &new_image)?; r.ok() } /// Delete conversation image pub fn delete_image(r: &mut HttpRequestHandler) -> RequestResult { let conv_membership = r.post_conv_admin("convID")?; let conv = conversations_helper::get_single(conv_membership.conv_id)?; conversations_helper::remove_conversation_image(&conv)?; r.ok() } /// Add a new member to a conversation pub fn add_member(r: &mut HttpRequestHandler) -> RequestResult { let conv_membership = r.post_conv("convID")?; let conv = conversations_helper::get_single(conv_membership.conv_id)?; let user_to_add = r.post_user_id("userID")?; if conv.is_managed() { r.bad_request("This conversation is managed, you can not manually change its members!".to_string())?; } if !conv.can_user_add_members(r.user_id_ref()?) { r.forbidden("You are not allowed to add members to this conversation!".to_string())?; } if conv.get_membership(&user_to_add).is_some() { 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()?)?; r.success("The user was added to the conversation!") } /// Update admin status of a user pub fn set_admin(r: &mut HttpRequestHandler) -> RequestResult { let conv_membership = r.post_conv_admin("convID")?; let conv = conversations_helper::get_single(conv_membership.conv_id)?; let user_to_update = r.post_user_id("userID")?; let set_admin = r.post_bool("setAdmin")?; if conv.is_managed() { r.bad_request("This conversation is managed, you can not manually change its members!".to_string())?; } if !conv.can_mark_other_users_admin(r.user_id_ref()?) { r.forbidden("You are not allowed to make users admin in this conversation!".to_string())?; } if conv.get_membership(&user_to_update).is_none() { r.bad_request("This user is not a member of this conversation!".to_string())?; } conversations_helper::set_admin(&conv.id, &user_to_update, set_admin)?; r.success("The user was added to the conversation!") } /// Remove a member from a conversation pub fn remove_member(r: &mut HttpRequestHandler) -> RequestResult { let conv_membership = r.post_conv_admin("convID")?; let conv = conversations_helper::get_single(conv_membership.conv_id)?; let user_to_remove = r.post_user_id("userID")?; if conv.is_managed() { r.bad_request("This conversation is managed, you can not manually change its members!".to_string())?; } if !conv.can_user_remove_members(r.user_id_ref()?) { r.forbidden("You are not allowed to remove members from this conversation!".to_string())?; } if conv.get_membership(&user_to_remove).is_none() { 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()?)?; r.ok() } /// Find a private conversation pub fn find_private(r: &mut HttpRequestHandler) -> RequestResult { let other_user = r.post_user_id("otherUser")?; let allow_create = r.post_bool_opt("allowCreate", false); // Query the database let mut list = conversations_helper::find_private(&r.user_id()?, &other_user)?; if list.is_empty() { if !allow_create { return r.not_found(format!("Not any private conversation was found. The server was not allowed to create a new one...")); } let mut members = HashSet::new(); members.insert(r.user_id()?); members.insert(r.user_id()?); let new_conv = NewConversation { owner_id: r.user_id()?, name: None, owner_following: true, members, can_everyone_add_members: true, color: None, logo: None, group_id: None, }; let conv_id = conversations_helper::create(&new_conv)?; list.push(conv_id); } r.set_response(ResFindPrivateConversations::new(list)) } /// Refresh a single conversation pub fn refresh_single(r: &mut HttpRequestHandler) -> RequestResult { let conv = r.post_conv("conversationID")?; let last_message_id = r.post_u64("last_message_id")?; let mut messages = match last_message_id { // Get latest messages of the conversation 0 => conversations_helper::get_last_messages(conv.conv_id, 10)?, // Get new messages _ => conversations_helper::get_new_messages(conv.conv_id, last_message_id)?, }; messages.sort_by(|one, two| one.id.cmp(&two.id)); if messages.len() > 0 && messages.last().unwrap().id > conv.last_message_seen { conversations_helper::mark_user_seen( conv.conv_id, r.user_id_ref()?, &messages.last().unwrap(), )?; } r.set_response(ConversationMessageAPI::for_list(&messages)) } /// Get older messages of a conversation pub fn get_older_messages(r: &mut HttpRequestHandler) -> RequestResult { let conv = r.post_conv("conversationID")?; let max_id = r.post_u64("oldest_message_id")? - 1; // Determine limit let limit = r.post_u64("limit")?; let limit = match limit { 0 => 1, 1..=60 => limit, _ => 60 }; let messages = conversations_helper::get_older_messages(conv.conv_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 = r.post_conv("conversationID")?; // Get associated file let file = match r.post_parameter_opt("file") { Some(RequestValue::File(file)) => { // File name let mut name = file.name.to_string(); if file.buff.len() > CONVERSATION_FILES_MAX_SIZE { r.bad_request("File is too big!".to_string())?; } // Determine file mime type let mut mime_type = r.post_file_type("file")?; // Check for thumbnail let mut thumbnail = match r.has_file("thumbnail") { false => None, true => Some("thumbnail".to_string()) }; let path; if !ALLOWED_CONVERSATION_FILES_TYPES.contains(&mime_type.as_str()) { r.bad_request("File type is not allowed!".to_string())?; } // Images if mime_type.starts_with("image/") { if let None = thumbnail { thumbnail = Some("file".to_string()); } path = r.save_post_image("file", "conversation", 2000, 2000)?; mime_type = "image/png".to_string(); name = "picture.png".to_string(); } // PDF else if mime_type.eq("application/pdf") { path = r.save_post_pdf("file", "conversation")?; } // MP3 else if mime_type.eq("audio/mpeg") { path = r.save_post_mp3("file", "conversation")?; } // MP4 else if mime_type.eq("video/mp4") { path = r.save_post_mp4("file", "conversation")?; } // ZIP archive else if mime_type.eq("application/zip") { path = r.save_post_zip("file", "conversation")?; } // Office document else if mime_type.starts_with("application/") { path = r.save_post_office_doc("file", "conversation")?; } // Text files else { path = r.save_post_txt_doc("file", "conversation")?; } // Attempt to save thumbnail, if it fails we can not save message let thumbnail = match thumbnail { None => None, Some(f) => Some(match r.save_post_image(&f, "conversations-thumb", 200, 200) { Ok(s) => Ok(s), Err(e) => { eprintln!("Failed to save conversation thumbnail! {:#?}", e); delete_user_data_file_if_exists(&path).unwrap(); Err(e) } }?) }; Some(ConversationMessageFile { path: path.clone(), size: std::fs::metadata(user_data_path(path.as_ref()))?.len(), name, thumbnail, r#type: mime_type, }) } _ => None, }; // Get message, if there is no file let message = if let None = file { let msg = r.post_string_without_html("message", MIN_CONVERSATION_MESSAGE_LENGTH, true)?; if msg.len() > MAX_CONVERSATION_MESSAGE_LENGTH { r.bad_request("Message is too long!".to_string())?; } Some(msg) } else { None }; conversations_helper::send_message(&NewConversationMessage { user_id: Some(r.user_id()?), conv_id: conv.conv_id, message, file, server_message: None, })?; r.success("Conversation message was successfully sent!") } /// Count the number of unread conversation of the user pub fn count_unread(r: &mut HttpRequestHandler) -> RequestResult { let num = conversations_helper::count_unread_for_user(&r.user_id()?)?; r.set_response(ResultCountUnreadConversations::new(num)) } /// Get the list of unread conversations of a user pub fn list_unread(r: &mut HttpRequestHandler) -> RequestResult { let list = conversations_helper::get_list_unread(&r.user_id()?)?; r.set_response(UnreadConversationAPI::for_list(&list)?) } /// Delete a conversation pub fn delete_conversation(r: &mut HttpRequestHandler) -> RequestResult { let conv_membership = r.post_conv("conversationID")?; let conv = conversations_helper::get_single(conv_membership.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") } /// Update a single conversation message pub fn update_message(r: &mut HttpRequestHandler) -> RequestResult { let msg_id = r.post_u64("messageID")?; let new_content = r.post_string_opt("content", MIN_CONVERSATION_MESSAGE_LENGTH, true)?; let msg = conversations_helper::get_single_message(msg_id)?; if msg.user_id != r.user_id_opt() { r.forbidden("You are not the owner of this message!".to_string())?; } if msg.file.is_some() { r.bad_request("Can not have both text and file in the same message!".to_string())?; } if new_content.len() > MAX_CONVERSATION_MESSAGE_LENGTH { r.bad_request("New message is too long!".to_string())?; } conversations_helper::update_message_content(msg_id, &new_content)?; r.success("Conversation message content successfully updated") } /// Delete a conversation message pub fn delete_message(r: &mut HttpRequestHandler) -> RequestResult { let msg_id = r.post_u64("messageID")?; if !conversations_helper::is_message_owner(&r.user_id()?, msg_id)? { r.forbidden("You are not the owner of this message!".to_string())?; } conversations_helper::delete_message_by_id(msg_id)?; r.success("The message has been successfully deleted!") } /// Events handler pub fn handle_event(e: &events_helper::Event) -> Res { match e { Event::UpdatedNumberUnreadConversations(users) => { for user in users.iter() { if user_ws_controller::is_user_connected(user) { user_ws_controller::send_message_to_user( &UserWsMessage::no_id_message( "number_unread_conversations", conversations_helper::count_unread_for_user(user)?, )?, user, )?; } } } Event::NewConversationMessage(msg) => { 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(), msg)), )?; } Event::UpdatedConversationMessage(msg) => { user_ws_controller::send_message_to_specific_connections( |f| f.conversations.contains(&msg.conv_id), |_| UserWsMessage::no_id_message("updated_conv_message", ConversationMessageAPI::new(msg)), None:: _>, )?; } Event::DeleteConversationMessage(msg) => { user_ws_controller::send_message_to_specific_connections( |f| f.conversations.contains(&msg.conv_id), |_| UserWsMessage::no_id_message("deleted_conv_message", ConversationMessageAPI::new(msg)), None:: _>, )?; } Event::RemovedUserFromConversation(user_id, conv_id) => { // Notify users user_ws_controller::send_message_to_specific_connections( |f| f.conversations.contains(conv_id), |_| UserWsMessage::no_id_message("removed_user_from_conv", RemovedUserFromConversationMessage::new(user_id, *conv_id)), None:: _>, )?; // Disconnect users from conversation user_ws_controller::foreach_connection(|f| { if f.user_id() == user_id && f.conversations.contains(conv_id) { f.clone().replace(|w| { w.conversations.remove(conv_id); }); } Ok(()) })?; } _ => {} } Ok(()) }