You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			844 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			844 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
// eslint:disable: no-require-imports no-var-requires one-variable-per-declaration no-void-expression function-name
 | 
						|
 | 
						|
import _ from 'lodash';
 | 
						|
import { MessageResultProps } from '../components/search/MessageSearchResults';
 | 
						|
import { ConversationModel } from '../models/conversation';
 | 
						|
import { ConversationAttributes } from '../models/conversationAttributes';
 | 
						|
import { MessageCollection, MessageModel } from '../models/message';
 | 
						|
import { MessageAttributes, MessageDirection } from '../models/messageType';
 | 
						|
import { StorageItem } from '../node/storage_item';
 | 
						|
import { HexKeyPair } from '../receiver/keypairs';
 | 
						|
import { Quote } from '../receiver/types';
 | 
						|
import { getSodiumRenderer } from '../session/crypto';
 | 
						|
import { PubKey } from '../session/types';
 | 
						|
import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String';
 | 
						|
import {
 | 
						|
  AsyncWrapper,
 | 
						|
  MsgDuplicateSearchOpenGroup,
 | 
						|
  SaveConversationReturn,
 | 
						|
  UnprocessedDataNode,
 | 
						|
  UpdateLastHashType,
 | 
						|
} from '../types/sqlSharedTypes';
 | 
						|
import { ExpirationTimerOptions } from '../util/expiringMessages';
 | 
						|
import { Storage } from '../util/storage';
 | 
						|
import { channels } from './channels';
 | 
						|
import * as dataInit from './dataInit';
 | 
						|
import { cleanData } from './dataUtils';
 | 
						|
import { SNODE_POOL_ITEM_ID } from './settings-key';
 | 
						|
 | 
						|
const ERASE_SQL_KEY = 'erase-sql-key';
 | 
						|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
 | 
						|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
 | 
						|
 | 
						|
export type IdentityKey = {
 | 
						|
  id: string;
 | 
						|
  publicKey: ArrayBuffer;
 | 
						|
  firstUse: boolean;
 | 
						|
  nonblockingApproval: boolean;
 | 
						|
  secretKey?: string; // found in medium groups
 | 
						|
};
 | 
						|
 | 
						|
export type GuardNode = {
 | 
						|
  ed25519PubKey: string;
 | 
						|
};
 | 
						|
 | 
						|
export interface Snode {
 | 
						|
  ip: string;
 | 
						|
  port: number;
 | 
						|
  pubkey_x25519: string;
 | 
						|
  pubkey_ed25519: string;
 | 
						|
}
 | 
						|
 | 
						|
export type SwarmNode = Snode & {
 | 
						|
  address: string;
 | 
						|
};
 | 
						|
 | 
						|
// Basic
 | 
						|
async function shutdown(): Promise<void> {
 | 
						|
  // Stop accepting new SQL jobs, flush outstanding queue
 | 
						|
  await dataInit.shutdown();
 | 
						|
  await close();
 | 
						|
}
 | 
						|
// Note: will need to restart the app after calling this, to set up afresh
 | 
						|
async function close(): Promise<void> {
 | 
						|
  await channels.close();
 | 
						|
}
 | 
						|
 | 
						|
// Note: will need to restart the app after calling this, to set up afresh
 | 
						|
async function removeDB(): Promise<void> {
 | 
						|
  await channels.removeDB();
 | 
						|
}
 | 
						|
 | 
						|
// Password hash
 | 
						|
 | 
						|
async function getPasswordHash(): Promise<string | null> {
 | 
						|
  return channels.getPasswordHash();
 | 
						|
}
 | 
						|
 | 
						|
// Guard Nodes
 | 
						|
async function getGuardNodes(): Promise<Array<GuardNode>> {
 | 
						|
  return channels.getGuardNodes();
 | 
						|
}
 | 
						|
async function updateGuardNodes(nodes: Array<string>): Promise<void> {
 | 
						|
  return channels.updateGuardNodes(nodes);
 | 
						|
}
 | 
						|
 | 
						|
