1
0
mirror of https://gitlab.com/comunic/comunicapiv3 synced 2024-11-26 07:19:22 +00:00

Continue refactoring

This commit is contained in:
Pierre HUBERT 2021-03-04 18:51:52 +01:00
parent 2cc8984dfa
commit fbf4728347
27 changed files with 527 additions and 355 deletions

View File

@ -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

View File

@ -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(),
}
}

View File

@ -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(),
}
}

View File

@ -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<String>);
impl Serialize for ConvName {
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::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<u64>,
canEveryoneAddMembers: bool,
id: u64,
last_activity: u64,
name: Option<String>,
color: Option<String>,
background: Option<String>,
group_id: Option<u64>,
members: Vec<ConversationMembersAPI>,
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),

View File

@ -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(),

View File

@ -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<String, Vec<ConversationMessageAPI>>,
}
impl ConversationRefreshResultAPI {
/// Create a new list
pub fn new(list: HashMap<u64, Vec<ConversationMessage>>) -> ConversationRefreshResultAPI {
let list = list
.iter()
.map(|v| (
format!("conversation-{}", v.0),
ConversationMessageAPI::for_list(v.1)
))
.collect();
ConversationRefreshResultAPI {
list
}
}
}

View File

@ -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(),
}
}

View File

@ -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(),
}
}

View File

@ -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;

View File

@ -19,7 +19,7 @@ pub struct NewCallSignalAPI {
impl NewCallSignalAPI {
pub fn new(call_id: &ConvID, peer_id: &UserID, data: &str) -> Res<Self> {
Ok(Self {
callID: call_id.clone(),
callID: call_id.id(),
peerID: peer_id.id(),
data: serde_json::from_str(data)?,
})

View File

@ -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(),
}
}

View File

@ -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<u64>) -> ResFindPrivateConversations {
pub fn new(list: Vec<ConvID>) -> ResFindPrivateConversations {
ResFindPrivateConversations {
conversationsID: list
conversationsID: list.iter().map(|i| i.id()).collect()
}
}
}

View File

@ -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";
@ -164,3 +163,6 @@ 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";
/// Minimum message length
pub const MIN_CONVERSATION_MESSAGE_LENGTH: usize = 1;

View File

@ -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<ConvID> {
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::<u64>()?;
let call_id = ConvID::new(split[0].parse::<u64>()?);
let peer_id = UserID::new(split[1].parse::<u64>()?);
let target_user = UserID::new(msg.peer_id.parse::<u64>()?);

View File

@ -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::<Vec<UserID>>();
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::<Vec<ConversationMemberSetting>>();
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::<Vec<ConversationMemberSetting>>();
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<String, HashMap<String, serde_json::Value>> =
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::<u64>()?;
// 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)),
)?;
}

View File

@ -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")
}

View File

@ -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<SurveyResponse>,
pub all_conversation_messages: Vec<ConversationMessage>,
pub conversations: Vec<Conversation>,
pub conversation_messages: HashMap<u64, Vec<ConversationMessage>>,
pub conversation_messages: HashMap<ConvID, Vec<ConversationMessage>>,
pub friends_list: Vec<Friend>,
pub groups: Vec<GroupID>,
}
@ -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

View File

@ -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<String> {
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<HashSet<UserID>> {
let users = self.post_numbers_list(name, 1)?
.iter()
.map(|u| UserID::new(u.clone()))
.collect::<HashSet<UserID>>();
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<ConvID> {
let conv_id = self.post_u64(name)?;
fn post_conv(&mut self, name: &str) -> ResultBoxError<ConversationMember> {
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<Option<String>> {
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))
}
}
}
}

View File

@ -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<String>,
pub members: Vec<UserID>,
pub color: Option<String>,
pub background: Option<String>,
pub creation_time: u64,
pub group_id: Option<GroupID>,
pub can_everyone_add_members: bool,
pub last_activity: u64,
pub members: Vec<ConversationMember>,
}
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<UserID> {
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<String>,
pub name: Option<String>,
pub can_everyone_add_members: bool,
pub last_active: u64,
pub time_create: u64,
pub following: bool,
pub saw_last_message: bool,
}

View File

@ -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<UserID>,
pub message: Option<String>,
pub server_message: Option<ConversationServerMessageType>,

View File

@ -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<String>,
pub group_id: Option<GroupID>,
pub color: Option<String>,
pub background: Option<String>,
pub owner_following: bool,
pub members: Vec<UserID>,
pub can_everyone_add_members: bool

View File

