//! # Conversations helper //! //! @author Pierre Hubert use crate::constants::database_tables_names::{CONV_LIST_TABLE, CONV_MEMBERS_TABLE, CONV_MESSAGES_TABLE}; 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; use crate::data::new_conversation_message::NewConversationMessage; use crate::data::user::{User, UserID}; use crate::helpers::{database, events_helper}; 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) -> 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_activity", time()) .add_u64("creation_time", time()) .add_opt_str("color", Option::from(&conv.color)) .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()) .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, admin)?; } Ok(conv_id) } /// Add a member to a conversation pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool) -> Res { InsertQuery::new(CONV_MEMBERS_TABLE) .add_conv_id("conv_id", conv_id) .add_user_id("user_id", user_id) .add_u64("added_on", time()) .add_legacy_bool("following", following) .add_legacy_bool("is_admin", admin) .add_u64("last_message_seen", 0) .insert()?; // TODO : create a message Ok(()) } /// 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) .alias("l") // Join with conversation members table .join(CONV_MEMBERS_TABLE, "u", "l.id = u.conv_id") // Specify selected fields .add_field("l.*") // Filter query .cond_user_id("u.user_id", user_id) // Sort results .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: ConvID) -> ResultBoxError { // Tables database::QueryInfo::new(CONV_LIST_TABLE) .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: ConvID) -> Res> { database::QueryInfo::new(CONV_MEMBERS_TABLE) .cond_conv_id("conv_id", conv_id) .exec(db_to_conversation_member) } /// Check if a user belongs to a conversation or not 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) .query_row(db_to_conversation_member) } /// Check out whether all the members of a conversation can add members to it or not pub fn can_everyone_add_members(conv_id: ConvID) -> ResultBoxError { database::QueryInfo::new(CONV_LIST_TABLE) .cond_conv_id("id", conv_id) .add_field("can_everyone_add_members") .query_row(|f| f.get_legacy_bool("can_everyone_add_members")) } /// Set whether a user is following a conversation or not pub fn set_following(user_id: &UserID, conv_id: ConvID, following: bool) -> ResultBoxError<()> { database::UpdateInfo::new(CONV_MEMBERS_TABLE) .cond_conv_id("conv_id", conv_id) .cond_user_id("user_id", user_id) .set_legacy_bool("following", following) .exec() } /// Set a new list of members for a given conversation 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 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)?; } } // Remove a member if can_delete { for member in curr_list { if new_list.iter().any(|m| m.user_id.eq(&member.user_id)) { continue; } remove_member(conv_id, &member.user_id)?; } } Ok(()) } /// Set a new name to the conversation pub fn set_settings(settings: NewConversationSettings) -> Res { database::UpdateInfo::new(CONV_LIST_TABLE) .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> { database::QueryInfo::new(CONV_MEMBERS_TABLE) .alias("t1") // Join .join(CONV_MEMBERS_TABLE, "t2", "t1.conv_id = t2.conv_id") // Conditions .cond_user_id("t1.user_id", user_1) .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_conv_id("conv_id")) } /// Get the last messages posted in a conversation pub fn get_last_messages(conv_id: ConvID, number_of_messages: u64) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_conv_id("conv_id", conv_id) .set_limit(number_of_messages) .set_order("id DESC") .exec(db_to_conversation_message) .map(|mut l| { l.sort_by(|a, b| a.id.partial_cmp(&b.id).unwrap()); l }) } /// Get the new messages of a conversation pub fn get_new_messages(conv_id: ConvID, last_message_id: u64) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_conv_id("conv_id", conv_id) .set_custom_where("id > ?") .add_custom_where_argument_u64(last_message_id) .set_order("id") .exec(db_to_conversation_message) } /// Get older messages of a conversation /// /// `conv_id` contains the ID of the target conversation /// `start_id` contains the ID from wich the research start /// `limit` Maximum number of messages to get pub fn get_older_messages(conv_id: ConvID, start_id: u64, limit: u64) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_conv_id("conv_id", conv_id) .set_custom_where("ID <= ?") .add_custom_where_argument_u64(start_id) .set_order("id DESC") .set_limit(limit) .exec(db_to_conversation_message) .map(|mut l| { l.sort_by(|a, b| a.id.partial_cmp(&b.id).unwrap()); l }) } /// Get all the messages of a single user for a conversation pub fn get_user_messages_for_conversations(conv_id: ConvID, user_id: &UserID) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_conv_id("conv_id", conv_id) .cond_user_id("user_id", user_id) .exec(db_to_conversation_message) } /// Export all the messages of a given user on all conversations pub fn export_all_user_messages(user_id: &UserID) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_user_id("user_id", user_id) .exec(db_to_conversation_message) } /// Clean old user conversation messages pub fn clean_old_messages(user: &User) -> Res { let lifetime = user.delete_conversation_messages_after.unwrap_or(0); if lifetime < 1 { return Ok(()); } let messages = database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_user_id("user_id", &user.id) .set_custom_where("time_insert < ?") .add_custom_where_argument_u64(time() - lifetime) .exec(db_to_conversation_message)?; for message in messages { delete_message(&message)?; } Ok(()) } /// Delete all the messages of a given user pub fn delete_all_user_messages(user_id: &UserID) -> ResultBoxError { for msg in &export_all_user_messages(user_id)? { delete_message(msg)?; } Ok(()) } /// 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, user_id)?; } Ok(()) } /// Get the entire list of messages of a given conversation pub fn get_all_messages(conv_id: ConvID) -> ResultBoxError> { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_conv_id("conv_id", conv_id) .exec(db_to_conversation_message) } /// Get a single message specified by its ID pub fn get_single_message(msg_id: u64) -> ResultBoxError { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_u64("id", msg_id) .query_row(db_to_conversation_message) } /// Send a new conversation message pub fn send_message(msg: &NewConversationMessage) -> ResultBoxError<()> { let t = time(); // Insert the message in the database 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_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_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.as_ref().unwrap_or(&UserID::invalid())) .exec(|r| r.get_user_id("user_id"))?; let new_message = get_single_message(msg_id)?; // Mark the user has seen his message if let Some(user_id) = &msg.user_id { mark_user_seen(msg.conv_id, user_id, &new_message)?; } // Send an event (updated_number_unread_conversations) events_helper::propagate_event(&Event::UpdatedNumberUnreadConversations(&list_to_notify))?; // Send an event (sent_conversation_message) events_helper::propagate_event(&Event::NewConversationMessage(&new_message))?; Ok(()) } /// Update message content pub fn update_message_content(msg_id: u64, new_content: &str) -> ResultBoxError<()> { database::UpdateInfo::new(CONV_MESSAGES_TABLE) .cond_u64("id", msg_id) .set_str("message", new_content) .exec()?; // Send an event (conv_message_updated) events_helper::propagate_event(&Event::UpdatedConversationMessage(&get_single_message(msg_id)?))?; Ok(()) } /// Remove a message from a conversation pub fn delete_message(msg: &ConversationMessage) -> ResultBoxError<()> { // Delete associated files if let Some(file) = &msg.file { delete_user_data_file_if_exists(&file.path)?; if let Some(thumb) = &file.thumbnail { delete_user_data_file_if_exists(thumb)?; } } database::DeleteQuery::new(CONV_MESSAGES_TABLE) .cond_u64("ID", msg.id) .exec()?; // Send en event (conv_message_deleted) events_helper::propagate_event(&Event::DeleteConversationMessage(msg))?; Ok(()) } /// Delete a message with a specific ID pub fn delete_message_by_id(id: u64) -> ResultBoxError<()> { delete_message(&get_single_message(id)?) } /// Count the number of unread conversation for a specified user pub fn count_unread_for_user(user_id: &UserID) -> ResultBoxError { get_list_unread(user_id).map(|l| l.len()) } /// Get the list of unread conversations of a user pub fn get_list_unread(user_id: &UserID) -> ResultBoxError> { // First, get the ID of unread conversation database::QueryInfo::new(CONV_MEMBERS_TABLE) .alias("mem") .join(CONV_MESSAGES_TABLE, "mess", "mem.conv_id = mess.conv_id") .cond_user_id("mem.user_id", user_id) .cond_legacy_bool("mem.following", true) .set_custom_where("mem.last_message_seen < mess.id") .add_field("distinct mem.conv_id") .exec(|r| r.get_conv_id("conv_id")) } /// Indicate that a user has seen the last messages of a conversation pub fn mark_user_seen(conv_id: ConvID, user_id: &UserID, last_msg: &ConversationMessage) -> ResultBoxError<()> { database::UpdateInfo::new(CONV_MEMBERS_TABLE) .cond_conv_id("conv_id", conv_id) .cond_user_id("user_id", user_id) .set_u64("last_message_seen", last_msg.id) .set_u64("last_access", last_msg.time_sent) .exec()?; // Push an event (updated_number_unread_conversations) events_helper::propagate_event(&Event::UpdatedNumberUnreadConversations(&vec![user_id.clone()]))?; Ok(()) } /// Remove a user from a conversation 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, remover) } } /// Remove permanently a conversation pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> { // Delete all the messages of the conversations 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_conv_id("conv_id", conv.id) .exec()?; // Delete associated logo image, if any if let Some(image) = &conv.logo { delete_user_data_file_if_exists(image)?; } // Delete the conversation entry itself database::DeleteQuery::new(CONV_LIST_TABLE) .cond_conv_id("id", conv.id) .exec()?; Ok(()) } /// Delete a conversation membership 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)?; } // Delete membership remove_member(conv_id, user_id)?; Ok(()) } /// Check out whether a user is the owner of a message or not pub fn is_message_owner(user_id: &UserID, message_id: u64) -> ResultBoxError { database::QueryInfo::new(CONV_MESSAGES_TABLE) .cond_u64("id", message_id) .cond_user_id("user_id", user_id) .exec_count() .map(|r| r > 0) } /// Turn a database entry into a ConversationInfo object fn db_to_conversation_info(row: &database::RowResult) -> ResultBoxError { let conv_id = row.get_conv_id("id")?; Ok(Conversation { id: conv_id, color: row.get_optional_str("color")?, logo: row.get_optional_str("logo")?, 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_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")?, is_admin: row.get_legacy_bool("is_admin")?, last_message_seen: row.get_u64("last_message_seen")?, last_access: row.get_optional_u64("last_access")?.unwrap_or(0), }) } /// Turn a database entry into a ConversationMessgae object fn db_to_conversation_message(row: &database::RowResult) -> ResultBoxError { let user_id = match row.is_null("user_id")? { true => None, false => Some(row.get_user_id("user_id")?) }; let file = match row.is_null_or_empty("file_path")? { true => None, false => Some(ConversationMessageFile { 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")?, r#type: row.get_str("file_type")?, }) }; let server_message = match &user_id { Some(_) => None, None => Some(ConversationServerMessageType::from_db(&row.get_str("message")?)?) }; let message = match server_message { None => row.get_optional_str("message")?, Some(_) => None, }; Ok(ConversationMessage { id: row.get_u64("id")?, time_sent: row.get_u64("time_sent")?, conv_id: row.get_conv_id("conv_id")?, user_id, message, server_message, file, }) }