async function generateAttachmentKeyIfEmpty() {
 | 
						|
  const existingKey = await getItemById('local_attachment_encrypted_key');
 | 
						|
  if (!existingKey) {
 | 
						|
    const sodium = await getSodiumRenderer();
 | 
						|
    const encryptingKey = sodium.to_hex(sodium.randombytes_buf(32));
 | 
						|
    await createOrUpdateItem({
 | 
						|
      id: 'local_attachment_encrypted_key',
 | 
						|
      value: encryptingKey,
 | 
						|
    });
 | 
						|
    // be sure to write the new key to the cache. so we can access it straight away
 | 
						|
    await Storage.put('local_attachment_encrypted_key', encryptingKey);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// Swarm nodes
 | 
						|
async function getSwarmNodesForPubkey(pubkey: string): Promise<Array<string>> {
 | 
						|
  return channels.getSwarmNodesForPubkey(pubkey);
 | 
						|
}
 | 
						|
 | 
						|
async function updateSwarmNodesForPubkey(
 | 
						|
  pubkey: string,
 | 
						|
  snodeEdKeys: Array<string>
 | 
						|
): Promise<void> {
 | 
						|
  await channels.updateSwarmNodesForPubkey(pubkey, snodeEdKeys);
 | 
						|
}
 | 
						|
 | 
						|
// Closed group
 | 
						|
 | 
						|
/**
 | 
						|
 * The returned array is ordered based on the timestamp, the latest is at the end.
 | 
						|
 */
 | 
						|
async function getAllEncryptionKeyPairsForGroup(
 | 
						|
  groupPublicKey: string | PubKey
 | 
						|
): Promise<Array<HexKeyPair> | undefined> {
 | 
						|
  const pubkey = (groupPublicKey as PubKey).key || (groupPublicKey as string);
 | 
						|
  return channels.getAllEncryptionKeyPairsForGroup(pubkey);
 | 
						|
}
 | 
						|
 | 
						|
async function getLatestClosedGroupEncryptionKeyPair(
 | 
						|
  groupPublicKey: string
 | 
						|
): Promise<HexKeyPair | undefined> {
 | 
						|
  return channels.getLatestClosedGroupEncryptionKeyPair(groupPublicKey);
 | 
						|
}
 | 
						|
 | 
						|
async function addClosedGroupEncryptionKeyPair(
 | 
						|
  groupPublicKey: string,
 | 
						|
  keypair: HexKeyPair
 | 
						|
): Promise<void> {
 | 
						|
  await channels.addClosedGroupEncryptionKeyPair(groupPublicKey, keypair);
 | 
						|
}
 | 
						|
 | 
						|
async function removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: string): Promise<void> {
 | 
						|
  return channels.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey);
 | 
						|
}
 | 
						|
 | 
						|
// Conversation
 | 
						|
async function saveConversation(data: ConversationAttributes): Promise<SaveConversationReturn> {
 | 
						|
  const cleaned = cleanData(data) as ConversationAttributes;
 | 
						|
  /**
 | 
						|
   * Merging two conversations in `handleMessageRequestResponse` introduced a bug where we would mark conversation active_at to be -Infinity.
 | 
						|
   * The root issue has been fixed, but just to make sure those INVALID DATE does not show up, update those -Infinity active_at conversations to be now(), once.,
 | 
						|
   */
 | 
						|
  if (cleaned.active_at === -Infinity) {
 | 
						|
    cleaned.active_at = Date.now();
 | 
						|
  }
 | 
						|
 | 
						|
  return channels.saveConversation(cleaned);
 | 
						|
}
 | 
						|
 | 
						|
async function fetchConvoMemoryDetails(convoId: string): Promise<SaveConversationReturn> {
 | 
						|
  return channels.fetchConvoMemoryDetails(convoId);
 | 
						|
}
 | 
						|
 | 
						|
async function getConversationById(id: string): Promise<ConversationModel | undefined> {
 | 
						|
  const data = await channels.getConversationById(id);
 | 
						|
  if (data) {
 | 
						|
    return new ConversationModel(data);
 | 
						|
  }
 | 
						|
  return undefined;
 | 
						|
}
 | 
						|
 | 
						|