@ -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<String>
pub user_id: Option<UserID>,
pub conv_id: ConvID,
pub message: Option<String>,
pub file: Option<ConversationMessageFile>,
pub server_message: Option<ConversationServerMessageType>
}

View File

@ -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,
}
}
}

View File

@ -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<u64> {
pub fn create(conv: &NewConversation) -> Res<ConvID> {
// 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<Vec<Conversation>> {
database::QueryInfo::new(CONV_LIST_TABLE)
@ -71,67 +95,46 @@ pub fn get_list_user(user_id: &UserID) -> ResultBoxError<Vec<Conversation>> {
.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<Conversation> {
pub fn get_single(conv_id: ConvID) -> ResultBoxError<Conversation> {
// 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<Vec<UserID>> {
pub fn get_list_members(conv_id: ConvID) -> Res<Vec<ConversationMember>> {
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<bool> {
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<ConversationMember> {
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<bool> {
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<bool> {
pub fn can_everyone_add_members(conv_id: ConvID) -> ResultBoxError<bool> {
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<UserID>, can_delete: bool) -> ResultBoxError<()> {
pub fn set_members(conv_id: ConvID, new_list: &Vec<ConversationMemberSetting>, 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<UserID>, can_delete: bool) -> Re
}
/// Set a new name to the conversation
pub fn set_name(conv_id: u64, name: Option<String>) -> 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<Vec<u64>> {
pub fn find_private(user_1: &UserID, user_2: &UserID) -> ResultBoxError<Vec<ConvID>> {
database::QueryInfo::new(CONV_MEMBERS_TABLE)
.alias("t1")
@ -201,7 +201,7 @@ pub fn find_private(user_1: &UserID, user_2: &UserID) -> ResultBoxError<Vec<u64>
.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<Vec<ConversationMessage>> {
pub fn get_user_messages_for_conversations(conv_id: ConvID, user_id: &UserID) -> ResultBoxError<Vec<ConversationMessage>> {
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<Vec<ConversationMessage>> {
pub fn get_all_messages(conv_id: ConvID) -> ResultBoxError<Vec<ConversationMessage>> {
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<Vec<UnreadConversatio
.cond_user_id("users.user_id", user_id)
.cond_legacy_bool("users.following", true)
.cond_legacy_bool("users.saw_last_message", false)
.set_custom_where("list.last_active = messages.time_insert")
.set_custom_where("list.last_activity = messages.time_insert AND user.last_message_seen < messages.id")
.set_order("list.last_active DESC")
.set_order("list.last_activity DESC")
.add_field("messages.conv_id")
.add_field("name")
.add_field("last_active")
.add_field("last_activity")
.add_field("messages.user_id")
.add_field("message")
.exec(|res| Ok(UnreadConversation {
id: res.get_u64("conv_id")?,
name: res.get_optional_str("name")?,
last_active: res.get_u64("last_active")?,
last_active: res.get_u64("last_activity")?,
user_id: res.get_user_id("user_id")?,
message: res.get_str("message")?,
}))
}
/// Indicate that a user has seen the last messages of a conversation
pub fn mark_user_seen(conv_id: u64, user_id: &UserID) -> 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<boo
/// Turn a database entry into a ConversationInfo object
fn db_to_conversation_info(row: &database::RowResult) -> ResultBoxError<Conversation> {
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<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")?,
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<Conve
false => 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<Conve
Ok(ConversationMessage {
id: row.get_u64("id")?,
time_sent: row.get_u64("time_sent")?,
conv_id: row.get_u64("conv_id")?,
conv_id: row.get_conv_id("conv_id")?,
user_id,
message,
server_message,

View File

@ -12,6 +12,7 @@ use crate::data::config::{conf, DatabaseConfig};
use crate::data::error::{ExecError, ResultBoxError};
use crate::data::group_id::GroupID;
use crate::data::user::UserID;
use crate::data::conversation::ConvID;
/// Database access helper
///
@ -174,6 +175,11 @@ impl QueryInfo {
self
}
pub fn cond_conv_id(mut self, key: &str, val: ConvID) -> 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<Option<GroupID>> {
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<ConvID> {
Ok(ConvID::new(self.get_u64(name)?))
}
/// Find a string included in the request
pub fn get_str(&self, name: &str) -> Result<String, Box<dyn Error>> {
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<u64>) -> 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<GroupID>) -> 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));

View File

@ -196,7 +196,6 @@ pub fn get_routes() -> Vec<Route> {
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> {
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

View File

@ -93,3 +93,17 @@ 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)
}