1
0
mirror of https://gitlab.com/comunic/comunicapiv3 synced 2025-01-30 14:03:00 +00:00
comunicapiv3/src/helpers/conversations_helper.rs

702 lines
25 KiB
Rust

//! # 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, ConvID, NewConversationSettings};
use crate::data::conversation_message::{ConversationMessage, ConversationMessageFile, ConversationServerMessageType, UserAddedAnotherUserToConversation, UserRemovedAnotherUserToConversation};
use crate::data::error::{ExecError, Res, ResultBoxError};
use crate::data::group_id::GroupID;
use crate::data::group_member::GroupMembershipLevel;
use crate::data::new_conversation::NewConversation;
use crate::data::new_conversation_message::NewConversationMessage;
use crate::data::user::{User, UserID};
use crate::helpers::{database, events_helper, groups_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<ConvID> {
// Create the conversation in the main table
let conv_id = InsertQuery::new(CONV_LIST_TABLE)
.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())
.add_opt_u32("min_group_membership_level",
conv.group_min_membership_level.as_ref().map(|s| s.to_db()))
.insert()?
.map(|i| ConvID::new(i))
.ok_or(ExecError::new("missing result conv id!"))?;
// Initialize the list of members of the group
if conv.group_id.is_some() {
update_members_list_for_group_conversation(conv_id)?;
} else {
// Add the creator of the conversation
add_member(conv_id, &conv.owner_id, conv.owner_following, true, Some(&conv.owner_id))?;
// Add other members to the conversation
for member in &conv.members {
if !member.eq(&conv.owner_id) {
add_member(conv_id, member, true, false, Some(&conv.owner_id))?;
}
}
}
Ok(conv_id)
}
/// Create a conversation for a group
pub fn create_conversation_for_group(group_id: GroupID, min_membership_level: GroupMembershipLevel, name: &String) -> Res<ConvID> {
create(&NewConversation {
owner_id: UserID::invalid(),
name: Some(name.to_string()),
group_id: Some(group_id),
group_min_membership_level: Some(min_membership_level),
color: None,
logo: None,
owner_following: false,
members: Default::default(),
can_everyone_add_members: false,
})
}
/// Add a member to a conversation
pub fn add_member(conv_id: ConvID, user_id: &UserID, following: bool, admin: bool, adder: Option<&UserID>) -> 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()?;
// Send the messages (if possible, no messages are created for groups conversations)
if let Some(adder) = adder {
// Create a message
if adder != user_id {
send_message(
&NewConversationMessage::new_server_message(
conv_id,
ConversationServerMessageType::UserAddedAnotherUserToConversation(UserAddedAnotherUserToConversation {
user_who_added: adder.clone(),
user_added: user_id.clone(),
}),
)
)?;
} else {
send_message(&NewConversationMessage::new_server_message(
conv_id, ConversationServerMessageType::UserCreatedConversation(user_id.clone()),
))?;
}
}
Ok(())
}
/// 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()
}
/// Get the list of conversations of a specific user
pub fn get_list_user(user_id: &UserID) -> ResultBoxError<Vec<Conversation>> {
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 the list of conversations of a group
pub fn get_list_group(group_id: &GroupID) -> Res<Vec<Conversation>> {
database::QueryInfo::new(CONV_LIST_TABLE)
.cond_group_id("group_id", group_id)
.exec(db_to_conversation_info)
}
/// Get information about a single conversation
pub fn get_single(conv_id: ConvID) -> ResultBoxError<Conversation> {
// 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<Vec<ConversationMember>> {
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<ConversationMember> {
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<bool> {
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 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()
}
/// Change minimal membership level to join a group conversation
pub fn set_min_group_conversation_membership_level(conv_id: ConvID, level: GroupMembershipLevel) -> Res {
database::UpdateInfo::new(CONV_LIST_TABLE)
.cond_conv_id("id", conv_id)
.set_u32("min_group_membership_level", level.to_db())
.exec()?;
update_members_list_for_group_conversation(conv_id)
}
/// Search for private conversation between two users
pub fn find_private(user_1: &UserID, user_2: &UserID) -> ResultBoxError<Vec<ConvID>> {
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<Vec<ConversationMessage>> {
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<Vec<ConversationMessage>> {
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<Vec<ConversationMessage>> {
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<Vec<ConversationMessage>> {
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<Vec<ConversationMessage>> {
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)?;
}
// Remove all server messages related with the user
database::DeleteQuery::new(CONV_MESSAGES_TABLE)
.set_custom_where(&format!(
"user_id IS NULL AND ((message LIKE \"%-{}-%\") OR (message LIKE \"%-{}\"))",
user_id.id(),
user_id.id()
))
.exec()?;
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<Vec<ConversationMessage>> {
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<ConversationMessage> {
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_opt_user_id("user_id", msg.user_id.clone())
.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<usize> {
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<Vec<ConvID>> {
// 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 {
remove_member(user_id, conv.id, Some(remover))
}
}
/// Update members list for all the conversations of a given group
pub fn update_members_list_for_group_conversations(group_id: &GroupID) -> Res {
for conv in get_list_group(group_id)? {
update_members_list_for_group_conversation(conv.id)?;
}
Ok(())
}
/// Update the list of members for a group conversation
pub fn update_members_list_for_group_conversation(conv_id: ConvID) -> Res {
let conv = get_single(conv_id)?;
if !conv.is_linked_to_group() {
return Err(ExecError::boxed_new("Attempted to update members list for a non-group conversation!"));
}
let group_members = groups_helper::get_list_members(conv.group_id.as_ref().unwrap())?;
// Add missing memberships / Update existing invalid memberships
for member in &group_members {
let conv_member = conv.members.iter().filter(|f| f.user_id == member.user_id).next();
if let Some(conv_member) = conv_member {
// Update admin status, if required
if conv_member.is_admin != member.is_admin() {
set_admin(&conv_id, &member.user_id, member.is_admin())?;
}
}
// Create the member
else if conv.min_group_membership_level.as_ref().unwrap() >= &member.level {
add_member(conv_id, &member.user_id, true, member.is_admin(), None)?;
}
}
// Remove memberships that have to be removed
for conv_member in &conv.members {
let member = group_members.iter().filter(|m| m.user_id == conv_member.user_id).next();
// Remove the member, if required
if member.is_none() || conv.min_group_membership_level.as_ref().unwrap() < &member.unwrap().level {
remove_member(&conv_member.user_id, conv_id, None)?;
}
}
Ok(())
}
/// Remove permanently a conversation
pub fn delete_conversation(conv: &Conversation) -> ResultBoxError<()> {
// Delete all the messages of the conversations
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()?;
// Propagate information
events_helper::propagate_event(&Event::DeletedConversation(conv.id))?;
Ok(())
}
/// Delete all the conversations of a group
pub fn delete_all_group_conversations(group_id: &GroupID) -> Res {
for conv in get_list_group(group_id)? {
delete_conversation(&conv)?;
}
Ok(())
}
/// Delete a conversation membership
pub fn remove_member(user_id: &UserID, conv_id: ConvID, remover: Option<&UserID>) -> ResultBoxError<()> {
for msg in get_user_messages_for_conversations(conv_id, user_id)? {
delete_message(&msg)?;
}
// Delete membership
database::DeleteQuery::new(CONV_MEMBERS_TABLE)
.cond_conv_id("conv_id", conv_id)
.cond_user_id("user_id", user_id)
.exec()?;
// Create a message
if let Some(remover) = remover {
if remover == user_id {
send_message(&NewConversationMessage::new_server_message(
conv_id,
ConversationServerMessageType::UserLeftConversation(user_id.clone()),
))?;
} else {
send_message(&NewConversationMessage::new_server_message(
conv_id,
ConversationServerMessageType::UserRemovedFromConversation(UserRemovedAnotherUserToConversation {
user_who_removed: remover.clone(),
user_removed: user_id.clone(),
}),
))?;
}
}
// Propagate event
events_helper::propagate_event(&Event::RemovedUserFromConversation(user_id, conv_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<bool> {
database::QueryInfo::new(CONV_MESSAGES_TABLE)
.cond_u64("id", message_id)
.cond_user_id("user_id", user_id)
.exec_count()
.map(|r| r > 0)
}
/// Remove conversation image
pub fn remove_conversation_image(conv: &Conversation) -> Res {
if let Some(image) = &conv.logo {
delete_user_data_file_if_exists(image)?;
database::UpdateInfo::new(CONV_LIST_TABLE)
.cond_conv_id("id", conv.id)
.set_opt_str("logo", None)
.exec()?;
}
Ok(())
}
/// Set a new conversation image
pub fn set_conversation_image(conv: &Conversation, new_image: &str) -> Res {
database::UpdateInfo::new(CONV_LIST_TABLE)
.cond_conv_id("id", conv.id)
.set_opt_str("logo", Some(new_image.to_string()))
.exec()
}
/// Turn a database entry into a ConversationInfo object
fn db_to_conversation_info(row: &database::RowResult) -> ResultBoxError<Conversation> {
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")?,
min_group_membership_level: row.get_optional_u32("min_group_membership_level")?
.map(GroupMembershipLevel::from_db),
})
}
/// Turn a database entry into a ConversationMember object
fn db_to_conversation_member(row: &database::RowResult) -> Res<ConversationMember> {
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<ConversationMessage> {
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,
})
}