async function removeConversation(id: string): Promise<void> {
 | 
						|
  const existing = await getConversationById(id);
 | 
						|
 | 
						|
  // Note: It's important to have a fully database-hydrated model to delete here because
 | 
						|
  //   it needs to delete all associated on-disk files along with the database delete.
 | 
						|
  if (existing) {
 | 
						|
    await channels.removeConversation(id);
 | 
						|
    await existing.cleanup();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
async function getAllConversations(): Promise<Array<ConversationModel>> {
 | 
						|
  const conversationsAttrs = (await channels.getAllConversations()) as Array<
 | 
						|
    ConversationAttributes
 | 
						|
  >;
 | 
						|
 | 
						|
  return conversationsAttrs.map(attr => new ConversationModel(attr));
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This returns at most MAX_PUBKEYS_MEMBERS members, the last MAX_PUBKEYS_MEMBERS members who wrote in the chat
 | 
						|
 */
 | 
						|
async function getPubkeysInPublicConversation(id: string): Promise<Array<string>> {
 | 
						|
  return channels.getPubkeysInPublicConversation(id);
 | 
						|
}
 | 
						|
 | 
						|
async function searchConversations(query: string): Promise<Array<any>> {
 | 
						|
  const conversations = await channels.searchConversations(query);
 | 
						|
  return conversations;
 | 
						|
}
 | 
						|
 | 
						|
async function searchMessages(query: string, limit: number): Promise<Array<MessageResultProps>> {
 | 
						|
  const messages = (await channels.searchMessages(query, limit)) as Array<MessageResultProps>;
 | 
						|
  return _.uniqWith(messages, (left: { id: string }, right: { id: string }) => {
 | 
						|
    return left.id === right.id;
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Returns just json objects not MessageModel
 | 
						|
 */
 | 
						|
async function searchMessagesInConversation(
 | 
						|
  query: string,
 | 
						|
  conversationId: string,
 | 
						|
  limit: number
 | 
						|
): Promise<Array<MessageAttributes>> {
 | 
						|
  const messages = (await channels.searchMessagesInConversation(
 | 
						|
    query,
 | 
						|
    conversationId,
 | 
						|
    limit
 | 
						|
  )) as Array<MessageAttributes>;
 | 
						|
  return messages;
 | 
						|
}
 | 
						|
 | 
						|
// Message
 | 
						|
 | 
						|
async function cleanSeenMessages(): Promise<void> {
 | 
						|
  await channels.cleanSeenMessages();
 | 
						|
}
 | 
						|
 | 
						|
async function cleanLastHashes(): Promise<void> {
 | 
						|
  await channels.cleanLastHashes();
 | 
						|
}
 | 
						|
 | 
						|
async function saveSeenMessageHashes(
 | 
						|
  data: Array<{
 | 
						|
    expiresAt: number;
 | 
						|
    hash: string;
 | 
						|
  }>
 | 
						|
): Promise<void> {
 | 
						|
  await channels.saveSeenMessageHashes(cleanData(data));
 | 
						|
}
 | 
						|
 | 
						|
async function updateLastHash(data: UpdateLastHashType): Promise<void> {
 | 
						|
  await channels.updateLastHash(cleanData(data));
 | 
						|
}
 | 
						|
 | 
						|
async function saveMessage(data: MessageAttributes): Promise<string> {
 | 
						|
  const cleanedData = cleanData(data);
 | 
						|
  const id = await channels.saveMessage(cleanedData);
 | 
						|
  ExpirationTimerOptions.updateExpiringMessagesCheck();
 | 
						|
  return id;
 | 
						|
}
 | 
						|
 | 
						|
async function saveMessages(arrayOfMessages: Array<MessageAttributes>): Promise<void> {
 | 
						|
  await channels.saveMessages(cleanData(arrayOfMessages));
 | 
						|
}
 | 
						|
 | 
						|
async function removeMessage(id: string): Promise<void> {
 | 
						|
  const message = await getMessageById(id, true);
 | 
						|
 | 
						|
  // Note: It's important to have a fully database-hydrated model to delete here because
 | 
						|
  //   it needs to delete all associated on-disk files along with the database delete.
 | 
						|
  if (message) {
 | 
						|
    await channels.removeMessage(id);
 | 
						|
    await message.cleanup();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Note: this method will not clean up external files, just delete from SQL.
 | 
						|
 * Files are cleaned up on app start if they are not linked to any messages
 | 
						|
 *
 | 
						|
 */
 | 
						|
async function removeMessagesByIds(ids: Array<string>): Promise<void> {
 | 
						|
  await channels.removeMessagesByIds(ids);
 | 
						|
}
 | 
						|
 | 
						|
async function getMessageIdsFromServerIds(
 | 
						|
  serverIds: Array<string> | Array<number>,
 | 
						|
  conversationId: string
 | 
						|
): Promise<Array<string> | undefined> {
 | 
						|
  return channels.getMessageIdsFromServerIds(serverIds, conversationId);
 | 
						|
}
 | 
						|
 | 
						|
async function getMessageById(
 | 
						|
  id: string,
 | 
						|
  skipTimerInit: boolean = false
 | 
						|
): Promise<MessageModel | null> {
 | 
						|
  const message = await channels.getMessageById(id);
 | 
						|
  if (!message) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
  if (skipTimerInit) {
 | 
						|
    message.skipTimerInit = skipTimerInit;
 | 
						|
  }
 | 
						|
 | 
						|
  return new MessageModel(message);
 | 
						|
}
 | 
						|
 | 
						|
async function getMessageByServerId(
 | 
						|
  conversationId: string,
 | 
						|
  serverId: number,
 | 
						|
  skipTimerInit: boolean = false
 | 
						|
): Promise<MessageModel | null> {
 | 
						|
  const message = await channels.getMessageByServerId(conversationId, serverId);
 | 
						|
  if (!message) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
  if (skipTimerInit) {
 | 
						|
    message.skipTimerInit = skipTimerInit;
 | 
						|
  }
 | 
						|
 | 
						|
  return new MessageModel(message);
 | 
						|
}
 | 
						|
 | 
						|
async function filterAlreadyFetchedOpengroupMessage(
 | 
						|
  msgDetails: MsgDuplicateSearchOpenGroup
 | 
						|
): Promise<MsgDuplicateSearchOpenGroup> {
 | 
						|
  const msgDetailsNotAlreadyThere = await channels.filterAlreadyFetchedOpengroupMessage(msgDetails);
 | 
						|
  return msgDetailsNotAlreadyThere || [];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Fetch all messages that match the sender pubkey and sent_at timestamp
 | 
						|
 * @param propsList An array of objects containing a source (the sender id) and timestamp of the message - not to be confused with the serverTimestamp. This is equivalent to sent_at
 | 
						|
 * @returns the fetched messageModels
 | 
						|
 */
 | 
						|
async function getMessagesBySenderAndSentAt(
 | 
						|
  propsList: Array<{
 | 
						|
    source: string;
 | 
						|
    timestamp: number;
 | 
						|
  }>
 | 
						|
): Promise<MessageCollection | null> {
 | 
						|
  const messages = await channels.getMessagesBySenderAndSentAt(propsList);
 | 
						|
 | 
						|
  if (!messages || !messages.length) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
async function getUnreadByConversation(
 | 
						|
  conversationId: string,
 | 
						|
  sentBeforeTimestamp: number
 | 
						|
): Promise<MessageCollection> {
 | 
						|
  const messages = await channels.getUnreadByConversation(conversationId, sentBeforeTimestamp);
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
async function markAllAsReadByConversationNoExpiration(
 | 
						|
  conversationId: string,
 | 
						|
  returnMessagesUpdated: boolean // for performance reason we do not return them because usually they are not needed
 | 
						|
): Promise<Array<number>> {
 | 
						|
  const messagesIds = await channels.markAllAsReadByConversationNoExpiration(
 | 
						|
    conversationId,
 | 
						|
    returnMessagesUpdated
 | 
						|
  );
 | 
						|
  return messagesIds;
 | 
						|
}
 | 
						|
 | 
						|
// might throw
 | 
						|
async function getUnreadCountByConversation(conversationId: string): Promise<number> {
 | 
						|
  return channels.getUnreadCountByConversation(conversationId);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Gets the count of messages for a direction
 | 
						|
 * @param conversationId Conversation for messages to retrieve from
 | 
						|
 * @param type outgoing/incoming
 | 
						|
 */
 | 
						|
async function getMessageCountByType(
 | 
						|
  conversationId: string,
 | 
						|
  type?: MessageDirection
 | 
						|
): Promise<number> {
 | 
						|
  return channels.getMessageCountByType(conversationId, type);
 | 
						|
}
 | 
						|
 | 
						|
async function getMessagesByConversation(
 | 
						|
  conversationId: string,
 | 
						|
  {
 | 
						|
    skipTimerInit = false,
 | 
						|
    returnQuotes = false,
 | 
						|
    messageId = null,
 | 
						|
  }: { skipTimerInit?: false; returnQuotes?: boolean; messageId: string | null }
 | 
						|
): Promise<{ messages: MessageCollection; quotes: Array<Quote> }> {
 | 
						|
  const { messages, quotes } = await channels.getMessagesByConversation(conversationId, {
 | 
						|
    messageId,
 | 
						|
    returnQuotes,
 | 
						|
  });
 | 
						|
 | 
						|
  if (skipTimerInit) {
 | 
						|
    // eslint-disable-next-line no-restricted-syntax
 | 
						|
    for (const message of messages) {
 | 
						|
      message.skipTimerInit = skipTimerInit;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return {
 | 
						|
    messages: new MessageCollection(messages),
 | 
						|
    quotes,
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This function should only be used when you don't want to render the messages.
 | 
						|
 * It just grabs the last messages of a conversation.
 | 
						|
 *
 | 
						|
 * To be used when you want for instance to remove messages from a conversations, in order.
 | 
						|
 * Or to trigger downloads of a attachments from a just approved contact (clicktotrustSender)
 | 
						|
 * @param conversationId the conversationId to fetch messages from
 | 
						|
 * @param limit the maximum number of messages to return
 | 
						|
 * @param skipTimerInit  see MessageModel.skipTimerInit
 | 
						|
 * @returns the fetched messageModels
 | 
						|
 */
 | 
						|
async function getLastMessagesByConversation(
 | 
						|
  conversationId: string,
 | 
						|
  limit: number,
 | 
						|
  skipTimerInit: boolean
 | 
						|
): Promise<MessageCollection> {
 | 
						|
  const messages = await channels.getLastMessagesByConversation(conversationId, limit);
 | 
						|
  if (skipTimerInit) {
 | 
						|
    // eslint-disable-next-line no-restricted-syntax
 | 
						|
    for (const message of messages) {
 | 
						|
      message.skipTimerInit = skipTimerInit;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
async function getLastMessageIdInConversation(conversationId: string) {
 | 
						|
  const collection = await getLastMessagesByConversation(conversationId, 1, true);
 | 
						|
  return collection.models.length ? collection.models[0].id : null;
 | 
						|
}
 | 
						|
 | 
						|
async function getLastMessageInConversation(conversationId: string) {
 | 
						|
  const messages = await channels.getLastMessagesByConversation(conversationId, 1);
 | 
						|
  // eslint-disable-next-line no-restricted-syntax
 | 
						|
  for (const message of messages) {
 | 
						|
    message.skipTimerInit = true;
 | 
						|
  }
 | 
						|
 | 
						|
  const collection = new MessageCollection(messages);
 | 
						|
  return collection.length ? collection.models[0] : null;
 | 
						|
}
 | 
						|
 | 
						|
async function getOldestMessageInConversation(conversationId: string) {
 | 
						|
  const messages = await channels.getOldestMessageInConversation(conversationId);
 | 
						|
  // eslint-disable-next-line no-restricted-syntax
 | 
						|
  for (const message of messages) {
 | 
						|
    message.skipTimerInit = true;
 | 
						|
  }
 | 
						|
 | 
						|
  const collection = new MessageCollection(messages);
 | 
						|
  return collection.length ? collection.models[0] : null;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @returns Returns count of all messages in the database
 | 
						|
 */
 | 
						|
async function getMessageCount() {
 | 
						|
  return channels.getMessageCount();
 | 
						|
}
 | 
						|
 | 
						|
async function getFirstUnreadMessageIdInConversation(
 | 
						|
  conversationId: string
 | 
						|
): Promise<string | undefined> {
 | 
						|
  return channels.getFirstUnreadMessageIdInConversation(conversationId);
 | 
						|
}
 | 
						|
 | 
						|
async function getFirstUnreadMessageWithMention(
 | 
						|
  conversationId: string
 | 
						|
): Promise<string | undefined> {
 | 
						|
  return channels.getFirstUnreadMessageWithMention(conversationId);
 | 
						|
}
 | 
						|
 | 
						|
async function hasConversationOutgoingMessage(conversationId: string): Promise<boolean> {
 | 
						|
  return channels.hasConversationOutgoingMessage(conversationId);
 | 
						|
}
 | 
						|
async function getLastHashBySnode(
 | 
						|
  convoId: string,
 | 
						|
  snode: string,
 | 
						|
  namespace: number
 | 
						|
): Promise<string> {
 | 
						|
  return channels.getLastHashBySnode(convoId, snode, namespace);
 | 
						|
}
 | 
						|
 | 
						|
async function getSeenMessagesByHashList(hashes: Array<string>): Promise<any> {
 | 
						|
  return channels.getSeenMessagesByHashList(hashes);
 | 
						|
}
 | 
						|
 | 
						|
async function removeAllMessagesInConversation(conversationId: string): Promise<void> {
 | 
						|
  const startFunction = Date.now();
 | 
						|
  let start = Date.now();
 | 
						|
 | 
						|
  let messages;
 | 
						|
  do {
 | 
						|
    // Yes, we really want the await in the loop. We're deleting 500 at a
 | 
						|
    //   time so we don't use too much memory.
 | 
						|
    // eslint-disable-next-line no-await-in-loop
 | 
						|
    messages = await getLastMessagesByConversation(conversationId, 1000, false);
 | 
						|
    if (!messages.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    window.log.info(
 | 
						|
      `removeAllMessagesInConversation getLastMessagesByConversation ${conversationId} ${
 | 
						|
        messages.length
 | 
						|
      } took ${Date.now() - start}ms`
 | 
						|
    );
 | 
						|
 | 
						|
    // Note: It's very important that these models are fully hydrated because
 | 
						|
    //   we need to delete all associated on-disk files along with the database delete.
 | 
						|
    const ids = messages.map(message => message.id);
 | 
						|
    start = Date.now();
 | 
						|
    for (let index = 0; index < messages.length; index++) {
 | 
						|
      const message = messages.at(index);
 | 
						|
      // eslint-disable-next-line no-await-in-loop
 | 
						|
      await message.cleanup();
 | 
						|
    }
 | 
						|
    window.log.info(
 | 
						|
      `removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${Date.now() -
 | 
						|
        start}ms`
 | 
						|
    );
 | 
						|
    start = Date.now();
 | 
						|
 | 
						|
    // eslint-disable-next-line no-await-in-loop
 | 
						|
    await channels.removeMessagesByIds(ids);
 | 
						|
    window.log.info(
 | 
						|
      `removeAllMessagesInConversation: removeMessagesByIds ${conversationId} took ${Date.now() -
 | 
						|
        start}ms`
 | 
						|
    );
 | 
						|
  } while (messages.length);
 | 
						|
 | 
						|
  await channels.removeAllMessagesInConversation(conversationId);
 | 
						|
  window.log.info(
 | 
						|
    `removeAllMessagesInConversation: complete time ${conversationId} took ${Date.now() -
 | 
						|
      startFunction}ms`
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
async function getMessagesBySentAt(sentAt: number): Promise<MessageCollection> {
 | 
						|
  const messages = await channels.getMessagesBySentAt(sentAt);
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
async function getExpiredMessages(): Promise<MessageCollection> {
 | 
						|
  const messages = await channels.getExpiredMessages();
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
async function getOutgoingWithoutExpiresAt(): Promise<MessageCollection> {
 | 
						|
  const messages = await channels.getOutgoingWithoutExpiresAt();
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
async function getNextExpiringMessage(): Promise<MessageCollection> {
 | 
						|
  const messages = await channels.getNextExpiringMessage();
 | 
						|
  return new MessageCollection(messages);
 | 
						|
}
 | 
						|
 | 
						|
// Unprocessed
 | 
						|
 | 
						|
const getUnprocessedCount: AsyncWrapper<UnprocessedDataNode['getUnprocessedCount']> = () => {
 | 
						|
  return channels.getUnprocessedCount();
 | 
						|
};
 | 
						|
 | 
						|
const getAllUnprocessed: AsyncWrapper<UnprocessedDataNode['getAllUnprocessed']> = () => {
 | 
						|
  return channels.getAllUnprocessed();
 | 
						|
};
 | 
						|
 | 
						|
const getUnprocessedById: AsyncWrapper<UnprocessedDataNode['getUnprocessedById']> = id => {
 | 
						|
  return channels.getUnprocessedById(id);
 | 
						|
};
 | 
						|
 | 
						|
const saveUnprocessed: AsyncWrapper<UnprocessedDataNode['saveUnprocessed']> = data => {
 | 
						|
  return channels.saveUnprocessed(cleanData(data));
 | 
						|
};
 | 
						|
 | 
						|
const updateUnprocessedAttempts: AsyncWrapper<UnprocessedDataNode['updateUnprocessedAttempts']> = (
 | 
						|
  id,
 | 
						|
  attempts
 | 
						|
) => {
 | 
						|
  return channels.updateUnprocessedAttempts(id, attempts);
 | 
						|
};
 | 
						|
const updateUnprocessedWithData: AsyncWrapper<UnprocessedDataNode['updateUnprocessedWithData']> = (
 | 
						|
  id,
 | 
						|
  data
 | 
						|
) => {
 | 
						|
  return channels.updateUnprocessedWithData(id, cleanData(data));
 | 
						|
};
 | 
						|
 | 
						|
const removeUnprocessed: AsyncWrapper<UnprocessedDataNode['removeUnprocessed']> = id => {
 | 
						|
  return channels.removeUnprocessed(id);
 | 
						|
};
 | 
						|
 | 
						|
const removeAllUnprocessed: AsyncWrapper<UnprocessedDataNode['removeAllUnprocessed']> = () => {
 | 
						|
  return channels.removeAllUnprocessed();
 | 
						|
};
 | 
						|
 | 
						|
// Attachment downloads
 | 
						|
 | 
						|
async function getNextAttachmentDownloadJobs(limit: number): Promise<any> {
 | 
						|
  return channels.getNextAttachmentDownloadJobs(limit);
 | 
						|
}
 | 
						|
async function saveAttachmentDownloadJob(job: any): Promise<void> {
 | 
						|
  await channels.saveAttachmentDownloadJob(job);
 | 
						|
}
 | 
						|
async function setAttachmentDownloadJobPending(id: string, pending: boolean): Promise<void> {
 | 
						|
  await channels.setAttachmentDownloadJobPending(id, pending ? 1 : 0);
 | 
						|
}
 | 
						|
async function resetAttachmentDownloadPending(): Promise<void> {
 | 
						|
  await channels.resetAttachmentDownloadPending();
 | 
						|
}
 | 
						|
async function removeAttachmentDownloadJob(id: string): Promise<void> {
 | 
						|
  await channels.removeAttachmentDownloadJob(id);
 | 
						|
}
 | 
						|
async function removeAllAttachmentDownloadJobs(): Promise<void> {
 | 
						|
  await channels.removeAllAttachmentDownloadJobs();
 | 
						|
}
 | 
						|
 | 
						|
// Other
 | 
						|
 | 
						|
async function removeAll(): Promise<void> {
 | 
						|
  await channels.removeAll();
 | 
						|
}
 | 
						|
 | 
						|
async function removeAllConversations(): Promise<void> {
 | 
						|
  await channels.removeAllConversations();
 | 
						|
}
 | 
						|
 | 
						|
async function cleanupOrphanedAttachments(): Promise<void> {
 | 
						|
  await dataInit.callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
 | 
						|
}
 | 
						|
 | 
						|
// Note: will need to restart the app after calling this, to set up afresh
 | 
						|
async function removeOtherData(): Promise<void> {
 | 
						|
  await Promise.all([
 | 
						|
    dataInit.callChannel(ERASE_SQL_KEY),
 | 
						|
    dataInit.callChannel(ERASE_ATTACHMENTS_KEY),
 | 
						|
  ]);
 | 
						|
}
 | 
						|
 | 
						|
// Functions below here return plain JSON instead of Backbone Models
 | 
						|
 | 
						|
async function getMessagesWithVisualMediaAttachments(
 | 
						|
  conversationId: string,
 | 
						|
  limit?: number
 | 
						|
): Promise<Array<MessageAttributes>> {
 | 
						|
  return channels.getMessagesWithVisualMediaAttachments(conversationId, limit);
 | 
						|
}
 | 
						|
 | 
						|
async function getMessagesWithFileAttachments(
 | 
						|
  conversationId: string,
 | 
						|
  limit: number
 | 
						|
): Promise<Array<MessageAttributes>> {
 | 
						|
  return channels.getMessagesWithFileAttachments(conversationId, limit);
 | 
						|
}
 | 
						|
 | 
						|
async function getSnodePoolFromDb(): Promise<Array<Snode> | null> {
 | 
						|
  // this is currently all stored as a big string as we don't really need to do anything with them (no filtering or anything)
 | 
						|
  // everything is made in memory and written to disk
 | 
						|
  const snodesJson = await Data.getItemById(SNODE_POOL_ITEM_ID);
 | 
						|
  if (!snodesJson || !snodesJson.value) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  return JSON.parse(snodesJson.value);
 | 
						|
}
 | 
						|
 | 
						|
async function updateSnodePoolOnDb(snodesAsJsonString: string): Promise<void> {
 | 
						|
  await Storage.put(SNODE_POOL_ITEM_ID, snodesAsJsonString);
 | 
						|
}
 | 
						|
 | 
						|
function keysToArrayBuffer(keys: any, data: any) {
 | 
						|
  const updated = _.cloneDeep(data);
 | 
						|
 | 
						|
  for (let i = 0, max = keys.length; i < max; i += 1) {
 | 
						|
    const key = keys[i];
 | 
						|
    const value = _.get(data, key);
 | 
						|
 | 
						|
    if (value) {
 | 
						|
      _.set(updated, key, fromBase64ToArrayBuffer(value));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return updated;
 | 
						|
}
 | 
						|
 | 
						|
function keysFromArrayBuffer(keys: any, data: any) {
 | 
						|
  const updated = _.cloneDeep(data);
 | 
						|
  for (let i = 0, max = keys.length; i < max; i += 1) {
 | 
						|
    const key = keys[i];
 | 
						|
    const value = _.get(data, key);
 | 
						|
 | 
						|
    if (value) {
 | 
						|
      _.set(updated, key, fromArrayBufferToBase64(value));
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return updated;
 | 
						|
}
 | 
						|
 | 
						|
const ITEM_KEYS: object = {
 | 
						|
  identityKey: ['value.pubKey', 'value.privKey'],
 | 
						|
  profileKey: ['value'],
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * For anything related to the UI and redux, do not use `createOrUpdateItem` directly. Instead use Storage.put (from the utils folder).
 | 
						|
 * `Storage.put` will update the settings redux slice if needed but createOrUpdateItem will not.
 | 
						|
 */
 | 
						|
export async function createOrUpdateItem(data: StorageItem): Promise<void> {
 | 
						|
  const { id } = data;
 | 
						|
  if (!id) {
 | 
						|
    throw new Error('createOrUpdateItem: Provided data did not have a truthy id');
 | 
						|
  }
 | 
						|
 | 
						|
  const keys = (ITEM_KEYS as any)[id];
 | 
						|
  const updated = Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data;
 | 
						|
 | 
						|
  await channels.createOrUpdateItem(updated);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Note: In the app, you should always call getItemById through Data.getItemById (from the data.ts file).
 | 
						|
 * This is to ensure testing and stubbbing works as expected
 | 
						|
 */
 | 
						|
export async function getItemById(id: string): Promise<StorageItem | undefined> {
 | 
						|
  const keys = (ITEM_KEYS as any)[id];
 | 
						|
  const data = await channels.getItemById(id);
 | 
						|
 | 
						|
  return Array.isArray(keys) ? keysToArrayBuffer(keys, data) : data;
 | 
						|
}
 | 
						|
/**
 | 
						|
 * Note: In the app, you should always call getAllItems through Data.getAllItems (from the data.ts file).
 | 
						|
 * This is to ensure testing and stubbbing works as expected
 | 
						|
 */
 | 
						|
export async function getAllItems(): Promise<Array<StorageItem>> {
 | 
						|
  const items = await channels.getAllItems();
 | 
						|
  return _.map(items, item => {
 | 
						|
    const { id } = item;
 | 
						|
    const keys = (ITEM_KEYS as any)[id];
 | 
						|
    return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item;
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Note: In the app, you should always call removeItemById through Data.removeItemById (from the data.ts file).
 | 
						|
 * This is to ensure testing and stubbbing works as expected
 | 
						|
 */
 | 
						|
export async function removeItemById(id: string): Promise<void> {
 | 
						|
  await channels.removeItemById(id);
 | 
						|
}
 | 
						|
 | 
						|
// we export them like this instead of directly with the `export function` cause this is helping a lot for testing
 | 
						|
export const Data = {
 | 
						|
  shutdown,
 | 
						|
  close,
 | 
						|
  removeDB,
 | 
						|
  getPasswordHash,
 | 
						|
 | 
						|
  // items table logic
 | 
						|
  createOrUpdateItem,
 | 
						|
  getItemById,
 | 
						|
  getAllItems,
 | 
						|
  removeItemById,
 | 
						|
 | 
						|
  // guard nodes
 | 
						|
  getGuardNodes,
 | 
						|
  updateGuardNodes,
 | 
						|
  generateAttachmentKeyIfEmpty,
 | 
						|
  getSwarmNodesForPubkey,
 | 
						|
  updateSwarmNodesForPubkey,
 | 
						|
  getAllEncryptionKeyPairsForGroup,
 | 
						|
  getLatestClosedGroupEncryptionKeyPair,
 | 
						|
  addClosedGroupEncryptionKeyPair,
 | 
						|
  removeAllClosedGroupEncryptionKeyPairs,
 | 
						|
  saveConversation,
 | 
						|
  fetchConvoMemoryDetails,
 | 
						|
  getConversationById,
 | 
						|
  removeConversation,
 | 
						|
  getAllConversations,
 | 
						|
  getPubkeysInPublicConversation,
 | 
						|
  searchConversations,
 | 
						|
  searchMessages,
 | 
						|
  searchMessagesInConversation,
 | 
						|
  cleanSeenMessages,
 | 
						|
  cleanLastHashes,
 | 
						|
  saveSeenMessageHashes,
 | 
						|
  updateLastHash,
 | 
						|
  saveMessage,
 | 
						|
  saveMessages,
 | 
						|
  removeMessage,
 | 
						|
  removeMessagesByIds,
 | 
						|
  getMessageIdsFromServerIds,
 | 
						|
  getMessageById,
 | 
						|
  getMessagesBySenderAndSentAt,
 | 
						|
  getMessageByServerId,
 | 
						|
  filterAlreadyFetchedOpengroupMessage,
 | 
						|
  getUnreadByConversation,
 | 
						|
  getUnreadCountByConversation,
 | 
						|
  markAllAsReadByConversationNoExpiration,
 | 
						|
  getMessageCountByType,
 | 
						|
  getMessagesByConversation,
 | 
						|
  getLastMessagesByConversation,
 | 
						|
  getLastMessageIdInConversation,
 | 
						|
  getLastMessageInConversation,
 | 
						|
  getOldestMessageInConversation,
 | 
						|
  getMessageCount,
 | 
						|
  getFirstUnreadMessageIdInConversation,
 | 
						|
  getFirstUnreadMessageWithMention,
 | 
						|
  hasConversationOutgoingMessage,
 | 
						|
  getLastHashBySnode,
 | 
						|
  getSeenMessagesByHashList,
 | 
						|
  removeAllMessagesInConversation,
 | 
						|
  getMessagesBySentAt,
 | 
						|
  getExpiredMessages,
 | 
						|
  getOutgoingWithoutExpiresAt,
 | 
						|
  getNextExpiringMessage,
 | 
						|
 | 
						|
  // Unprocessed messages data
 | 
						|
  getUnprocessedCount,
 | 
						|
  getAllUnprocessed,
 | 
						|
  getUnprocessedById,
 | 
						|
  saveUnprocessed,
 | 
						|
  updateUnprocessedAttempts,
 | 
						|
  updateUnprocessedWithData,
 | 
						|
  removeUnprocessed,
 | 
						|
  removeAllUnprocessed,
 | 
						|
 | 
						|
  // attachments download jobs
 | 
						|
  getNextAttachmentDownloadJobs,
 | 
						|
  saveAttachmentDownloadJob,
 | 
						|
  setAttachmentDownloadJobPending,
 | 
						|
  resetAttachmentDownloadPending,
 | 
						|
  removeAttachmentDownloadJob,
 | 
						|
  removeAllAttachmentDownloadJobs,
 | 
						|
  removeAll,
 | 
						|
  removeAllConversations,
 | 
						|
  cleanupOrphanedAttachments,
 | 
						|
  removeOtherData,
 | 
						|
  getMessagesWithVisualMediaAttachments,
 | 
						|
  getMessagesWithFileAttachments,
 | 
						|
  getSnodePoolFromDb,
 | 
						|
  updateSnodePoolOnDb,
 | 
						|
};
 |