UNPKG

@salad-labs/loopz-typescript

Version:
884 lines 242 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Engine, User, Conversation, QIError, Message, ConversationReport, MessageReport, ConversationTradingPool, MessageImportant, ConversationPin, Crypto, } from "./core/chat"; import { addBlockedUser, addImportantToMessage, addMembersToConversation, addPinToConversation, addPinToMessage, addReactionToMessage, addReportToConversation, addReportToMessage, archiveConversation, archiveConversations, createConversationGroup, createConversationOneToOne, deleteBatchConversationMessages, deleteConversationMessage, deleteRequestTrade, editMessage, ejectMember, eraseConversationByAdmin, joinConversation, leaveConversation, muteConversation, removeBlockedUser, removeImportantFromMessage, removePinFromConversation, removePinFromMessage, removeReactionFromMessage, requestTrade, sendMessage, unarchiveConversation, unarchiveConversations, unmuteConversation, updateConversationGroup, updateRequestTrade, updateUserInfo, } from "./constants/chat/mutations"; import { findUsersByUsername, getConversationById, getCurrentUser, listAllActiveUserConversationIds, listConversationsByIds, listConversationsPinnedByCurrentUser, listMessagesByConversationId, listMessagesImportantByUserConversationId, listConversationMemberByUserId, listUsersByIds, listTradesByConversationId, getConversationTradingPoolById, listMessagesByRangeOrder, listMessagesUpdated, getMembersFromConversationById, } from "./constants/chat/queries"; import { ConversationMember } from "./core/chat/conversationmember"; import { onDeleteMessage, onEditMessage, onSendMessage, onRemoveReaction, onAddReaction, onAddPinMessage, onRemovePinMessage, onUpdateConversationGroup, onEjectMember, onLeaveConversation, onAddPinConversation, onRemovePinConversation, onMuteConversation, onUnmuteConversation, onUpdateUser, onRequestTrade, onDeleteRequestTrade, onUpdateRequestTrade, onAddMembersToConversation, onBatchDeleteMessages, onChatMemberEvents, onChatMessageEvents, onChatJoinEvents, } from "./constants/chat/subscriptions"; import { ActiveUserConversationType, ConversationTradingPoolStatus, MessageType, } from "./enums"; import { Converter, findAddedAndRemovedConversation, Serpens } from "./core"; import { Reaction } from "./core/chat/reaction"; import { v4 as uuidv4 } from "uuid"; import { Auth } from "."; import { DetectiveMessage } from "./core/chat/detectivemessage"; export class Chat extends Engine { constructor() { if (!Chat._config) throw new Error("Chat must be configured before getting the instance"); super(Chat._config); this._isSyncing = false; this._syncingCounter = 0; this._eventsCallbacks = []; this._unsubscribeSyncSet = []; this._conversationsMap = []; this._canChat = false; this._hookMessageCreated = false; this._hookMessageUpdated = false; this._hookMessageDeleted = false; this._hookConversationCreated = false; this._hookConversationUpdated = false; this._syncRunning = false; this._hookMessageCreatingFn = null; this._hookMessageUpdatingFn = null; this._hookConversationCreatingFn = null; this._hookConversationUpdatingFn = null; this._syncTimeout = null; this.SYNCING_TIME_MS = 60000; this._currentPublicConversation = null; //let's build the instance of DetectiveMessage, starting the scan immediately if (!Chat._detectiveMessage) { DetectiveMessage.config({ storage: Chat._config.storage }); Chat._detectiveMessage = DetectiveMessage.getInstance(); } this._defineHookFnLocalDB(); this._syncPublicConversationState(); this._setupStorageEventListener(); Chat._instance = this; } /** static methods */ static config(config) { if (Chat._config) throw new Error("Chat already configured"); Chat._config = config; } static getInstance() { var _a; return (_a = Chat._instance) !== null && _a !== void 0 ? _a : new Chat(); } static getDetectiveMessageInstance() { return Chat._detectiveMessage; } static silentRestoreSubscriptionSync() { if (!Chat._instance) return; //let's clear the timeout if (Chat._instance._syncTimeout) clearTimeout(Chat._instance._syncTimeout); //let's unsubscribe everything from the previous results Chat._instance._removeSubscriptionsSync(); //let's clear the _unsubscribeSyncSet from the previous results Chat._instance._unsubscribeSyncSet = []; //add member to conversation. This event is global, basically the user is always listening if //someone wants to add him into a conversation. const onAddMembersToConversation = Chat._instance.onAddMembersToConversation((response, source, uuid) => { Chat._instance._onAddMembersToConversationSync(response, source, uuid); }, true); if (!(onAddMembersToConversation instanceof QIError)) { const { unsubscribe, uuid } = onAddMembersToConversation; Chat._instance._unsubscribeSyncSet.push({ type: "onAddMembersToConversation", unsubscribe, uuid, }); } else { const error = Chat._instance._handleUnauthorizedQIError(onAddMembersToConversation); if (error) { ; (() => __awaiter(this, void 0, void 0, function* () { if (!Chat._instance) return; yield Auth.fetchAuthToken(); Chat._instance.silentReset(); }))(); return; } Chat._instance._emit("syncError", { error: onAddMembersToConversation, }); Chat._instance.unsync(); return; } //now that we have a _conversationsMap array filled, we can add subscription for every conversation that is currently active Chat._instance._addSubscriptionsSync(); (() => __awaiter(this, void 0, void 0, function* () { if (!Chat._instance) return; yield Chat._instance._sync(Chat._instance._syncingCounter); }))(); } static unsyncBrutal() { return new Promise((resolve, reject) => { if (!Chat._instance) { reject("instance not setup correctly."); return; } Chat._instance.unsync().then(resolve).catch(reject); }); } /** message content handling */ _getMessageContent(content, type) { if ((type === MessageType.Textual || type === MessageType.Attachment || type === MessageType.Rent || type === MessageType.TradeProposal) && typeof content !== "string") throw new Error("The content of a textual message can not be different from a string."); switch (type) { case MessageType.Textual: case MessageType.Attachment: case MessageType.Rent: case MessageType.TradeProposal: return content; break; case MessageType.Nft: return JSON.stringify(content); break; } } /** private instance methods */ /** * Internal method to set the current public conversation * @param {string} conversationId */ _setCurrentPublicConversation(conversationId) { this._currentPublicConversation = { conversationId, }; } /** * Synchronize the public conversation state across windows */ _syncPublicConversationState() { const storedConversation = localStorage.getItem(Chat.PUBLIC_CONVERSATION_KEY); if (storedConversation) { const parsedConversation = JSON.parse(storedConversation); this._currentPublicConversation = parsedConversation; } else { this._currentPublicConversation = null; } } /** * Updates the public conversation state in localStorage */ _updatePublicConversationStorage(conversationId) { if (conversationId) { // Save the new conversation localStorage.setItem(Chat.PUBLIC_CONVERSATION_KEY, JSON.stringify({ conversationId, })); } else { // Remove the current conversation localStorage.removeItem(Chat.PUBLIC_CONVERSATION_KEY); } } /** * Adds a listener for cross-window storage events */ _setupStorageEventListener() { window.addEventListener("storage", (event) => { if (event.key === Chat.PUBLIC_CONVERSATION_KEY) { // Another window has changed the conversation state this._syncPublicConversationState(); this._emit("publicConversationChange", { oldConversation: event.oldValue, newConversation: event.newValue, }); } }); } _defineHookFnLocalDB() { this._hookMessageCreatingFn = (primaryKey, record) => { const _message = Object.assign(Object.assign({}, record), { content: Crypto.decryptAESorFail(record.content, this.findKeyPairById(record.conversationId)), reactions: record.reactions ? record.reactions.map((reaction) => { return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) }); }) : null, createdAt: new Date(record.createdAt), updatedAt: record.updatedAt ? new Date(record.updatedAt) : null, deletedAt: record.deletedAt ? new Date(record.deletedAt) : null }); _message.messageRoot = record.messageRoot ? Object.assign(Object.assign({}, record.messageRoot), { content: Crypto.decryptAESorFail(record.messageRoot.content, this.findKeyPairById(record.conversationId)), reactions: record.messageRoot.reactions ? record.messageRoot.reactions.map((reaction) => { return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) }); }) : null, createdAt: new Date(record.messageRoot.createdAt), updatedAt: record.messageRoot.updatedAt ? new Date(record.messageRoot.updatedAt) : null, deletedAt: record.messageRoot.deletedAt ? new Date(record.messageRoot.deletedAt) : null }) : null; this._emit("messageCreatedLDB", _message); }; this._hookMessageUpdatingFn = (modifications, primaryKey, record) => { const _message = Object.assign(Object.assign({}, record), { content: Crypto.decryptAESorFail(record.content, this.findKeyPairById(record.conversationId)), reactions: record.reactions ? record.reactions.map((reaction) => { return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) }); }) : null, createdAt: new Date(record.createdAt), updatedAt: record.updatedAt ? new Date(record.updatedAt) : null, deletedAt: record.deletedAt ? new Date(record.deletedAt) : null }); _message.messageRoot = record.messageRoot ? Object.assign(Object.assign({}, record.messageRoot), { content: Crypto.decryptAESorFail(record.messageRoot.content, this.findKeyPairById(record.conversationId)), reactions: record.messageRoot.reactions ? record.messageRoot.reactions.map((reaction) => { return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) }); }) : null, createdAt: new Date(record.messageRoot.createdAt), updatedAt: record.messageRoot.updatedAt ? new Date(record.messageRoot.updatedAt) : null, deletedAt: record.messageRoot.deletedAt ? new Date(record.messageRoot.deletedAt) : null }) : null; this._emit("messageUpdatedLDB", _message); }; this._hookConversationCreatingFn = (primaryKey, record) => { const _conversation = Object.assign(Object.assign({}, record), { name: Crypto.decryptAESorFail(record.name, this.findKeyPairById(record.id)), description: Crypto.decryptAESorFail(record.description, this.findKeyPairById(record.id)), imageURL: Crypto.decryptAESorFail(record.imageURL, this.findKeyPairById(record.id)), bannerImageURL: Crypto.decryptAESorFail(record.bannerImageURL, this.findKeyPairById(record.id)) }); this._emit("conversationCreatedLDB", _conversation); }; this._hookConversationUpdatingFn = (modifications, primaryKey, record) => { const _conversation = Object.assign(Object.assign({}, record), { name: modifications.name ? Crypto.decryptAESorFail(modifications.name, this.findKeyPairById(record.id)) : Crypto.decryptAESorFail(record.name, this.findKeyPairById(record.id)), description: modifications.description ? Crypto.decryptAESorFail(modifications.description, this.findKeyPairById(record.id)) : Crypto.decryptAESorFail(record.description, this.findKeyPairById(record.id)), imageURL: modifications.imageURL ? Crypto.decryptAESorFail(modifications.imageURL, this.findKeyPairById(record.id)) : Crypto.decryptAESorFail(record.imageURL, this.findKeyPairById(record.id)), bannerImageURL: modifications.bannerImageURL ? Crypto.decryptAESorFail(modifications.bannerImageURL, this.findKeyPairById(record.id)) : Crypto.decryptAESorFail(record.bannerImageURL, this.findKeyPairById(record.id)), lastMessageText: modifications.lastMessageText ? Crypto.decryptAESorFail(modifications.lastMessageText, this.findKeyPairById(record.id)) : record.lastMessageText ? Crypto.decryptAESorFail(record.lastMessageText, this.findKeyPairById(record.id)) : null, lastMessageSentAt: modifications.lastMessageSentAt ? modifications.lastMessageSentAt : record.lastMessageSentAt ? record.lastMessageSentAt : null, lastMessageAuthor: modifications.lastMessageAuthor ? modifications.lastMessageAuthor : record.lastMessageAuthor ? record.lastMessageAuthor : null, lastMessageAuthorId: modifications.lastMessageAuthorId ? modifications.lastMessageAuthorId : record.lastMessageAuthorId ? record.lastMessageAuthorId : null, lastMessageSentId: modifications.lastMessageSentId ? modifications.lastMessageSentId : record.lastMessageSentId ? record.lastMessageSentId : null, lastMessageSentOrder: modifications.lastMessageSentOrder ? modifications.lastMessageSentOrder : record.lastMessageSentOrder ? record.lastMessageSentOrder : null, lastMessageReadId: modifications.lastMessageReadId ? modifications.lastMessageReadId : record.lastMessageReadId ? record.lastMessageReadId : null, lastMessageReadOrder: modifications.lastMessageReadOrder ? modifications.lastMessageReadOrder : record.lastMessageReadOrder ? record.lastMessageReadOrder : null, messagesToRead: modifications.messagesToRead ? modifications.messagesToRead : record.messagesToRead, hasLastMessageSentAt: modifications.lastMessageSentAt ? true : record.hasLastMessageSentAt }); this._emit("conversationUpdatedLDB", _conversation); }; } _emit(event, args) { const index = this._eventsCallbacks.findIndex((item) => { return item.event === event; }); if (index > -1) this, this._eventsCallbacks[index].callbacks.forEach((callback) => { callback(args); }); } /** syncing data with backend*/ _recoverUserConversations(type) { return __awaiter(this, void 0, void 0, function* () { try { let AUCfirstSet = yield this.listAllActiveUserConversationIds({ type, }, true); if (AUCfirstSet instanceof QIError) { const error = this._handleUnauthorizedQIError(AUCfirstSet); if (error) throw "_401_"; throw new Error(JSON.stringify(AUCfirstSet)); } let { nextToken, items } = AUCfirstSet; let activeIds = [...items]; while (nextToken) { const set = yield this.listAllActiveUserConversationIds({ type, nextToken, }, true); if (set instanceof QIError) { const error = this._handleUnauthorizedQIError(set); if (error) throw "_401_"; break; } const { nextToken: token, items } = set; activeIds = [...activeIds, ...items]; if (token) nextToken = token; else break; } let conversationfirstSet = yield this.listConversationsByIds(activeIds, true); if (conversationfirstSet instanceof QIError) { const error = this._handleUnauthorizedQIError(conversationfirstSet); if (error) throw "_401_"; throw new Error(JSON.stringify(conversationfirstSet)); } let { unprocessedKeys, items: conversations } = conversationfirstSet; let conversationsItems = [...conversations]; while (unprocessedKeys) { const set = yield this.listConversationsByIds(unprocessedKeys, true); if (set instanceof QIError) { const error = this._handleUnauthorizedQIError(set); if (error) throw "_401_"; break; } const { unprocessedKeys: ids, items } = set; conversationsItems = [...conversationsItems, ...items]; if (ids) unprocessedKeys = ids; else break; } const currentUser = yield this.getCurrentUser(true); if (currentUser instanceof QIError) { const error = this._handleUnauthorizedQIError(currentUser); if (error) throw "_401_"; throw new Error(JSON.stringify(currentUser)); } //stores/update the conversations into the local db if (conversationsItems.length > 0) { const conversationsStored = yield new Promise((resolve, reject) => { Serpens.addAction(() => { this._storage.conversation .where("indexDid") .equals(Auth.account.did) .toArray() .then(resolve) .catch(reject); }); }); yield this._storage.insertBulkSafe("conversation", conversationsItems.map((conversation) => { let conversationStored = conversationsStored.find((item) => { return item.id === conversation.id; }); let isConversationArchived = false; if (currentUser.archivedConversations) { const index = currentUser.archivedConversations.findIndex((id) => { return id === conversation.id; }); if (index > -1) isConversationArchived = true; } return Converter.fromConversationToLocalDBConversation(conversation, Auth.account.did, Auth.account.organizationId, conversation.ownerId, isConversationArchived, conversationStored ? conversationStored.lastMessageAuthor : null, conversationStored ? conversationStored.lastMessageAuthorId : null, conversationStored ? conversationStored.lastMessageText : null, conversationStored ? conversationStored.lastMessageSentId : null, conversationStored ? conversationStored.lastMessageSentOrder : null, conversationStored ? conversationStored.messagesToRead : 0, conversationStored ? conversationStored.lastMessageReadOrder : null, conversationStored ? conversationStored.lastMessageReadId : null); })); } return conversationsItems; } catch (error) { if (typeof error === "string" && error === "_401_") { yield Auth.fetchAuthToken(); this.silentReset(); return "_401_"; } console.log("[ERROR]: recoverUserConversations() -> ", error); } return null; }); } _recoverKeysFromConversations() { return __awaiter(this, void 0, void 0, function* () { try { let firstConversationMemberSet = yield this.listConversationMemberByUserId(undefined, true); if (firstConversationMemberSet instanceof QIError) { const error = this._handleUnauthorizedQIError(firstConversationMemberSet); if (error) throw "_401_"; throw new Error(JSON.stringify(firstConversationMemberSet)); } let { nextToken, items } = firstConversationMemberSet; let conversationMemberItems = [...items]; while (nextToken) { const set = yield this.listConversationMemberByUserId(nextToken, true); if (set instanceof QIError) { const error = this._handleUnauthorizedQIError(set); if (error) throw "_401_"; break; } const { nextToken: token, items } = set; conversationMemberItems = [...conversationMemberItems, ...items]; if (token) nextToken = token; else break; } Chat._config && Chat._config.devMode && console.log("user key pair ", this.getUserKeyPair()); //now, from the private key of the user, we will decrypt all the information about the conversation member. //we will store these decrypted pairs public keys/private keys into the _keyPairsMap array. const _keyPairsMap = []; let isError = false; for (const conversationMember of conversationMemberItems) { const { encryptedConversationIVKey, encryptedConversationAESKey } = conversationMember; const iv = Crypto.decryptStringOrFail(this.getUserKeyPair().privateKey, encryptedConversationIVKey); const AES = Crypto.decryptStringOrFail(this.getUserKeyPair().privateKey, encryptedConversationAESKey); _keyPairsMap.push({ id: conversationMember.conversationId, AES, iv, }); } if (isError) throw new Error("Failed to convert a public/private key pair."); this.setKeyPairMap(_keyPairsMap); Chat._config && Chat._config.devMode && console.log("user key pair ", this.getUserKeyPair()); Chat._config && Chat._config.devMode && console.log("key pair map is ", _keyPairsMap); return true; } catch (error) { console.log("[ERROR]: recoverKeysFromConversations() -> ", error); if (typeof error === "string" && error === "_401_") { yield Auth.fetchAuthToken(); this.silentReset(); return "_401_"; } } return false; }); } _recoverMessagesFromConversations(conversations) { return __awaiter(this, void 0, void 0, function* () { try { for (const conversation of conversations) { const { id, lastMessageSentAt } = conversation; //if the conversation hasn't any message it's useless to download the messages. if (!lastMessageSentAt) continue; //let's see if the last message sent into the conversation is more recent than the last message stored in the database //messages important handling const messagesImportantFirstSet = yield this.listMessagesImportantByUserConversationId({ conversationId: id, }, true); if (messagesImportantFirstSet instanceof QIError) { const error = this._handleUnauthorizedQIError(messagesImportantFirstSet); if (error) throw "_401_"; throw new Error(JSON.stringify(messagesImportantFirstSet)); } let { nextToken, items } = messagesImportantFirstSet; let messagesImportant = [...items]; while (nextToken) { const set = yield this.listMessagesImportantByUserConversationId({ conversationId: id, nextToken, }, true); if (set instanceof QIError) { const error = this._handleUnauthorizedQIError(set); if (error) throw "_401_"; break; } const { nextToken: token, items } = set; messagesImportant = [...messagesImportant, ...items]; if (token) nextToken = token; else break; } //messages handling let lastMessageStored = yield new Promise((resolve, reject) => { Serpens.addAction(() => { this._storage.message .where("conversationId") .equals(id) .filter((element) => element.origin === "USER" && element.userDid === Auth.account.did) .toArray() .then((array) => { resolve(array.length > 0 ? array.sort((a, b) => { return (new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); })[0] : undefined); }) .catch(reject); }); }); const canDownloadMessages = !lastMessageStored || (lastMessageStored && new Date(lastMessageStored.createdAt).getTime() < new Date(lastMessageSentAt).getTime()); //the check of history message is already done on backend side if (canDownloadMessages) { const messagesFirstSet = yield this.listMessagesByConversationId({ id, }, true); if (messagesFirstSet instanceof QIError) { const error = this._handleUnauthorizedQIError(messagesFirstSet); if (error) throw "_401_"; throw new Error(JSON.stringify(messagesFirstSet)); } let { nextToken, items } = messagesFirstSet; let messages = [...items]; while (nextToken) { const set = yield this.listMessagesByConversationId({ id, nextToken, }, true); if (set instanceof QIError) { const error = this._handleUnauthorizedQIError(set); if (error) throw "_401_"; break; } const { nextToken: token, items } = set; messages = [...messages, ...items]; if (token) nextToken = token; else break; } //let's store the messages without create duplicates if (messages.length > 0) { //it's possible this array is empty when the chat history settings has value 'false' this._storage.insertBulkSafe("message", messages.map((message) => { const isMessageImportant = messagesImportant.findIndex((important) => { return important.messageId === message.id; }) > -1; return Converter.fromMessageToLocalDBMessage(message, Auth.account.did, Auth.account.organizationId, isMessageImportant, "USER"); })); yield new Promise((resolve, reject) => { Serpens.addAction(() => this._storage.conversation .where("[id+userDid]") .equals([messages[0].conversationId, Auth.account.did]) .modify((conversation) => { const lastMessage = messages[0]; //messages are ordered from the most recent to the less recent message, so the first item is the last message sent conversation.lastMessageAuthor = lastMessage.user.username; conversation.lastMessageAuthorId = lastMessage.user.id; conversation.lastMessageText = lastMessage.content; conversation.lastMessageSentAt = lastMessage.createdAt; conversation.lastMessageSentId = lastMessage.id; conversation.lastMessageSentOrder = lastMessage.order; conversation.messagesToRead = conversation.messagesToRead + messages.filter((message) => { return message.user.id !== Auth.account.dynamoDBUserID; }).length; }) .then(resolve) .catch(reject)); }); //let's collect these messages for our detective message instance (only if this sync is not the first) /*if (this._syncingCounter > 0) messages.forEach((message) => { Chat._detectiveMessage.collectClue( message, Auth.account!.did, Auth.account!.organizationId ) })*/ } } //now we check if we have some messages that were edited (adding/removing a reaction or editing the text) //first, we get the last sync date from the current user let user = (yield this._storage.get("user", "[did+organizationId]", [ Auth.account.did, Auth.account.organizationId, ])); //we do a check only if lastSyncAt is !== null if (user.lastSyncAt) { const { lastSyncAt } = user; const messagesUpdatedFirstSet = yield this.listMessagesUpdated({ conversationId: id, greaterThanDate: lastSyncAt, }, true); if (messagesUpdatedFirstSet instanceof QIError) { const error = this._handleUnauthorizedQIError(messagesUpdatedFirstSet); if (error) throw "_401_"; throw new Error(JSON.stringify(messagesUpdatedFirstSet)); } let { nextToken, items } = messagesUpdatedFirstSet; let messagesUpdated = [...items]; while (nextToken) { const set = yield this.listMessagesUpdated({ conversationId: id, greaterThanDate: lastSyncAt, nextToken, }, true); if (set instanceof QIError) { const error = this._handleUnauthorizedQIError(set); if (error) throw "_401_"; break; } const { nextToken: token, items } = set; messagesUpdated = [...messagesUpdated, ...items]; if (token) nextToken = token; else break; } //let's update the messages in the local table if (messagesUpdated.length > 0) { //it's possible this array is empty when the chat history settings has value 'false' this._storage.insertBulkSafe("message", messagesUpdated.map((message) => { const isMessageImportant = messagesImportant.findIndex((important) => { return important.messageId === message.id; }) > -1; return Converter.fromMessageToLocalDBMessage(message, Auth.account.did, Auth.account.organizationId, isMessageImportant, "USER"); })); } } } return true; } catch (error) { console.log("[ERROR]: recoverMessagesFromConversations() -> ", error); if (typeof error === "string" && error === "_401_") { yield Auth.fetchAuthToken(); this.silentReset(); return "_401_"; } } return false; }); } _sync(syncingCounter) { return __awaiter(this, void 0, void 0, function* () { this._isSyncing = true; this._emit("syncing", this._syncingCounter); //first operation. Recover the list of the conversations in which the user is a member. //unactive conversations are the convos in which the user left the group or has been ejected const activeConversations = yield this._recoverUserConversations(ActiveUserConversationType.Active); const unactiveConversations = yield this._recoverUserConversations(ActiveUserConversationType.Canceled); Chat._config && Chat._config.devMode && console.log("activeConversations", activeConversations); Chat._config && Chat._config.devMode && console.log("unactiveConversations", unactiveConversations); if (!activeConversations || !unactiveConversations) { this._emit("syncError", { error: `error during conversation syncing.` }); this.unsync(); return; } //_sync(..) is called internally by silentReset() if (activeConversations === "_401_" || unactiveConversations === "_401_") return; Chat._config && Chat._config.devMode && console.log("after check if (!activeConversations || !unactiveConversations)"); //second operation. Recover the list of conversation member objects, in order to retrieve the public & private keys of all conversations. const keysRecovered = yield this._recoverKeysFromConversations(); Chat._config && Chat._config.devMode && console.log("keysRecovered", keysRecovered); if (typeof keysRecovered === "boolean" && !keysRecovered) { this._emit("syncError", { error: `error during recovering of the keys from conversations.`, }); this.unsync(); return; } //_sync(..) is called internally by silentReset() if (keysRecovered === "_401_") return; Chat._config && Chat._config.devMode && console.log("after check if (!keysRecovered)"); //third operation. For each conversation, we need to download the messages if the lastMessageSentAt of the conversation is != null //and the date of the last message stored in the local db is less recent than the lastMessageSentAt date. const messagesRecovered = yield this._recoverMessagesFromConversations([ ...activeConversations, ...unactiveConversations, ]); Chat._config && Chat._config.devMode && console.log("messagesRecovered", messagesRecovered); if (!messagesRecovered) { this._emit("syncError", { error: `error during recovering of the messages from conversations.`, }); this.unsync(); return; } //_sync(..) is called internally by silentReset() if (messagesRecovered === "_401_") return; Chat._config && Chat._config.devMode && console.log("after check if (!messagesRecovered)"); //let's setup an array of the conversations in the first sync cycle. //This will allow to map the conversations in every single cycle that comes after the first one. if (syncingCounter === 0) { Chat._config && Chat._config.devMode && console.log("inside check if (syncingCounter === 0)"); for (const activeConversation of activeConversations) this._conversationsMap.push({ type: "ACTIVE", conversationId: activeConversation.id, conversation: activeConversation, }); for (const unactiveConversation of unactiveConversations) this._conversationsMap.push({ type: "CANCELED", conversationId: unactiveConversation.id, conversation: unactiveConversation, }); Chat._config && Chat._config.devMode && console.log("inside check if (syncingCounter === 0) this._conversationsMap is ", this._conversationsMap); } else { //this situation happens when a subscription between onAddMembersToConversation, onEjectMember, onLeaveConversation doesn't fire properly. //here we can check if there are differences between the previous sync and the current one //theoretically since we have subscriptions, we should be in a situation in which we don't have any difference //since the subscription role is to keep the array _conversationsMap synchronized. //But it can be also the opposite. So inside this block we will check if there are conversations that need //subscriptions to be added or the opposite (so subscriptions that need to be removed) Chat._config && Chat._config.devMode && console.log("inside else syncingCounter is > 0, now its value is ", this._syncingCounter); const conversations = [...activeConversations, ...unactiveConversations]; const flatConversationMap = this._conversationsMap.map((item) => item.conversation); const { added, removed } = findAddedAndRemovedConversation(flatConversationMap, conversations); Chat._config && Chat._config.devMode && console.log("added and removed are ", added, removed); if (added.length > 0) for (const conversation of added) { const conversationAdded = this._conversationsMap.find((item) => { return item.conversationId === conversation.id; }); if (conversationAdded) { conversationAdded.type = "ACTIVE"; } else { this._conversationsMap.push({ type: "ACTIVE", conversationId: conversation.id, conversation, }); } this._emit("conversationNewMembers", { conversation, conversationId: conversation.id, }); } if (removed.length > 0) { for (const conversation of removed) { const conversationRemoved = this._conversationsMap.find((item) => { return item.conversationId === conversation.id; }); if (conversationRemoved) conversationRemoved.type = "CANCELED"; } } } //we add the internal events for the local database if (!this._hookMessageCreated) this._onMessageCreatedLDB(); if (!this._hookMessageUpdated) this._onMessageUpdatedLDB(); if (!this._hookMessageDeleted) this._onMessageDeletedLDB(); if (!this._hookConversationCreated) this._onConversationCreatedLDB(); if (!this._hookConversationUpdated) this._onConversationUpdatedLDB(); this._isSyncing = false; Chat._config && Chat._config.devMode && console.log("ready to emit sync or syncUpdate, this._syncingCounter is ", this._syncingCounter); syncingCounter === 0 ? this._emit("sync") : this._emit("syncUpdate", this._syncingCounter); this._syncingCounter++; Chat._config && Chat._config.devMode && console.log("let's update the last sync date..."); let user = (yield this._storage.get("user", "[did+organizationId]", [ Auth.account.did, Auth.account.organizationId, ])); yield new Promise((resolve, reject) => { Serpens.addAction(() => { this._storage.user .update(user, { lastSyncAt: new Date(), }) .then(resolve) .catch(reject); }); }); Chat._config && Chat._config.devMode && console.log("calling another _sync()"); if (this._syncTimeout) clearTimeout(this._syncTimeout); this._syncTimeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () { yield this._sync(this._syncingCounter); }), this.SYNCING_TIME_MS); }); } _onAddMembersToConversationSync(response, source, uuid) { return __awaiter(this, void 0, void 0, function* () { let operation = null; if (response instanceof QIError) { this._emit("addMembersToConversationSubscriptionError", response); return; } try { const keypairMap = this.getKeyPairMap(); const alreadyMember = keypairMap.find((item) => { return item.id === response.conversationId; }); operation = alreadyMember ? "add_new_members" : "create_conversation"; if (!alreadyMember) { //we need to update the _keyPairsMap with the new keys of the new conversation const { conversationId, items } = response; const item = items.find((item) => { var _a; return item.userId === ((_a = Auth.account) === null || _a === void 0 ? void 0 : _a.dynamoDBUserID); }); const { encryptedConversationIVKey, encryptedConversationAESKey } = item; //these pair is encrypted with the public key of the current user, so we need to decrypt them const conversationIVKey = Crypto.decryptStringOrFail(this._userKeyPair.privateKey, encryptedConversationIVKey); const conversationAESKey = Crypto.decryptStringOrFail(this._userKeyPair.privateKey, encryptedConversationAESKey); //this add a key pair only if it doesn't exist. if it does, then internally skip this operation this.addKeyPairItem({ id: conversationId, AES: conversationAESKey, iv: conversationIVKey, }); //we update also the _unsubscribeSyncSet array using the uuid emitted by the subscription //in order to map the unsubscribe function with the conversation const index = this._unsubscribeSyncSet.findIndex((item) => { return item.u