import { Conversation, BaseConversation } from "../entities/Conversation"; import { DatabaseHelper } from "./DatabaseHelper"; import { time } from "../utils/DateUtils"; import { ConversationMessage, BaseConversationMessage } from "../entities/ConversationMessage"; import { UnreadConversation } from "../entities/UnreadConversation"; import { existsSync, unlinkSync } from "fs"; /** * Conversations helper * * @author Pierre HUBERT */ const LIST_TABLE = "comunic_conversations_list"; const USERS_TABLE = "comunic_conversations_users"; const MESSAGES_TABLE = "comunic_conversations_messages"; export class ConversationsHelper { /** * Create a new conversation * * @param conv Information about the conversation to create */ public static async Create(conv : BaseConversation) : Promise { // Create the conversation in the main table const convID = await DatabaseHelper.InsertRow(LIST_TABLE, { "user_id": conv.ownerID, "name": conv.name, "last_active": time(), "creation_time": time() }); // Add the members to the conversation for (const userID of conv.members) { await this.AddMember( convID, userID, conv.ownerID == userID ? conv.following : true); } return convID; } /** * Add a member to a conversation * * @param convID Conversation ID * @param userID User ID * @param following Specify whether the user is following * the conversation or not */ private static async AddMember(convID : number, userID: number, following : boolean = true) { await DatabaseHelper.InsertRow( USERS_TABLE, { "conv_id": convID, "user_id": userID, "time_add": time(), "following": following ? 1 : 0, "saw_last_message": 1 } ); } /** * Remove a user from a conversation * * @param convID Conversation ID * @param userID ID of the user to remove */ private static async RemoveMember(convID: number, userID: number) { await DatabaseHelper.DeleteRows(USERS_TABLE, { conv_id: convID, user_id: userID }); } /** * Get the list of conversations of the user * * @param userID Target user ID */ public static async GetListUser(userID: number) : Promise> { // Fetch the list of conversations const result = await DatabaseHelper.Query({ fields: [ "*", "l.id as id", "l.user_id as owner_id" // The field conflits with user.user_id ], table: LIST_TABLE + " l", joins: [ // Joins with conversation members table { table: USERS_TABLE + " u", condition: "l.id = u.conv_id" } ], where: { "u.user_id": userID }, order: "l.last_active DESC" }); const list = []; for (const el of result) { list.push(await this.DBToConversationInfo(el)); } return list; } /** * Get information about a single conversation * * @param convID The ID of the conversation to get */ public static async GetSingle(convID : number, userID: number) : Promise { const result = await DatabaseHelper.QueryRow({ fields: [ "*", "l.id as id", "l.user_id as owner_id", ], table: LIST_TABLE + " l", joins: [ // Joins with conversation members table { table: USERS_TABLE + " u", condition: "l.id = u.conv_id" } ], where: { "l.id": convID, "u.user_id": userID } }); if(!result) return null; return await this.DBToConversationInfo(result); } /** * Check out whether a user is the member of a conversation or not * * @param userID Target user ID * @param convID Target conversation */ public static async DoesUsersBelongsTo(userID: number, convID: number) : Promise { return await DatabaseHelper.Count({ table: USERS_TABLE, where: { conv_id: convID, user_id: userID } }) == 1; } /** * Change the name of a conversation * * @param convID Target conversation * @param name New name for the conversation (empty name * to remove it) */ public static async SetName(convID: number, name: string) { await DatabaseHelper.UpdateRows({ table: LIST_TABLE, where: { id: convID }, set: { name: name } }); } /** * Set a new list of members for a given conversation * * @param convID Target conversation ID * @param members The new list of members for the conversation */ public static async SetMembers(convID: number, newList: Set) { const currentList = await this.GetConversationMembers(convID); // Add new members for (const member of newList) { if(currentList.has(member)) continue; await this.AddMember(convID, member, true); } // Remove old members for(const member of currentList) { if(newList.has(member)) continue; await this.RemoveMember(convID, member); } } /** * Update following state of the conversation * * @param userID User to update * @param convID Target conversation ID * @param following New status */ public static async SetFollowing(userID: number, convID: number, following: boolean) { await DatabaseHelper.UpdateRows({ table: USERS_TABLE, set: { "following": following ? 1 : 0 }, where: { "conv_id": convID, "user_id": userID } }); } /** * Check out whether a user is the moderator of a conversation or not * * @param userID User to check * @param convID Target conversation */ public static async IsUserModerator(userID : number, convID : number) : Promise { return await DatabaseHelper.Count({ table: LIST_TABLE, where: { id: convID, user_id: userID } }) == 1; } /** * Check out whether a user is the owner of a message or not * * @param userID Target user ID * @param messageID Target message ID */ public static async IsUserMessageOwner(userID: number, messageID: number) : Promise { return (await DatabaseHelper.Count({ table: MESSAGES_TABLE, where: { id: messageID, user_id: userID } })) > 0; } /** * Get the last messages of a conversation * * @param convID Target conversation ID * @param numberOfMessages The maximum number of messages to return */ public static async GetLastMessages(convID: number, numberOfMessages: number) : Promise> { return (await DatabaseHelper.Query({ table: MESSAGES_TABLE, where: { conv_id: convID }, limit: numberOfMessages, order: "id DESC" })).map(m => this.DBToConversationMessage(convID, m)).reverse(); } /** * Get the new messages of a conversation * * @param convID Target conversation ID * @param lastMessageID The ID of the last known message */ public static async GetNewMessages(convID: number, lastMessageID: number): Promise> { return (await DatabaseHelper.Query({ table: MESSAGES_TABLE, where: { conv_id: convID }, customWhere: "ID > ?", customWhereArgs: [lastMessageID.toString()], order: "id" })).map(m => this.DBToConversationMessage(convID, m)); } /** * Get all the messages of a single user for a conversation * * @param convID Target conversation ID * @param userID Target user ID */ public static async GetUserMessagesForConversation(convID: number, userID: number): Promise> { return (await DatabaseHelper.Query({ table: MESSAGES_TABLE, where: { conv_id: convID, user_id: userID } })).map(m => this.DBToConversationMessage(convID, m)); } /** * Get information about a single conversation message * * @param messageID The ID of the message to get * @throws If the message was not found */ private static async GetSingleMessage(messageID: number) : Promise { const row = await DatabaseHelper.QueryRow({ table: MESSAGES_TABLE, where: { id: messageID } }); if(row == null) throw Error("The message was not found!"); return this.DBToConversationMessage(row.conv_id, row); } /** * Get older messages of a conversation * * @param convID ID of the target conversation * @param startID ID from which the research should start * @param limit Maximum number of messages to get * @return The list of messages */ public static async GetOlderMessage(convID: number, startID: number, limit: number) : Promise> { return (await DatabaseHelper.Query({ table: MESSAGES_TABLE, where: { conv_id: convID, }, customWhere: "ID <= ?", customWhereArgs: [startID.toString()], order: "id DESC", limit: limit })) .map(m => this.DBToConversationMessage(convID, m)).reverse(); } /** * Mark the user has seen the last messages of the conversation * * @param convID Target conversation ID * @param userID Target user ID */ public static async MarkUserSeen(convID: number, userID: number) { await DatabaseHelper.UpdateRows({ table: USERS_TABLE, where: { conv_id: convID, user_id: userID }, set: { saw_last_message: 1 } }); } /** * Insert a new message into the database * * @param message The message to insert */ public static async SendMessage(message: BaseConversationMessage) { const t = time(); // Insert the message in the database await DatabaseHelper.InsertRow( MESSAGES_TABLE, { conv_id: message.convID, user_id: message.userID, time_insert: t, message: message.message, image_path: message.imagePath } ); // Update the last activity of the conversation await DatabaseHelper.UpdateRows({ table: LIST_TABLE, where: { id: message.convID }, set: { last_active: t, } }); // Mark all the user of the conversations as unread, except current user await DatabaseHelper.UpdateRows({ table: USERS_TABLE, where: { conv_id: message.convID }, customWhere: "user_id != ?", customWhereArgs: [message.userID.toString()], set: { saw_last_message: 0 } }); } /** * Update message content * * @param messageID Target message ID * @param newContent New message content */ public static async UpdateMessageContent(messageID: number, newContent: string) { await DatabaseHelper.UpdateRows({ table: MESSAGES_TABLE, where: { id: messageID }, set: { message: newContent } }); } /** * Search for private conversations between two users * * @param user1 The first user * @param user2 The second user * @returns The entire list of found conversations */ public static async FindPrivate(user1: number, user2: number) : Promise> { const result = await DatabaseHelper.Query({ table: USERS_TABLE, tableAlias: "t1", joins: [ { table: USERS_TABLE, tableAlias: "t2", condition: "t1.conv_id = t2.conv_id" } ], where: { "t1.user_id": user1, "t2.user_id": user2 }, customWhere: "(SELECT COUNT(*) FROM " + USERS_TABLE + " WHERE conv_id = t1.conv_id) = 2", fields: ["t1.conv_id AS conv_id"] }); return result.map(r => r.conv_id); } /** * Count the number of unread conversations of the user * * @param userID Target user ID */ public static async CountUnreadForUser(userID: number) : Promise { return await DatabaseHelper.Count({ table: USERS_TABLE, where: { user_id: userID, saw_last_message: 0, following: 1 } }); } /** * Get the list of unread conversations of the user * * @param userID Target user ID */ public static async GetListUnread(userID: number) : Promise> { return (await DatabaseHelper.Query({ table: USERS_TABLE, tableAlias: "users", joins: [ // Join with conversations list table { table: LIST_TABLE, tableAlias: "list", condition: "users.conv_id = list.id" }, // Join with message table to get the latest message { table: MESSAGES_TABLE, tableAlias: "messages", condition: "messages.conv_id = users.conv_id" } ], where: { "users.user_id": userID, "users.following": 1, "users.saw_last_message": 0, }, customWhere: "list.last_active = messages.time_insert", order: "list.last_active DESC" })).map(m => { id: m.conv_id, name: m.name, lastActive: m.last_active, userID: m.user_id, message: m.message, }); } /** * Remove a user from a conversation * * @param userID Target user ID * @param convID Target conversation ID */ public static async RemoveUserFromConversation(userID: number, convID: number) { // Check whether the user is the owner of the conversation or not if(await this.IsUserModerator(userID, convID)) await this.DeleteConversations(convID); else // Only delete the messages & membership of teh user await this.DeleteMember(convID, userID); } /** * Delete a conversation * * @param convID The ID of the conversation to delete */ private static async DeleteConversations(convID: number) { // Get & delete all the messages of the conversations const messages = await this.GetNewMessages(convID, 0); for (const message of messages) { await this.DeleteMessage(message); } // Delete all the members of the conversation await DatabaseHelper.DeleteRows(USERS_TABLE, { conv_id: convID }); // Delete the conversation entry itself await DatabaseHelper.DeleteRows(LIST_TABLE, { id: convID }); } /** * Delete a conversation membership * * @param convID Target conversation * @param memberID Target user ID */ private static async DeleteMember(convID: number, memberID: number) { // Get & delete all the messages of the member const messages = await this.GetUserMessagesForConversation(convID, memberID); for (const message of messages) { await this.DeleteMessage(message); } // Delete membership await this.RemoveMember(convID, memberID); } /** * Delete a conversation message identified by its ID * * @param id The ID of the message to delete */ public static async DeleteMessageById(id: number) { // Get information about the message const message = await this.GetSingleMessage(id); await this.DeleteMessage(message); } /** * Delete a conversation message from the database * * @param m The message to delete */ private static async DeleteMessage(m: ConversationMessage) { // Delete conversation message image (if any) if(m.hasImage) { if(existsSync(m.imageSysPath)) unlinkSync(m.imageSysPath); } // Delete the message from the database await DatabaseHelper.DeleteRows( MESSAGES_TABLE, { ID: m.id } ) } /** * Get the list of members of a conversation * * @param convID The ID of the target conversation */ private static async GetConversationMembers(convID : number): Promise> { const result = await DatabaseHelper.Query({ table: USERS_TABLE, where: { "conv_id": convID }, fields: ["user_id"] }); return new Set(result.map((e) => e.user_id)); } /** * Turn a database entry into a conversation object * * @param row */ private static async DBToConversationInfo(row: any) : Promise { return { id: row.id, ownerID: row.owner_id, name: row.name, lastActive: row.last_active, timeCreate: row.time_add, following: row.following, sawLastMessage: row.saw_last_message == 1, members: await this.GetConversationMembers(row.id) } } /** * Turn a database entry into a conversation message * * @param convID The ID of the conversation the message belongs to * @param row Row to convert * @return Generated conversation message */ private static DBToConversationMessage(convID: number, row: any) : ConversationMessage { return new ConversationMessage({ id: row.id, convID: convID, userID: row.user_id, timeSent: row.time_insert, imagePath: row.image_path ? row.image_path : "", message: row.message ? row.message : "" }); } }