From a51f95d28c224b21b4aa4535130dcf8154403c23 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 29 Jan 2021 11:29:24 +1100 Subject: [PATCH 001/109] move models to TS part1 --- AUDRICTOCLEAN.txt | 7 +- background.html | 2 - background_test.html | 2 - js/background.js | 24 +- js/delivery_receipts.js | 5 +- js/expiring_messages.js | 6 +- js/models/conversations.d.ts | 103 - js/models/conversations.js | 1719 ---------------- js/models/messages.d.ts | 134 -- js/models/messages.js | 1492 -------------- js/modules/attachment_downloads.js | 5 +- js/modules/backup.js | 8 +- js/modules/data.d.ts | 3 +- js/modules/data.js | 2 +- js/modules/indexeddb.js | 6 +- js/modules/loki_app_dot_net_api.d.ts | 4 + js/modules/loki_app_dot_net_api.js | 15 +- js/modules/messages_data_migrator.js | 4 +- js/modules/signal.js | 51 +- js/modules/types/message.js | 2 - js/read_receipts.js | 5 +- js/read_syncs.js | 2 +- js/views/app_view.js | 2 +- js/views/invite_contacts_dialog_view.js | 2 +- js/views/update_group_dialog_view.js | 4 +- package.json | 1 + preload.js | 85 +- protos/SignalService.proto | 6 +- test/backup_test.js | 7 +- test/fixtures.js | 2 +- test/fixtures_test.js | 5 +- test/index.html | 2 - test/models/conversations_test.js | 22 +- test/models/messages_test.js | 14 +- ts/components/ConversationListItem.tsx | 36 +- ts/components/EditProfileDialog.tsx | 8 +- ts/components/LeftPane.tsx | 3 - ts/components/conversation/AddMentions.tsx | 6 +- ts/components/conversation/Message.tsx | 27 +- ts/components/conversation/MessageBody.tsx | 33 +- ts/components/conversation/MessageDetail.tsx | 2 +- .../conversation/ModeratorsAddDialog.tsx | 2 +- .../conversation/ModeratorsRemoveDialog.tsx | 2 +- .../conversation/message/MessageMetadata.tsx | 5 +- .../session/LeftPaneMessageSection.tsx | 4 +- .../session/SessionClosableOverlay.tsx | 3 +- ts/components/session/SessionInboxView.tsx | 28 +- .../conversation/SessionCompositionBox.tsx | 4 +- .../conversation/SessionConversation.tsx | 28 +- .../conversation/SessionMessagesList.tsx | 11 +- .../conversation/SessionRightPanel.tsx | 5 +- .../usingClosedConversationDetails.tsx | 6 +- ts/models/conversation.ts | 1821 +++++++++++++++++ ts/models/index.ts | 6 + ts/models/message.ts | 1394 +++++++++++++ ts/models/messageType.ts | 213 ++ ts/receiver/attachments.ts | 42 +- ts/receiver/closedGroups.ts | 260 ++- ts/receiver/contentMessage.ts | 2 +- ts/receiver/dataMessage.ts | 34 +- ts/receiver/errors.ts | 10 +- ts/receiver/queuedJob.ts | 37 +- ts/receiver/receiver.ts | 2 +- ts/session/constants.ts | 6 +- .../conversations/ConversationController.ts | 325 --- ts/session/conversations/index.ts | 327 ++- ts/session/group/index.ts | 335 +-- ts/session/messages/MessageController.ts | 6 +- .../group/ClosedGroupAddedMembersMessage.ts | 47 + .../group/ClosedGroupMemberLeftMessage.ts | 31 + .../group/ClosedGroupNameChangeMessage.ts | 42 + .../group/ClosedGroupRemovedMembersMessage.ts | 46 + .../data/group/ClosedGroupUpdateMessage.ts | 51 - .../outgoing/content/data/group/index.ts | 4 +- .../messages/outgoing/content/data/index.ts | 1 - ts/session/onions/index.ts | 3 +- ts/session/sending/MessageQueue.ts | 9 +- ts/session/sending/MessageQueueInterface.ts | 9 +- ts/session/snode_api/swarmPolling.ts | 26 +- ts/session/types/OpenGroup.ts | 2 +- ts/session/utils/User.ts | 36 +- ts/shims/Whisper.ts | 4 - ts/state/ducks/conversations.ts | 23 +- ts/state/ducks/search.ts | 10 +- ts/state/selectors/conversations.ts | 5 + .../unit/crypto/MessageEncrypter_test.ts | 2 +- .../unit/receiving/ClosedGroupUpdates_test.ts | 60 + .../session/unit/sending/MessageQueue_test.ts | 2 +- .../unit/sending/MessageSender_test.ts | 2 +- ts/test/session/unit/utils/Messages_test.ts | 37 +- ts/test/state/selectors/conversations_test.ts | 5 - ts/test/test-utils/utils/envelope.ts | 39 + ts/test/test-utils/utils/index.ts | 1 + ts/test/test-utils/utils/message.ts | 6 +- ts/test/util/blockedNumberController_test.ts | 4 +- ts/types/Message.ts | 15 +- ts/util/blockedNumberController.ts | 2 +- ts/util/findMember.ts | 4 +- ts/util/lint/linter.ts | 1 - ts/window.d.ts | 6 +- 100 files changed, 4817 insertions(+), 4504 deletions(-) delete mode 100644 js/models/conversations.d.ts delete mode 100644 js/models/conversations.js delete mode 100644 js/models/messages.d.ts delete mode 100644 js/models/messages.js create mode 100644 ts/models/conversation.ts create mode 100644 ts/models/index.ts create mode 100644 ts/models/message.ts create mode 100644 ts/models/messageType.ts delete mode 100644 ts/session/conversations/ConversationController.ts create mode 100644 ts/session/messages/outgoing/content/data/group/ClosedGroupAddedMembersMessage.ts create mode 100644 ts/session/messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage.ts create mode 100644 ts/session/messages/outgoing/content/data/group/ClosedGroupNameChangeMessage.ts create mode 100644 ts/session/messages/outgoing/content/data/group/ClosedGroupRemovedMembersMessage.ts delete mode 100644 ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts delete mode 100644 ts/shims/Whisper.ts create mode 100644 ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts create mode 100644 ts/test/test-utils/utils/envelope.ts diff --git a/AUDRICTOCLEAN.txt b/AUDRICTOCLEAN.txt index 115d0d3589..97cdd874cb 100644 --- a/AUDRICTOCLEAN.txt +++ b/AUDRICTOCLEAN.txt @@ -11,7 +11,7 @@ remove from db * 'signedPreKeys' * senderkeys - +getContact() remove what is is Storage / user.js remove on the UI ts files the calls to conversationModel. everything should be on the props conversationModel @@ -24,6 +24,9 @@ getRecipients() does not make asny sense right ReadSyncs SyncMessage sendSyncMessage needs to be rewritten -sendSyncMessageOnly to fix +sendSyncMessageOnly to fix + + +LONG_ATTAHCMENNT diff --git a/background.html b/background.html index f1eab8f015..4a873f3a50 100644 --- a/background.html +++ b/background.html @@ -141,8 +141,6 @@ - - diff --git a/background_test.html b/background_test.html index bc4a9d3895..599b6200cd 100644 --- a/background_test.html +++ b/background_test.html @@ -145,8 +145,6 @@ - - diff --git a/js/background.js b/js/background.js index 283365e64d..5aefe70d06 100644 --- a/js/background.js +++ b/js/background.js @@ -120,7 +120,7 @@ accountManager = new textsecure.AccountManager(USERNAME, PASSWORD); accountManager.addEventListener('registration', () => { const user = { - ourNumber: textsecure.storage.user.getNumber(), + ourNumber: libsession.Utils.UserUtils.getOurPubKeyStrFromCache(), ourPrimary: window.textsecure.storage.get('primaryDevicePubKey'), }; Whisper.events.trigger('userChanged', user); @@ -152,7 +152,8 @@ } const publicConversations = await window.Signal.Data.getAllPublicConversations( { - ConversationCollection: Whisper.ConversationCollection, + ConversationCollection: + window.models.Conversation.ConversationCollection, } ); publicConversations.forEach(conversation => { @@ -166,7 +167,7 @@ if (window.initialisedAPI) { return; } - const ourKey = textsecure.storage.user.getNumber(); + const ourKey = libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); window.lokiMessageAPI = new window.LokiMessageAPI(); // singleton to relay events to libtextsecure/message_receiver window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); @@ -227,7 +228,10 @@ Whisper.Registration.isDone() && !storage.get('primaryDevicePubKey', null) ) { - storage.put('primaryDevicePubKey', textsecure.storage.user.getNumber()); + storage.put( + 'primaryDevicePubKey', + window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache() + ); } // These make key operations available to IPC handlers created in preload.js @@ -322,8 +326,8 @@ if (!isMigrationWithIndexComplete) { const batchWithIndex = await MessageDataMigrator.processNext({ - BackboneMessage: Whisper.Message, - BackboneMessageCollection: Whisper.MessageCollection, + BackboneMessage: window.models.Message.MessageModel, + BackboneMessageCollection: window.models.Message.MessageCollection, numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, upgradeMessageSchema, getMessagesNeedingUpgrade: @@ -391,7 +395,7 @@ conversation.removeMessage(id); } window.Signal.Data.removeMessage(id, { - Message: Whisper.Message, + Message: window.models.Message.MessageModel, }); }); } @@ -411,7 +415,7 @@ const results = await Promise.all([ window.Signal.Data.getOutgoingWithoutExpiresAt({ - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, }), ]); @@ -450,7 +454,7 @@ window.log.info(`Cleanup: Deleting unsent message ${sentAt}`); await window.Signal.Data.removeMessage(message.id, { - Message: Whisper.Message, + Message: window.models.Message.MessageModel, }); }) ); @@ -1018,7 +1022,7 @@ }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); // TODO: Investigate the case where we reconnect - const ourKey = textsecure.storage.user.getNumber(); + const ourKey = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); window.SwarmPolling.addPubkey(ourKey); window.SwarmPolling.start(); diff --git a/js/delivery_receipts.js b/js/delivery_receipts.js index fad68e8243..fdc419a11f 100644 --- a/js/delivery_receipts.js +++ b/js/delivery_receipts.js @@ -45,7 +45,8 @@ const groups = await window.Signal.Data.getAllGroupsInvolvingId( originalSource, { - ConversationCollection: Whisper.ConversationCollection, + ConversationCollection: + window.models.Conversation.ConversationCollection, } ); @@ -67,7 +68,7 @@ const messages = await window.Signal.Data.getMessagesBySentAt( receipt.get('timestamp'), { - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, } ); diff --git a/js/expiring_messages.js b/js/expiring_messages.js index b89e45a676..41eba87214 100644 --- a/js/expiring_messages.js +++ b/js/expiring_messages.js @@ -17,7 +17,7 @@ try { window.log.info('destroyExpiredMessages: Loading messages...'); const messages = await window.Signal.Data.getExpiredMessages({ - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, }); await Promise.all( @@ -31,7 +31,7 @@ // We delete after the trigger to allow the conversation time to process // the expiration before the message is removed from the database. await window.Signal.Data.removeMessage(message.id, { - Message: Whisper.Message, + Message: window.models.Message.MessageModel, }); Whisper.events.trigger('messageExpired', { @@ -60,7 +60,7 @@ async function checkExpiringMessages() { // Look up the next expiring message and set a timer to destroy it const messages = await window.Signal.Data.getNextExpiringMessage({ - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, }); const next = messages.at(0); diff --git a/js/models/conversations.d.ts b/js/models/conversations.d.ts deleted file mode 100644 index 88d27f5d30..0000000000 --- a/js/models/conversations.d.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { MessageModel, MessageAttributes } from './messages'; - -interface ConversationAttributes { - profileName?: string; - id: string; - name: string; - members: Array; - left: boolean; - expireTimer: number; - profileSharing: boolean; - mentionedUs: boolean; - unreadCount: number; - active_at: number; - timestamp: number; // timestamp of what? - lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group - groupAdmins?: Array; - isKickedFromGroup?: boolean; - avatarPath?: string; - isMe?: boolean; - subscriberCount?: number; - sessionRestoreSeen?: boolean; - is_medium_group?: boolean; - type: string; - lastMessage?: string; -} - -export interface ConversationModel - extends Backbone.Model { - destroyMessages(); - getPublicSendData(); - leaveGroup(); - idForLogging: () => string; - // Save model changes to the database - commit: () => Promise; - notify: (message: MessageModel) => void; - updateExpirationTimer: ( - expireTimer: number | null, - source?: string, - receivedAt?: number, - options?: object - ) => Promise; - isPrivate: () => boolean; - getProfile: (id: string) => Promise; - getProfiles: () => Promise; - setProfileKey: (key: string) => Promise; - isMe: () => boolean; - getRecipients: () => Array; - getTitle: () => string; - onReadMessage: (message: MessageModel) => void; - getName: () => string; - addMessage: (attributes: Partial) => Promise; - isMediumGroup: () => boolean; - getNickname: () => string | undefined; - setNickname: (nickname: string | undefined) => Promise; - getUnread: () => Promise; - getUnreadCount: () => Promise; - - isPublic: () => boolean; - isClosedGroup: () => boolean; - isBlocked: () => boolean; - isAdmin: (id: string) => boolean; - throttledBumpTyping: () => void; - - // types to make more specific - sendMessage: ( - body: any, - attachments: any, - quote: any, - preview: any, - groupInvitation: any, - otherOptions: any - ) => Promise; - updateGroupAdmins: any; - setLokiProfile: any; - getLokiProfile: any; - getNumber: any; - getProfileName: any; - getAvatarPath: any; - markRead: (timestamp: number) => Promise; - showChannelLightbox: any; - deletePublicMessages: any; - makeQuote: any; - unblock: any; - deleteContact: any; - removeMessage: (messageId: string) => Promise; - deleteMessages(); - - endSession: () => Promise; - block: any; - copyPublicKey: any; - getAvatar: any; - notifyTyping: ( - { isTyping, sender } = { isTyping: boolean, sender: string } - ) => any; - queueJob: any; - onUpdateGroupName: any; - getContactProfileNameOrShortenedPubKey: () => string; - getContactProfileNameOrFullPubKey: () => string; - getProps(): any; - updateLastMessage: () => void; - updateProfileName: any; - updateProfileAvatar: any; -} diff --git a/js/models/conversations.js b/js/models/conversations.js deleted file mode 100644 index a16f2fe505..0000000000 --- a/js/models/conversations.js +++ /dev/null @@ -1,1719 +0,0 @@ -/* global - _, - log, - i18n, - Backbone, - libsession, - getMessageController, - storage, - textsecure, - Whisper, - profileImages, - clipboard, - BlockedNumberController, - lokiPublicChatAPI, -*/ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - const { Contact, Conversation, Message } = window.Signal.Types; - const { - upgradeMessageSchema, - loadAttachmentData, - getAbsoluteAttachmentPath, - deleteAttachmentData, - } = window.Signal.Migrations; - - Whisper.Conversation = Backbone.Model.extend({ - storeName: 'conversations', - defaults() { - return { - unreadCount: 0, - groupAdmins: [], - isKickedFromGroup: false, - profileSharing: false, - left: false, - lastJoinedTimestamp: new Date('1970-01-01Z00:00:00:000').getTime(), - }; - }, - - idForLogging() { - if (this.isPrivate()) { - return this.id; - } - - return `group(${this.id})`; - }, - - initialize() { - this.ourNumber = textsecure.storage.user.getNumber(); - - // This may be overridden by ConversationController.getOrCreate, and signify - // our first save to the database. Or first fetch from the database. - this.initialPromise = Promise.resolve(); - - this.messageCollection = new Whisper.MessageCollection([], { - conversation: this, - }); - - this.throttledBumpTyping = _.throttle(this.bumpTyping, 300); - this.updateLastMessage = _.throttle( - this.bouncyUpdateLastMessage.bind(this), - 1000 - ); - // this.listenTo( - // this.messageCollection, - // 'add remove destroy', - // debouncedUpdateLastMessage - // ); - // Listening for out-of-band data updates - this.on('delivered', this.updateAndMerge); - this.on('read', this.updateAndMerge); - this.on('expiration-change', this.updateAndMerge); - this.on('expired', this.onExpired); - - this.on('ourAvatarChanged', avatar => - this.updateAvatarOnPublicChat(avatar) - ); - - // Always share profile pics with public chats - if (this.isPublic) { - this.set('profileSharing', true); - } - this.unset('hasFetchedProfile'); - this.unset('tokens'); - - this.typingRefreshTimer = null; - this.typingPauseTimer = null; - - // Keep props ready - const generateProps = () => { - this.cachedProps = this.getProps(); - }; - this.on('change', generateProps); - generateProps(); - }, - isMe() { - return this.id === this.ourNumber; - }, - isPublic() { - return !!(this.id && this.id.match(/^publicChat:/)); - }, - isClosedGroup() { - return this.get('type') === Message.GROUP && !this.isPublic(); - }, - - isBlocked() { - if (!this.id || this.isMe()) { - return false; - } - - if (this.isClosedGroup()) { - return BlockedNumberController.isGroupBlocked(this.id); - } - - if (this.isPrivate()) { - return BlockedNumberController.isBlocked(this.id); - } - - return false; - }, - isMediumGroup() { - return this.get('is_medium_group'); - }, - async block() { - if (!this.id || this.isPublic()) { - return; - } - - const promise = this.isPrivate() - ? BlockedNumberController.block(this.id) - : BlockedNumberController.blockGroup(this.id); - await promise; - this.commit(); - }, - async unblock() { - if (!this.id || this.isPublic()) { - return; - } - const promise = this.isPrivate() - ? BlockedNumberController.unblock(this.id) - : BlockedNumberController.unblockGroup(this.id); - await promise; - this.commit(); - }, - async bumpTyping() { - if (this.isPublic() || this.isMediumGroup()) { - return; - } - // We don't send typing messages if the setting is disabled - // or we blocked that user - - if (!storage.get('typing-indicators-setting') || this.isBlocked()) { - return; - } - - if (!this.typingRefreshTimer) { - const isTyping = true; - this.setTypingRefreshTimer(); - this.sendTypingMessage(isTyping); - } - - this.setTypingPauseTimer(); - }, - - setTypingRefreshTimer() { - if (this.typingRefreshTimer) { - clearTimeout(this.typingRefreshTimer); - } - this.typingRefreshTimer = setTimeout( - this.onTypingRefreshTimeout.bind(this), - 10 * 1000 - ); - }, - - onTypingRefreshTimeout() { - const isTyping = true; - this.sendTypingMessage(isTyping); - - // This timer will continue to reset itself until the pause timer stops it - this.setTypingRefreshTimer(); - }, - - setTypingPauseTimer() { - if (this.typingPauseTimer) { - clearTimeout(this.typingPauseTimer); - } - this.typingPauseTimer = setTimeout( - this.onTypingPauseTimeout.bind(this), - 10 * 1000 - ); - }, - - onTypingPauseTimeout() { - const isTyping = false; - this.sendTypingMessage(isTyping); - - this.clearTypingTimers(); - }, - - clearTypingTimers() { - if (this.typingPauseTimer) { - clearTimeout(this.typingPauseTimer); - this.typingPauseTimer = null; - } - if (this.typingRefreshTimer) { - clearTimeout(this.typingRefreshTimer); - this.typingRefreshTimer = null; - } - }, - - sendTypingMessage(isTyping) { - if (!this.isPrivate()) { - return; - } - - const recipientId = this.id; - - if (!recipientId) { - throw new Error('Need to provide either recipientId'); - } - - const primaryDevicePubkey = window.storage.get('primaryDevicePubKey'); - if (recipientId && primaryDevicePubkey === recipientId) { - // note to self - return; - } - - const typingParams = { - timestamp: Date.now(), - isTyping, - typingTimestamp: Date.now(), - }; - const typingMessage = new libsession.Messages.Outgoing.TypingMessage( - typingParams - ); - - // send the message to a single recipient if this is a session chat - const device = new libsession.Types.PubKey(recipientId); - libsession - .getMessageQueue() - .sendToPubKey(device, typingMessage) - .catch(log.error); - }, - - async cleanup() { - await window.Signal.Types.Conversation.deleteExternalFiles( - this.attributes, - { - deleteAttachmentData, - } - ); - profileImages.removeImage(this.id); - }, - - async updateProfileAvatar() { - if (this.isPublic()) { - return; - } - - // Remove old identicons - if (profileImages.hasImage(this.id)) { - profileImages.removeImage(this.id); - await this.setProfileAvatar(null); - } - }, - - async updateAndMerge(message) { - this.updateLastMessage(); - - const mergeMessage = () => { - const existing = this.messageCollection.get(message.id); - if (!existing) { - return; - } - - existing.merge(message.attributes); - }; - - mergeMessage(); - }, - - async onExpired(message) { - this.updateLastMessage(); - - const removeMessage = () => { - const { id } = message; - const existing = this.messageCollection.get(id); - if (!existing) { - return; - } - - window.log.info('Remove expired message from collection', { - sentAt: existing.get('sent_at'), - }); - - this.messageCollection.remove(id); - existing.trigger('expired'); - }; - - removeMessage(); - }, - - // Get messages with the given timestamp - getMessagesWithTimestamp(pubKey, timestamp) { - if (this.id !== pubKey) { - return []; - } - - // Go through our messages and find the one that we need to update - return this.messageCollection.models.filter( - m => m.get('sent_at') === timestamp - ); - }, - - async onCalculatingPoW(pubKey, timestamp) { - const messages = this.getMessagesWithTimestamp(pubKey, timestamp); - await Promise.all(messages.map(m => m.setCalculatingPoW())); - }, - - async onPublicMessageSent(identifier, serverId, serverTimestamp) { - const registeredMessage = window.getMessageController().get(identifier); - - if (!registeredMessage || !registeredMessage.message) { - return null; - } - const model = registeredMessage.message; - await model.setIsPublic(true); - await model.setServerId(serverId); - await model.setServerTimestamp(serverTimestamp); - return undefined; - }, - addSingleMessage(message, setToExpire = true) { - const model = this.messageCollection.add(message, { merge: true }); - if (setToExpire) { - model.setToExpire(); - } - return model; - }, - format() { - return this.cachedProps; - }, - getGroupAdmins() { - return this.get('groupAdmins') || this.get('moderators'); - }, - getProps() { - const typingKeys = Object.keys(this.contactTypingTimers || {}); - - const groupAdmins = this.getGroupAdmins(); - - const members = - this.isGroup() && !this.isPublic() ? this.get('members') : undefined; - - const result = { - id: this.id, - activeAt: this.get('active_at'), - avatarPath: this.getAvatarPath(), - type: this.isPrivate() ? 'direct' : 'group', - isMe: this.isMe(), - isPublic: this.isPublic(), - isTyping: typingKeys.length > 0, - lastUpdated: this.get('timestamp'), - name: this.getName(), - profileName: this.getProfileName(), - timestamp: this.get('timestamp'), - title: this.getTitle(), - unreadCount: this.get('unreadCount') || 0, - mentionedUs: this.get('mentionedUs') || false, - isBlocked: this.isBlocked(), - phoneNumber: this.id, - lastMessage: { - status: this.get('lastMessageStatus'), - text: this.get('lastMessage'), - }, - hasNickname: !!this.getNickname(), - isKickedFromGroup: !!this.get('isKickedFromGroup'), - left: !!this.get('left'), - groupAdmins, - members, - onClick: () => this.trigger('select', this), - onBlockContact: () => this.block(), - onUnblockContact: () => this.unblock(), - onCopyPublicKey: () => this.copyPublicKey(), - onDeleteContact: () => this.deleteContact(), - onLeaveGroup: () => { - window.Whisper.events.trigger('leaveGroup', this); - }, - onDeleteMessages: () => this.deleteMessages(), - onInviteContacts: () => { - window.Whisper.events.trigger('inviteContacts', this); - }, - onClearNickname: () => { - this.setLokiProfile({ displayName: null }); - }, - }; - - return result; - }, - - async updateGroupAdmins(groupAdmins) { - const existingAdmins = _.sortBy(this.getGroupAdmins()); - const newAdmins = _.sortBy(groupAdmins); - - if (_.isEqual(existingAdmins, newAdmins)) { - window.log.info( - 'Skipping updates of groupAdmins/moderators. No change detected.' - ); - return; - } - this.set({ groupAdmins }); - await this.commit(); - }, - - async onReadMessage(message, readAt) { - // We mark as read everything older than this message - to clean up old stuff - // still marked unread in the database. If the user generally doesn't read in - // the desktop app, so the desktop app only gets read syncs, we can very - // easily end up with messages never marked as read (our previous early read - // sync handling, read syncs never sent because app was offline) - - // We queue it because we often get a whole lot of read syncs at once, and - // their markRead calls could very easily overlap given the async pull from DB. - - // Lastly, we don't send read syncs for any message marked read due to a read - // sync. That's a notification explosion we don't need. - return this.queueJob(() => - this.markRead(message.get('received_at'), { - sendReadReceipts: false, - readAt, - }) - ); - }, - - async getUnread() { - return window.Signal.Data.getUnreadByConversation(this.id, { - MessageCollection: Whisper.MessageCollection, - }); - }, - - async getUnreadCount() { - return window.Signal.Data.getUnreadCountByConversation(this.id); - }, - - validate(attributes) { - const required = ['id', 'type']; - const missing = _.filter(required, attr => !attributes[attr]); - if (missing.length) { - return `Conversation must have ${missing}`; - } - - if (attributes.type !== 'private' && attributes.type !== 'group') { - return `Invalid conversation type: ${attributes.type}`; - } - - const error = this.validateNumber(); - if (error) { - return error; - } - - return null; - }, - - validateNumber() { - if (!this.id) { - return 'Invalid ID'; - } - if (!this.isPrivate()) { - return null; - } - - // Check if it's hex - const isHex = this.id.replace(/[\s]*/g, '').match(/^[0-9a-fA-F]+$/); - if (!isHex) { - return 'Invalid Hex ID'; - } - - // Check if the pubkey length is 33 and leading with 05 or of length 32 - const len = this.id.length; - if ((len !== 33 * 2 || !/^05/.test(this.id)) && len !== 32 * 2) { - return 'Invalid Pubkey Format'; - } - - this.set({ id: this.id }); - return null; - }, - - queueJob(callback) { - const previous = this.pending || Promise.resolve(); - - const taskWithTimeout = textsecure.createTaskWithTimeout( - callback, - `conversation ${this.idForLogging()}` - ); - - this.pending = previous.then(taskWithTimeout, taskWithTimeout); - const current = this.pending; - - current.then(() => { - if (this.pending === current) { - delete this.pending; - } - }); - - return current; - }, - getRecipients() { - if (this.isPrivate()) { - return [this.id]; - } - const me = textsecure.storage.user.getNumber(); - return _.without(this.get('members'), me); - }, - - async getQuoteAttachment(attachments, preview) { - if (attachments && attachments.length) { - return Promise.all( - attachments - .filter( - attachment => - attachment && - attachment.contentType && - !attachment.pending && - !attachment.error - ) - .slice(0, 1) - .map(async attachment => { - const { fileName, thumbnail, contentType } = attachment; - - return { - contentType, - // Our protos library complains about this field being undefined, so we - // force it to null - fileName: fileName || null, - thumbnail: thumbnail - ? { - ...(await loadAttachmentData(thumbnail)), - objectUrl: getAbsoluteAttachmentPath(thumbnail.path), - } - : null, - }; - }) - ); - } - - if (preview && preview.length) { - return Promise.all( - preview - .filter(item => item && item.image) - .slice(0, 1) - .map(async attachment => { - const { image } = attachment; - const { contentType } = image; - - return { - contentType, - // Our protos library complains about this field being undefined, so we - // force it to null - fileName: null, - thumbnail: image - ? { - ...(await loadAttachmentData(image)), - objectUrl: getAbsoluteAttachmentPath(image.path), - } - : null, - }; - }) - ); - } - - return []; - }, - - async makeQuote(quotedMessage) { - const { getName } = Contact; - const contact = quotedMessage.getContact(); - const attachments = quotedMessage.get('attachments'); - const preview = quotedMessage.get('preview'); - - const body = quotedMessage.get('body'); - const embeddedContact = quotedMessage.get('contact'); - const embeddedContactName = - embeddedContact && embeddedContact.length > 0 - ? getName(embeddedContact[0]) - : ''; - - return { - author: contact.id, - id: quotedMessage.get('sent_at'), - text: body || embeddedContactName, - attachments: await this.getQuoteAttachment(attachments, preview), - }; - }, - - toOpenGroup() { - if (!this.isPublic()) { - return undefined; - } - - return new libsession.Types.OpenGroup({ - server: this.get('server'), - channel: this.get('channelId'), - conversationId: this.id, - }); - }, - async sendMessageJob(message) { - try { - const uploads = await message.uploadData(); - const { id } = message; - const expireTimer = this.get('expireTimer'); - const destination = this.id; - - const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ - body: uploads.body, - identifier: id, - timestamp: message.get('sent_at'), - attachments: uploads.attachments, - expireTimer, - preview: uploads.preview, - quote: uploads.quote, - lokiProfile: this.getOurProfile(), - }); - - if (this.isPublic()) { - const openGroup = this.toOpenGroup(); - - const openGroupParams = { - body: uploads.body, - timestamp: message.get('sent_at'), - group: openGroup, - attachments: uploads.attachments, - preview: uploads.preview, - quote: uploads.quote, - identifier: id, - }; - const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( - openGroupParams - ); - // we need the return await so that errors are caught in the catch {} - return await libsession - .getMessageQueue() - .sendToGroup(openGroupMessage); - } - - const destinationPubkey = new libsession.Types.PubKey(destination); - if (this.isPrivate()) { - // Handle Group Invitation Message - if (message.get('groupInvitation')) { - const groupInvitation = message.get('groupInvitation'); - const groupInvitMessage = new libsession.Messages.Outgoing.GroupInvitationMessage( - { - identifier: id, - timestamp: message.get('sent_at'), - serverName: groupInvitation.name, - channelId: groupInvitation.channelId, - serverAddress: groupInvitation.address, - expireTimer: this.get('expireTimer'), - } - ); - // we need the return await so that errors are caught in the catch {} - return await libsession - .getMessageQueue() - .sendToPubKey(destinationPubkey, groupInvitMessage); - } - // we need the return await so that errors are caught in the catch {} - return await libsession - .getMessageQueue() - .sendToPubKey(destinationPubkey, chatMessage); - } - - if (this.isMediumGroup()) { - const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( - { - chatMessage, - groupId: destination, - } - ); - - // we need the return await so that errors are caught in the catch {} - return await libsession - .getMessageQueue() - .sendToGroup(closedGroupChatMessage); - } - - if (this.isClosedGroup()) { - throw new Error( - 'Legacy group are not supported anymore. You need to recreate this group.' - ); - } - - throw new TypeError(`Invalid conversation type: '${this.get('type')}'`); - } catch (e) { - await message.saveErrors(e); - return null; - } - }, - async sendMessage( - body, - attachments, - quote, - preview, - groupInvitation = null - ) { - this.clearTypingTimers(); - - const destination = this.id; - const expireTimer = this.get('expireTimer'); - const recipients = this.getRecipients(); - - const now = Date.now(); - - window.log.info( - 'Sending message to conversation', - this.idForLogging(), - 'with timestamp', - now - ); - // be sure an empty quote is marked as undefined rather than being empty - // otherwise upgradeMessageSchema() will return an object with an empty array - // and this.get('quote') will be true, even if there is no quote. - const editedQuote = _.isEmpty(quote) ? undefined : quote; - - const messageWithSchema = await upgradeMessageSchema({ - type: 'outgoing', - body, - conversationId: destination, - quote: editedQuote, - preview, - attachments, - sent_at: now, - received_at: now, - expireTimer, - recipients, - }); - - if (this.isPublic()) { - // Public chats require this data to detect duplicates - messageWithSchema.source = textsecure.storage.user.getNumber(); - messageWithSchema.sourceDevice = 1; - } else { - messageWithSchema.destination = destination; - } - - const attributes = { - ...messageWithSchema, - groupInvitation, - id: window.getGuid(), - }; - - const model = this.addSingleMessage(attributes); - const message = getMessageController().register(model.id, model); - - await message.commit(true); - - if (this.isPrivate()) { - message.set({ destination }); - } - if (this.isPublic()) { - message.setServerTimestamp(new Date().getTime()); - } - - const id = await message.commit(); - message.set({ id }); - - window.Whisper.events.trigger('messageAdded', { - conversationKey: this.id, - messageModel: message, - }); - - this.set({ - lastMessage: model.getNotificationText(), - lastMessageStatus: 'sending', - active_at: now, - timestamp: now, - }); - await this.commit(); - - // We're offline! - if (!textsecure.messaging) { - const error = new Error('Network is not available'); - error.name = 'SendMessageNetworkError'; - error.number = this.id; - await message.saveErrors([error]); - return null; - } - - this.queueJob(async () => { - await this.sendMessageJob(message); - }); - return null; - }, - - async updateAvatarOnPublicChat({ url, profileKey }) { - if (!this.isPublic()) { - return; - } - if (!this.get('profileSharing')) { - return; - } - - if (profileKey && typeof profileKey !== 'string') { - // eslint-disable-next-line no-param-reassign - profileKey = window.Signal.Crypto.arrayBufferToBase64(profileKey); - } - const serverAPI = await lokiPublicChatAPI.findOrCreateServer( - this.get('server') - ); - await serverAPI.setAvatar(url, profileKey); - }, - async bouncyUpdateLastMessage() { - if (!this.id) { - return; - } - if (!this.get('active_at')) { - window.log.info('Skipping update last message as active_at is falsy'); - return; - } - const messages = await window.Signal.Data.getMessagesByConversation( - this.id, - { limit: 1, MessageCollection: Whisper.MessageCollection } - ); - const lastMessageModel = messages.at(0); - const lastMessageJSON = lastMessageModel - ? lastMessageModel.toJSON() - : null; - const lastMessageStatusModel = lastMessageModel - ? lastMessageModel.getMessagePropStatus() - : null; - const lastMessageUpdate = Conversation.createLastMessageUpdate({ - currentTimestamp: this.get('timestamp') || null, - lastMessage: lastMessageJSON, - lastMessageStatus: lastMessageStatusModel, - lastMessageNotificationText: lastMessageModel - ? lastMessageModel.getNotificationText() - : null, - }); - // Because we're no longer using Backbone-integrated saves, we need to manually - // clear the changed fields here so our hasChanged() check below is useful. - this.changed = {}; - this.set(lastMessageUpdate); - if (this.hasChanged()) { - await this.commit(); - } - }, - - async updateExpirationTimer( - providedExpireTimer, - providedSource, - receivedAt, - options = {} - ) { - let expireTimer = providedExpireTimer; - let source = providedSource; - - _.defaults(options, { fromSync: false, fromGroupUpdate: false }); - - if (!expireTimer) { - expireTimer = null; - } - if ( - this.get('expireTimer') === expireTimer || - (!expireTimer && !this.get('expireTimer')) - ) { - return null; - } - - window.log.info("Update conversation 'expireTimer'", { - id: this.idForLogging(), - expireTimer, - source, - }); - - source = source || textsecure.storage.user.getNumber(); - - // When we add a disappearing messages notification to the conversation, we want it - // to be above the message that initiated that change, hence the subtraction. - const timestamp = (receivedAt || Date.now()) - 1; - - this.set({ expireTimer }); - await this.commit(); - - const message = this.messageCollection.add({ - // Even though this isn't reflected to the user, we want to place the last seen - // indicator above it. We set it to 'unread' to trigger that placement. - unread: 1, - conversationId: this.id, - // No type; 'incoming' messages are specially treated by conversation.markRead() - sent_at: timestamp, - received_at: timestamp, - flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, - expirationTimerUpdate: { - expireTimer, - source, - fromSync: options.fromSync, - fromGroupUpdate: options.fromGroupUpdate, - }, - }); - - message.set({ destination: this.id }); - - if (message.isOutgoing()) { - message.set({ recipients: this.getRecipients() }); - } - - const id = await message.commit(); - - message.set({ id }); - window.Whisper.events.trigger('messageAdded', { - conversationKey: this.id, - messageModel: message, - }); - - await this.commit(); - - // if change was made remotely, don't send it to the number/group - if (receivedAt) { - return message; - } - - let profileKey; - if (this.get('profileSharing')) { - profileKey = storage.get('profileKey'); - } - - const expireUpdate = { - identifier: id, - timestamp: message.get('sent_at'), - expireTimer, - profileKey, - }; - - if (this.isMe()) { - const expirationTimerMessage = new libsession.Messages.Outgoing.ExpirationTimerUpdateMessage( - expireUpdate - ); - return message.sendSyncMessageOnly(expirationTimerMessage); - } - - if (this.get('type') === 'private') { - const expirationTimerMessage = new libsession.Messages.Outgoing.ExpirationTimerUpdateMessage( - expireUpdate - ); - const pubkey = new libsession.Types.PubKey(this.get('id')); - await libsession - .getMessageQueue() - .sendToPubKey(pubkey, expirationTimerMessage); - } else { - expireUpdate.groupId = this.get('id'); - const expirationTimerMessage = new libsession.Messages.Outgoing.ExpirationTimerUpdateMessage( - expireUpdate - ); - // special case when we are the only member of a closed group - const ourNumber = textsecure.storage.user.getNumber(); - - if ( - this.get('members').length === 1 && - this.get('members')[0] === ourNumber - ) { - return message.sendSyncMessageOnly(expirationTimerMessage); - } - await libsession.getMessageQueue().sendToGroup(expirationTimerMessage); - } - return message; - }, - - isSearchable() { - return !this.get('left'); - }, - - async commit() { - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); - await this.trigger('change', this); - }, - - async addMessage(messageAttributes) { - const message = this.messageCollection.add(messageAttributes); - - const messageId = await message.commit(); - message.set({ id: messageId }); - window.Whisper.events.trigger('messageAdded', { - conversationKey: this.id, - messageModel: message, - }); - return message; - }, - - async leaveGroup() { - if (this.get('type') !== 'group') { - log.error('Cannot leave a non-group conversation'); - return; - } - - if (this.isMediumGroup()) { - await window.libsession.ClosedGroup.leaveClosedGroup(this.id); - } else { - throw new Error( - 'Legacy group are not supported anymore. You need to create this group again.' - ); - } - }, - - async markRead(newestUnreadDate, providedOptions) { - const options = providedOptions || {}; - _.defaults(options, { sendReadReceipts: true }); - - const conversationId = this.id; - Whisper.Notifications.remove( - Whisper.Notifications.where({ - conversationId, - }) - ); - let unreadMessages = await this.getUnread(); - - const oldUnread = unreadMessages.filter( - message => message.get('received_at') <= newestUnreadDate - ); - - let read = await Promise.all( - _.map(oldUnread, async providedM => { - const m = getMessageController().register(providedM.id, providedM); - - await m.markRead(options.readAt); - const errors = m.get('errors'); - return { - sender: m.get('source'), - timestamp: m.get('sent_at'), - hasErrors: Boolean(errors && errors.length), - }; - }) - ); - - // Some messages we're marking read are local notifications with no sender - read = _.filter(read, m => Boolean(m.sender)); - const realUnreadCount = await this.getUnreadCount(); - if (read.length === 0) { - const cachedUnreadCountOnConvo = this.get('unreadCount'); - if (cachedUnreadCountOnConvo !== read.length) { - // reset the unreadCount on the convo to the real one coming from markRead messages on the db - this.set({ unreadCount: 0 }); - this.commit(); - } else { - // window.log.info('markRead(): nothing newly read.'); - } - return; - } - unreadMessages = unreadMessages.filter(m => Boolean(m.isIncoming())); - - this.set({ unreadCount: realUnreadCount }); - - const mentionRead = (() => { - const stillUnread = unreadMessages.filter( - m => m.get('received_at') > newestUnreadDate - ); - const ourNumber = textsecure.storage.user.getNumber(); - return !stillUnread.some( - m => - m.propsForMessage && - m.propsForMessage.text && - m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1 - ); - })(); - - if (mentionRead) { - this.set({ mentionedUs: false }); - } - - await this.commit(); - - // If a message has errors, we don't want to send anything out about it. - // read syncs - let's wait for a client that really understands the message - // to mark it read. we'll mark our local error read locally, though. - // read receipts - here we can run into infinite loops, where each time the - // conversation is viewed, another error message shows up for the contact - read = read.filter(item => !item.hasErrors); - - if (this.isPublic()) { - window.log.debug('public conversation... No need to send read receipt'); - return; - } - - if (this.isPrivate() && read.length && options.sendReadReceipts) { - window.log.info(`Sending ${read.length} read receipts`); - if (storage.get('read-receipt-setting')) { - await Promise.all( - _.map(_.groupBy(read, 'sender'), async (receipts, sender) => { - const timestamps = _.map(receipts, 'timestamp'); - const receiptMessage = new libsession.Messages.Outgoing.ReadReceiptMessage( - { - timestamp: Date.now(), - timestamps, - } - ); - - const device = new libsession.Types.PubKey(sender); - await libsession - .getMessageQueue() - .sendToPubKey(device, receiptMessage); - }) - ); - } - } - }, - - // LOKI PROFILES - async setNickname(nickname) { - const trimmed = nickname && nickname.trim(); - if (this.get('nickname') === trimmed) { - return; - } - - this.set({ nickname: trimmed }); - await this.commit(); - - await this.updateProfileName(); - }, - async setLokiProfile(newProfile) { - if (!_.isEqual(this.get('profile'), newProfile)) { - this.set({ profile: newProfile }); - await this.commit(); - } - - // a user cannot remove an avatar. Only change it - // if you change this behavior, double check all setLokiProfile calls (especially the one in EditProfileDialog) - if (newProfile.avatar) { - await this.setProfileAvatar({ path: newProfile.avatar }); - } - - await this.updateProfileName(); - }, - async updateProfileName() { - // Prioritise nickname over the profile display name - const nickname = this.getNickname(); - const profile = this.getLokiProfile(); - const displayName = profile && profile.displayName; - - const profileName = nickname || displayName || null; - await this.setProfileName(profileName); - }, - getLokiProfile() { - return this.get('profile'); - }, - getNickname() { - return this.get('nickname'); - }, - // maybe "Backend" instead of "Source"? - async setPublicSource(newServer, newChannelId) { - if (!this.isPublic()) { - log.warn( - `trying to setPublicSource on non public chat conversation ${this.id}` - ); - return; - } - if ( - this.get('server') !== newServer || - this.get('channelId') !== newChannelId - ) { - // mark active so it's not in the contacts list but in the conversation list - this.set({ - server: newServer, - channelId: newChannelId, - active_at: Date.now(), - }); - await this.commit(); - } - }, - getPublicSource() { - if (!this.isPublic()) { - log.warn( - `trying to getPublicSource on non public chat conversation ${this.id}` - ); - return null; - } - return { - server: this.get('server'), - channelId: this.get('channelId'), - conversationId: this.get('id'), - }; - }, - async getPublicSendData() { - const channelAPI = await lokiPublicChatAPI.findOrCreateChannel( - this.get('server'), - this.get('channelId'), - this.id - ); - return channelAPI; - }, - getLastRetrievedMessage() { - if (!this.isPublic()) { - return null; - } - const lastMessageId = this.get('lastPublicMessage') || 0; - return lastMessageId; - }, - async setLastRetrievedMessage(newLastMessageId) { - if (!this.isPublic()) { - return; - } - if (this.get('lastPublicMessage') !== newLastMessageId) { - this.set({ lastPublicMessage: newLastMessageId }); - await this.commit(); - } - }, - isAdmin(pubKey) { - if (!this.isPublic()) { - return false; - } - if (!pubKey) { - throw new Error('isAdmin() pubKey is falsy'); - } - const groupAdmins = this.getGroupAdmins(); - return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey); - }, - // SIGNAL PROFILES - getProfiles() { - // request all conversation members' keys - let ids = []; - if (this.isPrivate()) { - ids = [this.id]; - } else { - ids = this.get('members'); - } - return Promise.all(_.map(ids, this.getProfile)); - }, - - // This function is wrongly named by signal - // This is basically an `update` function and thus we have overwritten it with such - async getProfile(id) { - const c = await window - .getConversationController() - .getOrCreateAndWait(id, 'private'); - - // We only need to update the profile as they are all stored inside the conversation - await c.updateProfileName(); - }, - async setProfileName(name) { - const profileName = this.get('profileName'); - if (profileName !== name) { - this.set({ profileName: name }); - await this.commit(); - } - }, - async setGroupName(name) { - const profileName = this.get('name'); - if (profileName !== name) { - this.set({ name }); - await this.commit(); - } - }, - async setSubscriberCount(count) { - this.set({ subscriberCount: count }); - // Not sure if we care about updating the database - }, - async setGroupNameAndAvatar(name, avatarPath) { - const currentName = this.get('name'); - const profileAvatar = this.get('profileAvatar'); - if (profileAvatar !== avatarPath || currentName !== name) { - // only update changed items - if (profileAvatar !== avatarPath) { - this.set({ profileAvatar: avatarPath }); - } - if (currentName !== name) { - this.set({ name }); - } - // save - await this.commit(); - } - }, - async setProfileAvatar(avatar) { - const profileAvatar = this.get('profileAvatar'); - if (profileAvatar !== avatar) { - this.set({ profileAvatar: avatar }); - await this.commit(); - } - }, - async setProfileKey(profileKey) { - // profileKey is a string so we can compare it directly - if (this.get('profileKey') !== profileKey) { - this.set({ - profileKey, - accessKey: null, - }); - - await this.deriveAccessKeyIfNeeded(); - - await this.commit(); - } - }, - - async deriveAccessKeyIfNeeded() { - const profileKey = this.get('profileKey'); - if (!profileKey) { - return; - } - if (this.get('accessKey')) { - return; - } - - try { - const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer( - profileKey - ); - const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey( - profileKeyBuffer - ); - const accessKey = window.Signal.Crypto.arrayBufferToBase64( - accessKeyBuffer - ); - this.set({ accessKey }); - } catch (e) { - window.log.warn(`Failed to derive access key for ${this.id}`); - } - }, - - async upgradeMessages(messages) { - for (let max = messages.length, i = 0; i < max; i += 1) { - const message = messages.at(i); - const { attributes } = message; - const { schemaVersion } = attributes; - - if (schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY) { - // Yep, we really do want to wait for each of these - // eslint-disable-next-line no-await-in-loop - const upgradedMessage = await upgradeMessageSchema(attributes); - message.set(upgradedMessage); - // eslint-disable-next-line no-await-in-loop - await upgradedMessage.commit(); - } - } - }, - - hasMember(number) { - return _.contains(this.get('members'), number); - }, - // returns true if this is a closed/medium or open group - isGroup() { - return this.get('type') === 'group'; - }, - - copyPublicKey() { - clipboard.writeText(this.id); - - window.libsession.Utils.ToastUtils.pushCopiedToClipBoard(); - }, - - changeNickname() { - window.Whisper.events.trigger('showNicknameDialog', { - pubKey: this.id, - nickname: this.getNickname(), - onOk: newName => this.setNickname(newName), - }); - }, - - deleteContact() { - let title = i18n('delete'); - let message = i18n('deleteContactConfirmation'); - - if (this.isGroup()) { - title = i18n('leaveGroup'); - message = i18n('leaveGroupConfirmation'); - } - - window.confirmationDialog({ - title, - message, - resolve: () => { - window.getConversationController().deleteContact(this.id); - }, - }); - }, - - async deletePublicMessages(messages) { - const channelAPI = await this.getPublicSendData(); - - if (!channelAPI) { - log.error('Unable to get public channel API'); - return false; - } - - const invalidMessages = messages.filter(m => !m.attributes.serverId); - const pendingMessages = messages.filter(m => m.attributes.serverId); - - let deletedServerIds = []; - let ignoredServerIds = []; - - if (pendingMessages.length > 0) { - const result = await channelAPI.deleteMessages( - pendingMessages.map(m => m.attributes.serverId) - ); - deletedServerIds = result.deletedIds; - ignoredServerIds = result.ignoredIds; - } - - const toDeleteLocallyServerIds = _.union( - deletedServerIds, - ignoredServerIds - ); - let toDeleteLocally = messages.filter(m => - toDeleteLocallyServerIds.includes(m.attributes.serverId) - ); - toDeleteLocally = _.union(toDeleteLocally, invalidMessages); - - toDeleteLocally.forEach(m => this.removeMessage(m.id)); - - return toDeleteLocally; - }, - - removeMessage(messageId) { - window.Signal.Data.removeMessage(messageId, { - Message: Whisper.Message, - }); - window.Whisper.events.trigger('messageDeleted', { - conversationKey: this.id, - messageId, - }); - }, - - deleteMessages() { - let params; - if (this.isPublic()) { - throw new Error( - 'Called deleteMessages() on an open group. Only leave group is supported.' - ); - } else { - params = { - title: i18n('deleteMessages'), - message: i18n('deleteConversationConfirmation'), - resolve: () => this.destroyMessages(), - }; - } - - window.confirmationDialog(params); - }, - - async destroyMessages() { - await window.Signal.Data.removeAllMessagesInConversation(this.id, { - MessageCollection: Whisper.MessageCollection, - }); - - window.Whisper.events.trigger('conversationReset', { - conversationKey: this.id, - }); - // destroy message keeps the active timestamp set so the - // conversation still appears on the conversation list but is empty - this.set({ - lastMessage: null, - unreadCount: 0, - mentionedUs: false, - }); - - await this.commit(); - }, - - getName() { - if (this.isPrivate()) { - return this.get('name'); - } - return this.get('name') || i18n('unknown'); - }, - - getTitle() { - if (this.isPrivate()) { - const profileName = this.getProfileName(); - const number = this.getNumber(); - let name; - if (window.libsession) { - name = profileName - ? `${profileName} (${window.libsession.Types.PubKey.shorten( - number - )})` - : number; - } else { - name = profileName ? `${profileName} (${number})` : number; - } - return this.get('name') || name; - } - return this.get('name') || 'Unknown group'; - }, - - /** - * For a private convo, returns the loki profilename if set, or a shortened - * version of the contact pubkey. - * Throws an error if called on a group convo. - * */ - getContactProfileNameOrShortenedPubKey() { - if (!this.isPrivate()) { - throw new Error( - 'getContactProfileNameOrShortenedPubKey() cannot be called with a non private convo.' - ); - } - - const profileName = this.get('profileName'); - const pubkey = this.id; - if (pubkey === textsecure.storage.user.getNumber()) { - return i18n('you'); - } - return profileName || window.libsession.Types.PubKey.shorten(pubkey); - }, - - /** - * For a private convo, returns the loki profilename if set, or a full length - * version of the contact pubkey. - * Throws an error if called on a group convo. - * */ - getContactProfileNameOrFullPubKey() { - if (!this.isPrivate()) { - throw new Error( - 'getContactProfileNameOrFullPubKey() cannot be called with a non private convo.' - ); - } - const profileName = this.get('profileName'); - const pubkey = this.id; - if (pubkey === textsecure.storage.user.getNumber()) { - return i18n('you'); - } - return profileName || pubkey; - }, - - getProfileName() { - if (this.isPrivate() && !this.get('name')) { - return this.get('profileName'); - } - return null; - }, - - /** - * Returns - * displayName: string; - * avatarPointer: string; - * profileKey: Uint8Array; - */ - getOurProfile() { - try { - // Secondary devices have their profile stored - // in their primary device's conversation - const ourNumber = window.storage.get('primaryDevicePubKey'); - const ourConversation = window - .getConversationController() - .get(ourNumber); - let profileKey = null; - if (this.get('profileSharing')) { - profileKey = new Uint8Array(storage.get('profileKey')); - } - const avatarPointer = ourConversation.get('avatarPointer'); - const { displayName } = ourConversation.getLokiProfile(); - return { displayName, avatarPointer, profileKey }; - } catch (e) { - window.log.error(`Failed to get our profile: ${e}`); - return null; - } - }, - - getNumber() { - if (!this.isPrivate()) { - return ''; - } - return this.id; - }, - - isPrivate() { - return this.get('type') === 'private'; - }, - - getAvatarPath() { - const avatar = this.get('avatar') || this.get('profileAvatar'); - if (typeof avatar === 'string') { - return avatar; - } - - if (avatar && avatar.path && typeof avatar.path === 'string') { - return getAbsoluteAttachmentPath(avatar.path); - } - - return null; - }, - getAvatar() { - const url = this.getAvatarPath(); - - return { url: url || null }; - }, - - getNotificationIcon() { - return new Promise(resolve => { - const avatar = this.getAvatar(); - if (avatar.url) { - resolve(avatar.url); - } else { - resolve(new Whisper.IdenticonSVGView(avatar).getDataUrl()); - } - }); - }, - - notify(message) { - if (!message.isIncoming()) { - return Promise.resolve(); - } - const conversationId = this.id; - - return window - .getConversationController() - .getOrCreateAndWait(message.get('source'), 'private') - .then(sender => - sender.getNotificationIcon().then(iconUrl => { - const messageJSON = message.toJSON(); - const messageSentAt = messageJSON.sent_at; - const messageId = message.id; - const isExpiringMessage = Message.hasExpiration(messageJSON); - - // window.log.info('Add notification', { - // conversationId: this.idForLogging(), - // isExpiringMessage, - // messageSentAt, - // }); - Whisper.Notifications.add({ - conversationId, - iconUrl, - isExpiringMessage, - message: message.getNotificationText(), - messageId, - messageSentAt, - title: sender.getTitle(), - }); - }) - ); - }, - notifyTyping({ isTyping, sender }) { - // We don't do anything with typing messages from our other devices - if (sender === this.ourNumber) { - return; - } - - // For groups, block typing messages from non-members (e.g. from kicked members) - if (this.get('type') === 'group') { - const knownMembers = this.get('members'); - - if (knownMembers) { - const fromMember = knownMembers.includes(sender); - - if (!fromMember) { - window.log.warn( - 'Blocking typing messages from a non-member: ', - sender - ); - return; - } - } - } - - this.contactTypingTimers = this.contactTypingTimers || {}; - const record = this.contactTypingTimers[sender]; - - if (record) { - clearTimeout(record.timer); - } - - // Note: We trigger two events because: - // 'change' causes a re-render of this conversation's list item in the left pane - - if (isTyping) { - this.contactTypingTimers[sender] = this.contactTypingTimers[sender] || { - timestamp: Date.now(), - sender, - }; - - this.contactTypingTimers[sender].timer = setTimeout( - this.clearContactTypingTimer.bind(this, sender), - 15 * 1000 - ); - if (!record) { - // User was not previously typing before. State change! - this.commit(); - } - } else { - delete this.contactTypingTimers[sender]; - if (record) { - // User was previously typing, and is no longer. State change! - this.commit(); - } - } - }, - - clearContactTypingTimer(sender) { - this.contactTypingTimers = this.contactTypingTimers || {}; - const record = this.contactTypingTimers[sender]; - - if (record) { - clearTimeout(record.timer); - delete this.contactTypingTimers[sender]; - - // User was previously typing, but timed out or we received message. State change! - this.commit(); - } - }, - }); - - Whisper.ConversationCollection = Backbone.Collection.extend({ - model: Whisper.Conversation, - - comparator(m) { - return -m.get('timestamp'); - }, - - async destroyAll() { - await Promise.all( - this.models.map(conversation => - window.Signal.Data.removeConversation(conversation.id, { - Conversation: Whisper.Conversation, - }) - ) - ); - this.reset([]); - }, - }); -})(); diff --git a/js/models/messages.d.ts b/js/models/messages.d.ts deleted file mode 100644 index e4b83ffd31..0000000000 --- a/js/models/messages.d.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { LocalizerType } from '../../ts/types/Util'; -import { ConversationModel } from './conversations'; - -type MessageModelType = 'incoming' | 'outgoing'; -type MessageDeliveryStatus = - | 'sending' - | 'sent' - | 'delivered' - | 'read' - | 'error'; - -interface MessageAttributes { - id: number; - source: string; - quote: any; - expireTimer: number; - received_at: number; - sent_at: number; - preview: any; - body: string; - expirationStartTimestamp: any; - read_by: Array; - delivered_to: Array; - decrypted_at: number; - recipients: Array; - delivered: number; - type: MessageModelType; - group_update: any; - groupInvitation: any; - attachments: any; - contact: any; - conversationId: any; - errors: any; - flags: number; - hasAttachments: boolean; - hasFileAttachments: boolean; - hasVisualMediaAttachments: boolean; - schemaVersion: number; - expirationTimerUpdate: any; - unread: boolean; - group: any; - bodyPending: boolean; - timestamp: number; - status: MessageDeliveryStatus; -} - -export interface MessageRegularProps { - disableMenu?: boolean; - isDeletable: boolean; - isAdmin?: boolean; - weAreAdmin?: boolean; - text?: string; - bodyPending?: boolean; - id: string; - collapseMetadata?: boolean; - direction: 'incoming' | 'outgoing'; - timestamp: number; - serverTimestamp?: number; - status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error' | 'pow'; - // What if changed this over to a single contact like quote, and put the events on it? - contact?: Contact & { - onSendMessage?: () => void; - onClick?: () => void; - }; - authorName?: string; - authorProfileName?: string; - /** Note: this should be formatted for display */ - authorPhoneNumber: string; - conversationType: 'group' | 'direct'; - attachments?: Array; - quote?: { - text: string; - attachment?: QuotedAttachmentType; - isFromMe: boolean; - authorPhoneNumber: string; - authorProfileName?: string; - authorName?: string; - messageId?: string; - onClick: (data: any) => void; - referencedMessageNotFound: boolean; - }; - previews: Array; - authorAvatarPath?: string; - isExpired: boolean; - expirationLength?: number; - expirationTimestamp?: number; - convoId: string; - isPublic?: boolean; - selected: boolean; - isKickedFromGroup: boolean; - // whether or not to show check boxes - multiSelectMode: boolean; - firstMessageOfSeries: boolean; - isUnread: boolean; - isQuotedMessageToAnimate?: boolean; - - onClickAttachment?: (attachment: AttachmentType) => void; - onClickLinkPreview?: (url: string) => void; - onCopyText?: () => void; - onSelectMessage: (messageId: string) => void; - onReply?: (messagId: number) => void; - onRetrySend?: () => void; - onDownload?: (attachment: AttachmentType) => void; - onDeleteMessage: (messageId: string) => void; - onCopyPubKey?: () => void; - onBanUser?: () => void; - onShowDetail: () => void; - onShowUserDetails: (userPubKey: string) => void; - markRead: (readAt: number) => Promise; - theme: DefaultTheme; -} - -export interface MessageModel extends Backbone.Model { - idForLogging: () => string; - isGroupUpdate: () => boolean; - isExpirationTimerUpdate: () => boolean; - getNotificationText: () => string; - markRead: () => void; - merge: (other: MessageModel) => void; - saveErrors: (error: any) => void; - sendSyncMessageOnly: (message: any) => void; - isUnread: () => boolean; - commit: () => Promise; - getPropsForMessageDetail: () => any; - getConversation: () => ConversationModel; - handleMessageSentSuccess: (sentMessage: any, wrappedEnvelope: any) => any; - handleMessageSentFailure: (sentMessage: any, error: any) => any; - - propsForMessage?: MessageRegularProps; - propsForTimerNotification?: any; - propsForGroupInvitation?: any; - propsForGroupNotification?: any; - firstMessageOfSeries: boolean; -} diff --git a/js/models/messages.js b/js/models/messages.js deleted file mode 100644 index c4d3e80eb2..0000000000 --- a/js/models/messages.js +++ /dev/null @@ -1,1492 +0,0 @@ -/* global - _, - Backbone, - storage, - filesize, - getMessageController, - i18n, - Signal, - textsecure, - Whisper, - clipboard, - libsession -*/ - -/* eslint-disable more/no-then */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - const { Message: TypedMessage, Contact } = Signal.Types; - - const { - deleteExternalMessageFiles, - getAbsoluteAttachmentPath, - loadAttachmentData, - loadQuoteData, - loadPreviewData, - } = window.Signal.Migrations; - const { bytesFromString } = window.Signal.Crypto; - - window.AccountCache = Object.create(null); - window.AccountJobs = Object.create(null); - - window.doesAcountCheckJobExist = number => - Boolean(window.AccountJobs[number]); - - window.Whisper.Message = Backbone.Model.extend({ - initialize(attributes) { - if (_.isObject(attributes)) { - this.set( - TypedMessage.initializeSchemaVersion({ - message: attributes, - logger: window.log, - }) - ); - } - - this.on('destroy', this.onDestroy); - this.on('change:expirationStartTimestamp', this.setToExpire); - this.on('change:expireTimer', this.setToExpire); - this.on('expired', this.onExpired); - this.setToExpire(); - // Keep props ready - const generateProps = (triggerEvent = true) => { - if (this.isExpirationTimerUpdate()) { - this.propsForTimerNotification = this.getPropsForTimerNotification(); - } else if (this.isGroupUpdate()) { - this.propsForGroupNotification = this.getPropsForGroupNotification(); - } else if (this.isGroupInvitation()) { - this.propsForGroupInvitation = this.getPropsForGroupInvitation(); - } else { - this.propsForSearchResult = this.getPropsForSearchResult(); - this.propsForMessage = this.getPropsForMessage(); - } - if (triggerEvent) { - Whisper.events.trigger('messageChanged', this); - } - }; - this.on('change', generateProps); - - // const applicableConversationChanges = - // 'change:color change:name change:number change:profileName change:profileAvatar'; - // FIXME AUDRIC - // const conversation = this.getConversation(); - // const fromContact = this.getIncomingContact(); - // this.listenTo(conversation, applicableConversationChanges, generateProps); - - // if (fromContact) { - // this.listenTo( - // fromContact, - // applicableConversationChanges, - // generateProps - // ); - // } - - window.contextMenuShown = false; - - generateProps(false); - }, - idForLogging() { - return `${this.get('source')}.${this.get('sourceDevice')} ${this.get( - 'sent_at' - )}`; - }, - defaults() { - return { - timestamp: new Date().getTime(), - attachments: [], - sent: false, - }; - }, - validate(attributes) { - const required = ['conversationId', 'received_at', 'sent_at']; - const missing = _.filter(required, attr => !attributes[attr]); - if (missing.length) { - window.log.warn(`Message missing attributes: ${missing}`); - } - }, - isExpirationTimerUpdate() { - const expirationTimerFlag = - textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; - // eslint-disable-next-line no-bitwise - return !!(this.get('flags') & expirationTimerFlag); - }, - isGroupUpdate() { - return !!this.get('group_update'); - }, - isIncoming() { - return this.get('type') === 'incoming'; - }, - isUnread() { - return !!this.get('unread'); - }, - // Important to allow for this.unset('unread'), save to db, then fetch() - // to propagate. We don't want the unset key in the db so our unread index - // stays small. - merge(model) { - const attributes = model.attributes || model; - - const { unread } = attributes; - if (typeof unread === 'undefined') { - this.unset('unread'); - } - - this.set(attributes); - }, - getDescription() { - if (this.isGroupUpdate()) { - const groupUpdate = this.get('group_update'); - const ourPrimary = window.textsecure.storage.get('primaryDevicePubKey'); - if ( - groupUpdate.left === 'You' || - (Array.isArray(groupUpdate.left) && - groupUpdate.left.length === 1 && - groupUpdate.left[0] === ourPrimary) - ) { - return i18n('youLeftTheGroup'); - } else if (groupUpdate.left) { - return i18n( - 'leftTheGroup', - window - .getConversationController() - .getContactProfileNameOrShortenedPubKey(groupUpdate.left) - ); - } - - if (groupUpdate.kicked === 'You') { - return i18n('youGotKickedFromGroup'); - } - - const messages = []; - if (!groupUpdate.name && !groupUpdate.joined && !groupUpdate.kicked) { - messages.push(i18n('updatedTheGroup')); - } - if (groupUpdate.name) { - messages.push(i18n('titleIsNow', groupUpdate.name)); - } - if (groupUpdate.joined && groupUpdate.joined.length) { - const names = groupUpdate.joined.map(pubKey => - window - .getConversationController() - .getContactProfileNameOrFullPubKey(pubKey) - ); - - if (names.length > 1) { - messages.push(i18n('multipleJoinedTheGroup', names.join(', '))); - } else { - messages.push(i18n('joinedTheGroup', names[0])); - } - } - - if (groupUpdate.kicked && groupUpdate.kicked.length) { - const names = _.map( - groupUpdate.kicked, - window.getConversationController() - .getContactProfileNameOrShortenedPubKey - ); - - if (names.length > 1) { - messages.push(i18n('multipleKickedFromTheGroup', names.join(', '))); - } else { - messages.push(i18n('kickedFromTheGroup', names[0])); - } - } - return messages.join(' '); - } - if (this.isIncoming() && this.hasErrors()) { - return i18n('incomingError'); - } - if (this.isGroupInvitation()) { - return `<${i18n('groupInvitation')}>`; - } - return this.get('body'); - }, - - isKeyChange() { - return this.get('type') === 'keychange'; - }, - isGroupInvitation() { - return !!this.get('groupInvitation'); - }, - getNotificationText() { - let description = this.getDescription(); - if (description) { - // regex with a 'g' to ignore part groups - const regex = new RegExp( - `@${window.libsession.Types.PubKey.regexForPubkeys}`, - 'g' - ); - const pubkeysInDesc = description.match(regex); - (pubkeysInDesc || []).forEach(pubkey => { - const displayName = window - .getConversationController() - .getContactProfileNameOrShortenedPubKey(pubkey.slice(1)); - if (displayName && displayName.length) { - description = description.replace(pubkey, `@${displayName}`); - } - }); - return description; - } - if (this.get('attachments').length > 0) { - return i18n('mediaMessage'); - } - if (this.isExpirationTimerUpdate()) { - const { expireTimer } = this.get('expirationTimerUpdate'); - if (!expireTimer) { - return i18n('disappearingMessagesDisabled'); - } - - return i18n( - 'timerSetTo', - Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0) - ); - } - if (this.isKeyChange()) { - const phoneNumber = this.get('key_changed'); - const conversation = this.findContact(phoneNumber); - return i18n( - 'safetyNumberChangedGroup', - conversation ? conversation.getTitle() : null - ); - } - const contacts = this.get('contact'); - if (contacts && contacts.length) { - return Contact.getName(contacts[0]); - } - - return ''; - }, - onDestroy() { - this.cleanup(); - }, - async cleanup() { - getMessageController().unregister(this.id); - await deleteExternalMessageFiles(this.attributes); - }, - onExpired() { - this.hasExpired = true; - }, - getPropsForTimerNotification() { - const timerUpdate = this.get('expirationTimerUpdate'); - if (!timerUpdate) { - return null; - } - - const { expireTimer, fromSync, source } = timerUpdate; - const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0); - const disabled = !expireTimer; - - const basicProps = { - type: 'fromOther', - ...this.findAndFormatContact(source), - timespan, - disabled, - }; - - if (fromSync) { - return { - ...basicProps, - type: 'fromSync', - }; - } else if (source === textsecure.storage.user.getNumber()) { - return { - ...basicProps, - type: 'fromMe', - }; - } - - return basicProps; - }, - getPropsForGroupInvitation() { - const invitation = this.get('groupInvitation'); - - let direction = this.get('direction'); - if (!direction) { - direction = this.get('type') === 'outgoing' ? 'outgoing' : 'incoming'; - } - - return { - serverName: invitation.serverName, - serverAddress: invitation.serverAddress, - direction, - onClick: () => { - Whisper.events.trigger( - 'publicChatInvitationAccepted', - invitation.serverAddress, - invitation.channelId - ); - }, - }; - }, - findContact(phoneNumber) { - return window.getConversationController().get(phoneNumber); - }, - findAndFormatContact(phoneNumber) { - const contactModel = this.findContact(phoneNumber); - let profileName; - if (phoneNumber === window.storage.get('primaryDevicePubKey')) { - profileName = i18n('you'); - } else { - profileName = contactModel ? contactModel.getProfileName() : null; - } - - return { - phoneNumber, - color: null, - avatarPath: contactModel ? contactModel.getAvatarPath() : null, - name: contactModel ? contactModel.getName() : null, - profileName, - title: contactModel ? contactModel.getTitle() : null, - }; - }, - getPropsForGroupNotification() { - const groupUpdate = this.get('group_update'); - const changes = []; - - if (!groupUpdate.name && !groupUpdate.left && !groupUpdate.joined) { - changes.push({ - type: 'general', - }); - } - - if (groupUpdate.joined) { - changes.push({ - type: 'add', - contacts: _.map( - Array.isArray(groupUpdate.joined) - ? groupUpdate.joined - : [groupUpdate.joined], - phoneNumber => this.findAndFormatContact(phoneNumber) - ), - }); - } - - if (groupUpdate.kicked === 'You') { - changes.push({ - type: 'kicked', - isMe: true, - }); - } else if (groupUpdate.kicked) { - changes.push({ - type: 'kicked', - contacts: _.map( - Array.isArray(groupUpdate.kicked) - ? groupUpdate.kicked - : [groupUpdate.kicked], - phoneNumber => this.findAndFormatContact(phoneNumber) - ), - }); - } - - if (groupUpdate.left === 'You') { - changes.push({ - type: 'remove', - isMe: true, - }); - } else if (groupUpdate.left) { - if ( - Array.isArray(groupUpdate.left) && - groupUpdate.left.length === 1 && - groupUpdate.left[0] === textsecure.storage.user.getNumber() - ) { - changes.push({ - type: 'remove', - isMe: true, - }); - } else { - changes.push({ - type: 'remove', - contacts: _.map( - Array.isArray(groupUpdate.left) - ? groupUpdate.left - : [groupUpdate.left], - phoneNumber => this.findAndFormatContact(phoneNumber) - ), - }); - } - } - - if (groupUpdate.name) { - changes.push({ - type: 'name', - newName: groupUpdate.name, - }); - } - - return { - changes, - }; - }, - getMessagePropStatus() { - if (this.hasErrors()) { - return 'error'; - } - - // Only return the status on outgoing messages - if (!this.isOutgoing()) { - return null; - } - - const readBy = this.get('read_by') || []; - if (storage.get('read-receipt-setting') && readBy.length > 0) { - return 'read'; - } - const delivered = this.get('delivered'); - const deliveredTo = this.get('delivered_to') || []; - if (delivered || deliveredTo.length > 0) { - return 'delivered'; - } - const sent = this.get('sent'); - const sentTo = this.get('sent_to') || []; - if (sent || sentTo.length > 0) { - return 'sent'; - } - const calculatingPoW = this.get('calculatingPoW'); - if (calculatingPoW) { - return 'pow'; - } - - return 'sending'; - }, - getPropsForSearchResult() { - const fromNumber = this.getSource(); - const from = this.findAndFormatContact(fromNumber); - if (fromNumber === textsecure.storage.user.getNumber()) { - from.isMe = true; - } - - const toNumber = this.get('conversationId'); - let to = this.findAndFormatContact(toNumber); - if (toNumber === textsecure.storage.user.getNumber()) { - to.isMe = true; - } else if (fromNumber === toNumber) { - to = { - isMe: true, - }; - } - - return { - from, - to, - - isSelected: this.isSelected, - - id: this.id, - conversationId: this.get('conversationId'), - receivedAt: this.get('received_at'), - snippet: this.get('snippet'), - }; - }, - getPropsForMessage(options) { - const phoneNumber = this.getSource(); - const contact = this.findAndFormatContact(phoneNumber); - const contactModel = this.findContact(phoneNumber); - - const authorAvatarPath = contactModel - ? contactModel.getAvatarPath() - : null; - - const expirationLength = this.get('expireTimer') * 1000; - const expireTimerStart = this.get('expirationStartTimestamp'); - const expirationTimestamp = - expirationLength && expireTimerStart - ? expireTimerStart + expirationLength - : null; - - // TODO: investigate why conversation is undefined - // for the public group chat - const conversation = this.getConversation(); - - const convoId = conversation ? conversation.id : undefined; - const isGroup = !!conversation && !conversation.isPrivate(); - const isPublic = !!this.get('isPublic'); - - const attachments = this.get('attachments') || []; - - return { - text: this.createNonBreakingLastSeparator(this.get('body')), - bodyPending: this.get('bodyPending'), - id: this.id, - direction: this.isIncoming() ? 'incoming' : 'outgoing', - timestamp: this.get('sent_at'), - serverTimestamp: this.get('serverTimestamp'), - status: this.getMessagePropStatus(), - authorName: contact.name, - authorProfileName: contact.profileName, - authorPhoneNumber: contact.phoneNumber, - conversationType: isGroup ? 'group' : 'direct', - convoId, - attachments: attachments - .filter(attachment => !attachment.error) - .map(attachment => this.getPropsForAttachment(attachment)), - previews: this.getPropsForPreview(), - quote: this.getPropsForQuote(options), - authorAvatarPath, - isExpired: this.hasExpired, - isUnread: this.isUnread(), - expirationLength, - expirationTimestamp, - isPublic, - isKickedFromGroup: - conversation && conversation.get('isKickedFromGroup'), - - onCopyText: () => this.copyText(), - onCopyPubKey: () => this.copyPubKey(), - onBanUser: () => this.banUser(), - onRetrySend: () => this.retrySend(), - markRead: readAt => this.markRead(readAt), - - onShowUserDetails: pubkey => - window.Whisper.events.trigger('onShowUserDetails', { - userPubKey: pubkey, - }), - }; - }, - createNonBreakingLastSeparator(text) { - if (!text) { - return null; - } - - const nbsp = '\xa0'; - const regex = /(\S)( +)(\S+\s*)$/; - return text.replace(regex, (match, start, spaces, end) => { - const newSpaces = - end.length < 12 - ? _.reduce(spaces, accumulator => accumulator + nbsp, '') - : spaces; - return `${start}${newSpaces}${end}`; - }); - }, - processQuoteAttachment(attachment) { - const { thumbnail } = attachment; - const path = - thumbnail && - thumbnail.path && - getAbsoluteAttachmentPath(thumbnail.path); - const objectUrl = thumbnail && thumbnail.objectUrl; - - const thumbnailWithObjectUrl = - !path && !objectUrl - ? null - : Object.assign({}, attachment.thumbnail || {}, { - objectUrl: path || objectUrl, - }); - - return Object.assign({}, attachment, { - isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment), - thumbnail: thumbnailWithObjectUrl, - }); - }, - getPropsForPreview() { - // Don't generate link previews if user has turned them off - if (!storage.get('link-preview-setting', false)) { - return null; - } - - const previews = this.get('preview') || []; - - return previews.map(preview => { - let image = null; - try { - if (preview.image) { - image = this.getPropsForAttachment(preview.image); - } - } catch (e) { - window.log.info('Failed to show preview'); - } - - return { - ...preview, - domain: window.Signal.LinkPreviews.getDomain(preview.url), - image, - }; - }); - }, - getPropsForQuote(options = {}) { - const { noClick } = options; - const quote = this.get('quote'); - - if (!quote) { - return null; - } - - const { author, id, referencedMessageNotFound } = quote; - const contact = author && window.getConversationController().get(author); - - const authorName = contact ? contact.getName() : null; - const isFromMe = contact - ? contact.id === textsecure.storage.user.getNumber() - : false; - const onClick = noClick - ? null - : event => { - event.stopPropagation(); - this.trigger('scroll-to-message', { - author, - id, - referencedMessageNotFound, - }); - }; - - const firstAttachment = quote.attachments && quote.attachments[0]; - - return { - text: this.createNonBreakingLastSeparator(quote.text), - attachment: firstAttachment - ? this.processQuoteAttachment(firstAttachment) - : null, - isFromMe, - authorPhoneNumber: author, - messageId: id, - authorName, - onClick, - referencedMessageNotFound, - }; - }, - getPropsForAttachment(attachment) { - if (!attachment) { - return null; - } - - const { path, pending, flags, size, screenshot, thumbnail } = attachment; - - return { - ...attachment, - fileSize: size ? filesize(size) : null, - isVoiceMessage: - flags && - // eslint-disable-next-line no-bitwise - flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE, - pending, - url: path ? getAbsoluteAttachmentPath(path) : null, - screenshot: screenshot - ? { - ...screenshot, - url: getAbsoluteAttachmentPath(screenshot.path), - } - : null, - thumbnail: thumbnail - ? { - ...thumbnail, - url: getAbsoluteAttachmentPath(thumbnail.path), - } - : null, - }; - }, - async getPropsForMessageDetail() { - const newIdentity = i18n('newIdentity'); - const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; - - // We include numbers we didn't successfully send to so we can display errors. - // Older messages don't have the recipients included on the message, so we fall - // back to the conversation's current recipients - const phoneNumbers = this.isIncoming() - ? [this.get('source')] - : _.union( - this.get('sent_to') || [], - this.get('recipients') || this.getConversation().getRecipients() - ); - - // This will make the error message for outgoing key errors a bit nicer - const allErrors = (this.get('errors') || []).map(error => { - if (error.name === OUTGOING_KEY_ERROR) { - // eslint-disable-next-line no-param-reassign - error.message = newIdentity; - } - - return error; - }); - - // If an error has a specific number it's associated with, we'll show it next to - // that contact. Otherwise, it will be a standalone entry. - const errors = _.reject(allErrors, error => Boolean(error.number)); - const errorsGroupedById = _.groupBy(allErrors, 'number'); - const finalContacts = await Promise.all( - (phoneNumbers || []).map(async id => { - const errorsForContact = errorsGroupedById[id]; - const isOutgoingKeyError = Boolean( - _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) - ); - - const contact = this.findAndFormatContact(id); - return { - ...contact, - // fallback to the message status if we do not have a status with a user - // this is useful for medium groups. - status: this.getStatus(id) || this.getMessagePropStatus(), - errors: errorsForContact, - isOutgoingKeyError, - isPrimaryDevice: true, - profileName: contact.profileName, - }; - }) - ); - - // The prefix created here ensures that contacts with errors are listed - // first; otherwise it's alphabetical - const sortedContacts = _.sortBy( - finalContacts, - contact => - `${contact.isPrimaryDevice ? '0' : '1'}${contact.phoneNumber}` - ); - - return { - sentAt: this.get('sent_at'), - receivedAt: this.get('received_at'), - message: { - ...this.propsForMessage, - disableMenu: true, - // To ensure that group avatar doesn't show up - conversationType: 'direct', - }, - errors, - contacts: sortedContacts, - }; - }, - - copyPubKey() { - if (this.isIncoming()) { - clipboard.writeText(this.get('source')); - } else { - clipboard.writeText(textsecure.storage.user.getNumber()); - } - - window.libsession.Utils.ToastUtils.pushCopiedToClipBoard(); - }, - - banUser() { - window.confirmationDialog({ - title: i18n('banUser'), - message: i18n('banUserConfirm'), - resolve: async () => { - const source = this.get('source'); - const conversation = this.getConversation(); - - const channelAPI = await conversation.getPublicSendData(); - const success = await channelAPI.banUser(source); - - if (success) { - window.libsession.Utils.ToastUtils.pushUserBanSuccess(); - } else { - window.libsession.Utils.ToastUtils.pushUserBanFailure(); - } - }, - }); - }, - - copyText() { - clipboard.writeText(this.get('body')); - - window.libsession.Utils.ToastUtils.pushCopiedToClipBoard(); - }, - - /** - * Uploads attachments, previews and quotes. - * If body is too long then it is also converted to an attachment. - * - * @returns The uploaded data which includes: body, attachments, preview and quote. - */ - async uploadData() { - // TODO: In the future it might be best if we cache the upload results if possible. - // This way we don't upload duplicated data. - - const attachmentsWithData = await Promise.all( - (this.get('attachments') || []).map(loadAttachmentData) - ); - const { - body, - attachments: finalAttachments, - } = Whisper.Message.getLongMessageAttachment({ - body: this.get('body'), - attachments: attachmentsWithData, - now: this.get('sent_at'), - }); - const filenameOverridenAttachments = finalAttachments.map(attachment => ({ - ...attachment, - fileName: Signal.Types.Attachment.getSuggestedFilenameSending({ - attachment, - timestamp: Date.now(), - }), - })); - - const quoteWithData = await loadQuoteData(this.get('quote')); - const previewWithData = await loadPreviewData(this.get('preview')); - - const conversation = this.getConversation(); - const openGroup = conversation && conversation.toOpenGroup(); - - const { AttachmentUtils } = libsession.Utils; - const [attachments, preview, quote] = await Promise.all([ - AttachmentUtils.uploadAttachments( - filenameOverridenAttachments, - openGroup - ), - AttachmentUtils.uploadLinkPreviews(previewWithData, openGroup), - AttachmentUtils.uploadQuoteThumbnails(quoteWithData, openGroup), - ]); - - return { - body, - attachments, - preview, - quote, - }; - }, - - // One caller today: event handler for the 'Retry Send' entry in triple-dot menu - async retrySend() { - if (!textsecure.messaging) { - window.log.error('retrySend: Cannot retry since we are offline!'); - return null; - } - - this.set({ errors: null }); - await this.commit(); - try { - const conversation = this.getConversation(); - const intendedRecipients = this.get('recipients') || []; - const successfulRecipients = this.get('sent_to') || []; - const currentRecipients = conversation.getRecipients(); - - if (conversation.isPublic()) { - const openGroup = { - server: conversation.get('server'), - channel: conversation.get('channelId'), - conversationId: conversation.id, - }; - const { body, attachments, preview, quote } = await this.uploadData(); - - const openGroupParams = { - identifier: this.id, - body, - timestamp: Date.now(), - group: openGroup, - attachments, - preview, - quote, - }; - const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( - openGroupParams - ); - return libsession.getMessageQueue().sendToGroup(openGroupMessage); - } - - let recipients = _.intersection(intendedRecipients, currentRecipients); - recipients = recipients.filter( - key => !successfulRecipients.includes(key) - ); - - if (!recipients.length) { - window.log.warn('retrySend: Nobody to send to!'); - - return this.commit(); - } - - const { body, attachments, preview, quote } = await this.uploadData(); - const ourNumber = window.storage.get('primaryDevicePubKey'); - const ourConversation = window - .getConversationController() - .get(ourNumber); - - const chatParams = { - identifier: this.id, - body, - timestamp: this.get('sent_at'), - expireTimer: this.get('expireTimer'), - attachments, - preview, - quote, - }; - if (ourConversation) { - chatParams.lokiProfile = ourConversation.getOurProfile(); - } - - const chatMessage = new libsession.Messages.Outgoing.ChatMessage( - chatParams - ); - - // Special-case the self-send case - we send only a sync message - if (recipients.length === 1) { - const isOurDevice = await libsession.Utils.UserUtils.isUs( - recipients[0] - ); - if (isOurDevice) { - return this.sendSyncMessageOnly(chatMessage); - } - } - - if (conversation.isPrivate()) { - const [number] = recipients; - const recipientPubKey = new libsession.Types.PubKey(number); - - return libsession - .getMessageQueue() - .sendToPubKey(recipientPubKey, chatMessage); - } - - // TODO should we handle medium groups message here too? - // Not sure there is the concept of retrySend for those - const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( - { - identifier: this.id, - chatMessage, - groupId: this.get('conversationId'), - } - ); - // Because this is a partial group send, we send the message with the groupId field set, but individually - // to each recipient listed - return Promise.all( - recipients.map(async r => { - const recipientPubKey = new libsession.Types.PubKey(r); - return libsession - .getMessageQueue() - .sendToPubKey(recipientPubKey, closedGroupChatMessage); - }) - ); - } catch (e) { - await this.saveErrors(e); - return null; - } - }, - - // Called when the user ran into an error with a specific user, wants to send to them - async resend(number) { - const error = this.removeOutgoingErrors(number); - if (!error) { - window.log.warn('resend: requested number was not present in errors'); - return null; - } - - try { - const { body, attachments, preview, quote } = await this.uploadData(); - - const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ - identifier: this.id, - body, - timestamp: this.get('sent_at'), - expireTimer: this.get('expireTimer'), - attachments, - preview, - quote, - }); - - // Special-case the self-send case - we send only a sync message - if (number === textsecure.storage.user.getNumber()) { - return this.sendSyncMessageOnly(chatMessage); - } - - const conversation = this.getConversation(); - const recipientPubKey = new libsession.Types.PubKey(number); - - if (conversation.isPrivate()) { - return libsession - .getMessageQueue() - .sendToPubKey(recipientPubKey, chatMessage); - } - - const closedGroupChatMessage = new libsession.Messages.Outgoing.ClosedGroupChatMessage( - { - chatMessage, - groupId: this.get('conversationId'), - } - ); - // resend tries to send the message to that specific user only in the context of a closed group - return libsession - .getMessageQueue() - .sendToPubKey(recipientPubKey, closedGroupChatMessage); - } catch (e) { - await this.saveErrors(e); - return null; - } - }, - removeOutgoingErrors(number) { - const errors = _.partition( - this.get('errors'), - e => - e.number === number && - (e.name === 'MessageError' || - e.name === 'SendMessageNetworkError' || - e.name === 'OutgoingIdentityKeyError') - ); - this.set({ errors: errors[1] }); - return errors[0][0]; - }, - - /** - * This function is called by inbox_view.js when a message was successfully sent for one device. - * So it might be called several times for the same message - */ - async handleMessageSentSuccess(sentMessage, wrappedEnvelope) { - let sentTo = this.get('sent_to') || []; - - let isOurDevice = false; - if (sentMessage.device) { - isOurDevice = await window.libsession.Utils.UserUtils.isUs( - sentMessage.device - ); - } - // FIXME this is not correct and will cause issues with syncing - // At this point the only way to check for medium - // group is by comparing the encryption type - const isClosedGroupMessage = - sentMessage.encryption === libsession.Types.EncryptionType.ClosedGroup; - - const isOpenGroupMessage = - !!sentMessage.group && - sentMessage.group instanceof libsession.Types.OpenGroup; - - // We trigger a sync message only when the message is not to one of our devices, AND - // the message is not for an open group (there is no sync for opengroups, each device pulls all messages), AND - // if we did not sync or trigger a sync message for this specific message already - const shouldTriggerSyncMessage = - !isOurDevice && - !isClosedGroupMessage && - !this.get('synced') && - !this.get('sentSync'); - - // A message is synced if we triggered a sync message (sentSync) - // and the current message was sent to our device (so a sync message) - const shouldMarkMessageAsSynced = isOurDevice && this.get('sentSync'); - - const isSessionOrClosedMessage = !isOpenGroupMessage; - - if (isSessionOrClosedMessage) { - const contentDecoded = textsecure.protobuf.Content.decode( - sentMessage.plainTextBuffer - ); - const { dataMessage } = contentDecoded; - - /** - * We should hit the notify endpoint for push notification only if: - * • It's a one-to-one chat or a closed group - * • The message has either text or attachments - */ - const hasBodyOrAttachments = Boolean( - dataMessage && - (dataMessage.body || - (dataMessage.attachments && dataMessage.attachments.length)) - ); - const shouldNotifyPushServer = - hasBodyOrAttachments && isSessionOrClosedMessage; - - if (shouldNotifyPushServer) { - // notify the push notification server if needed - if (!wrappedEnvelope) { - window.log.warn( - 'Should send PN notify but no wrapped envelope set.' - ); - } else { - if (!window.LokiPushNotificationServer) { - window.LokiPushNotificationServer = new window.LokiPushNotificationServerApi(); - } - - window.LokiPushNotificationServer.notify( - wrappedEnvelope, - sentMessage.device - ); - } - } - - // Handle the sync logic here - if (shouldTriggerSyncMessage) { - if (dataMessage) { - await this.sendSyncMessage(dataMessage); - } - } else if (shouldMarkMessageAsSynced) { - this.set({ synced: true }); - } - - sentTo = _.union(sentTo, [sentMessage.device]); - } - - this.set({ - sent_to: sentTo, - sent: true, - expirationStartTimestamp: Date.now(), - }); - - await this.commit(); - - this.getConversation().updateLastMessage(); - - this.trigger('sent', this); - }, - - async handleMessageSentFailure(sentMessage, error) { - if (error instanceof Error) { - this.saveErrors(error); - if (error.name === 'OutgoingIdentityKeyError') { - const c = window.getConversationController().get(sentMessage.device); - await c.getProfiles(); - } - } - let isOurDevice = false; - if (sentMessage.device) { - isOurDevice = await window.libsession.Utils.UserUtils.isUs( - sentMessage.device - ); - } - - const expirationStartTimestamp = Date.now(); - if (isOurDevice && !this.get('sync')) { - this.set({ sentSync: false }); - } - this.set({ - sent: true, - expirationStartTimestamp, - }); - await this.commit(); - - this.getConversation().updateLastMessage(); - this.trigger('done'); - }, - - getConversation() { - // This needs to be an unsafe call, because this method is called during - // initial module setup. We may be in the middle of the initial fetch to - // the database. - return window - .getConversationController() - .getUnsafe(this.get('conversationId')); - }, - getSourceDeviceConversation() { - // This gets the conversation of the device that sent this message - // while getConversation will return the primary device conversation - return window - .getConversationController() - .getOrCreateAndWait(this.get('source'), 'private'); - }, - getIncomingContact() { - if (!this.isIncoming()) { - return null; - } - const source = this.get('source'); - if (!source) { - return null; - } - - const key = source.key ? source.key : source; - - return window.getConversationController().getOrCreate(key, 'private'); - }, - getQuoteContact() { - const quote = this.get('quote'); - if (!quote) { - return null; - } - const { author } = quote; - if (!author) { - return null; - } - - return window.getConversationController().get(author); - }, - - getSource() { - if (this.isIncoming()) { - return this.get('source'); - } - - return textsecure.storage.user.getNumber(); - }, - getContact() { - const source = this.getSource(); - - if (!source) { - return null; - } - - // TODO: remove this when we are certain that source - // is PubKey ano not a string - const sourceStr = source.key ? source.key : source; - - return window - .getConversationController() - .getOrCreate(sourceStr, 'private'); - }, - isOutgoing() { - return this.get('type') === 'outgoing'; - }, - hasErrors() { - return _.size(this.get('errors')) > 0; - }, - - getStatus(number) { - const readBy = this.get('read_by') || []; - if (readBy.indexOf(number) >= 0) { - return 'read'; - } - const deliveredTo = this.get('delivered_to') || []; - if (deliveredTo.indexOf(number) >= 0) { - return 'delivered'; - } - const sentTo = this.get('sent_to') || []; - if (sentTo.indexOf(number) >= 0) { - return 'sent'; - } - - return null; - }, - async setCalculatingPoW() { - if (this.get('calculatingPoW')) { - return; - } - - this.set({ - calculatingPoW: true, - }); - - await this.commit(); - }, - async setServerId(serverId) { - if (_.isEqual(this.get('serverId'), serverId)) { - return; - } - - this.set({ - serverId, - }); - - await this.commit(); - }, - async setServerTimestamp(serverTimestamp) { - if (_.isEqual(this.get('serverTimestamp'), serverTimestamp)) { - return; - } - - this.set({ - serverTimestamp, - }); - - await this.commit(); - }, - async setIsPublic(isPublic) { - if (_.isEqual(this.get('isPublic'), isPublic)) { - return; - } - - this.set({ - isPublic: !!isPublic, - }); - - await this.commit(); - }, - - async sendSyncMessageOnly(dataMessage) { - this.set({ - sent_to: [textsecure.storage.user.getNumber()], - sent: true, - expirationStartTimestamp: Date.now(), - }); - - await this.commit(); - - const data = - dataMessage instanceof libsession.Messages.Outgoing.DataMessage - ? dataMessage.dataProto() - : dataMessage; - await this.sendSyncMessage(data); - }, - - async sendSyncMessage(/* dataMessage */) { - if (this.get('synced') || this.get('sentSync')) { - return; - } - - window.log.error( - 'sendSyncMessage to upgrade to multi device protocol v2' - ); - - // const data = - // dataMessage instanceof libsession.Messages.Outgoing.DataMessage - // ? dataMessage.dataProto() - // : dataMessage; - - // const syncMessage = new libsession.Messages.Outgoing.SentSyncMessage({ - // timestamp: this.get('sent_at'), - // identifier: this.id, - // dataMessage: data, - // destination: this.get('destination'), - // expirationStartTimestamp: this.get('expirationStartTimestamp'), - // sent_to: this.get('sent_to'), - // unidentifiedDeliveries: this.get('unidentifiedDeliveries'), - // }); - - // await libsession.getMessageQueue().sendSyncMessage(syncMessage); - - this.set({ sentSync: true }); - await this.commit(); - }, - - async markMessageSyncOnly(dataMessage) { - this.set({ - // These are the same as a normal send() - dataMessage, - sent_to: [textsecure.storage.user.getNumber()], - sent: true, - expirationStartTimestamp: Date.now(), - }); - - await this.commit(); - }, - - async saveErrors(providedErrors) { - let errors = providedErrors; - - if (!(errors instanceof Array)) { - errors = [errors]; - } - errors.forEach(e => { - window.log.error( - 'Message.saveErrors:', - e && e.reason ? e.reason : null, - e && e.stack ? e.stack : e - ); - }); - errors = errors.map(e => { - if ( - e.constructor === Error || - e.constructor === TypeError || - e.constructor === ReferenceError - ) { - return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); - } - return e; - }); - errors = errors.concat(this.get('errors') || []); - - this.set({ errors }); - await this.commit(); - }, - hasNetworkError() { - const error = _.find( - this.get('errors'), - e => e.name === 'MessageError' || e.name === 'SendMessageNetworkError' - ); - return !!error; - }, - async commit(forceSave = false) { - // TODO investigate the meaning of the forceSave - const id = await window.Signal.Data.saveMessage(this.attributes, { - forceSave, - Message: Whisper.Message, - }); - this.trigger('change'); - return id; - }, - async markRead(readAt) { - this.unset('unread'); - - if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { - const expirationStartTimestamp = Math.min( - Date.now(), - readAt || Date.now() - ); - this.set({ expirationStartTimestamp }); - } - - Whisper.Notifications.remove( - Whisper.Notifications.where({ - messageId: this.id, - }) - ); - - await this.commit(); - }, - isExpiring() { - return this.get('expireTimer') && this.get('expirationStartTimestamp'); - }, - isExpired() { - return this.msTilExpire() <= 0; - }, - msTilExpire() { - if (!this.isExpiring()) { - return Infinity; - } - const now = Date.now(); - const start = this.get('expirationStartTimestamp'); - const delta = this.get('expireTimer') * 1000; - let msFromNow = start + delta - now; - if (msFromNow < 0) { - msFromNow = 0; - } - return msFromNow; - }, - async setToExpire(force = false) { - if (this.isExpiring() && (force || !this.get('expires_at'))) { - const start = this.get('expirationStartTimestamp'); - const delta = this.get('expireTimer') * 1000; - const expiresAt = start + delta; - - this.set({ expires_at: expiresAt }); - const id = this.get('id'); - if (id) { - await this.commit(); - } - - window.log.info('Set message expiration', { - expiresAt, - sentAt: this.get('sent_at'), - }); - } - }, - }); - - // Receive will be enabled before we enable send - Whisper.Message.LONG_MESSAGE_CONTENT_TYPE = 'text/x-signal-plain'; - - Whisper.Message.getLongMessageAttachment = ({ body, attachments, now }) => { - if (body.length <= 2048) { - return { - body, - attachments, - }; - } - - const data = bytesFromString(body); - const attachment = { - contentType: Whisper.Message.LONG_MESSAGE_CONTENT_TYPE, - fileName: `long-message-${now}.txt`, - data, - size: data.byteLength, - }; - - return { - body: body.slice(0, 2048), - attachments: [attachment, ...attachments], - }; - }; - - Whisper.Message.refreshExpirationTimer = () => - Whisper.ExpiringMessagesListener.update(); - - Whisper.MessageCollection = Backbone.Collection.extend({ - model: Whisper.Message, - initialize(models, options) { - if (options) { - this.conversation = options.conversation; - } - }, - async destroyAll() { - await Promise.all( - this.models.map(message => - window.Signal.Data.removeMessage(message.id, { - Message: Whisper.Message, - }) - ) - ); - this.reset([]); - }, - - getLoadedUnreadCount() { - return this.reduce((total, model) => { - const unread = model.get('unread') && model.isIncoming(); - return total + (unread ? 1 : 0); - }, 0); - }, - }); -})(); diff --git a/js/modules/attachment_downloads.js b/js/modules/attachment_downloads.js index 38b30a84ab..88a38de385 100644 --- a/js/modules/attachment_downloads.js +++ b/js/modules/attachment_downloads.js @@ -143,7 +143,7 @@ async function _runJob(job) { } const found = await getMessageById(messageId, { - Message: Whisper.Message, + Message: window.models.Message.MessageModel, }); if (!found) { logger.error('_runJob: Source message not found, deleting job'); @@ -227,7 +227,7 @@ async function _runJob(job) { async function _finishJob(message, id) { if (message) { await saveMessage(message.attributes, { - Message: Whisper.Message, + Message: window.models.Message.MessageModel, }); const conversation = message.getConversation(); if (conversation) { @@ -263,7 +263,6 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) { const { data } = await Signal.Migrations.loadAttachmentData(attachment); message.set({ body: attachment.isError ? message.get('body') : stringFromBytes(data), - bodyPending: false, }); } finally { Signal.Migrations.deleteAttachmentData(attachment.path); diff --git a/js/modules/backup.js b/js/modules/backup.js index 571b0e57d4..0675978ddf 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -144,7 +144,7 @@ async function exportConversationList(fileWriter) { stream.write('"conversations": '); const conversations = await window.Signal.Data.getAllConversations({ - ConversationCollection: Whisper.ConversationCollection, + ConversationCollection: window.models.Conversation.ConversationCollection, }); window.log.info(`Exporting ${conversations.length} conversations`); writeArray(stream, getPlainJS(conversations)); @@ -701,7 +701,7 @@ async function exportConversation(conversation, options = {}) { { limit: CHUNK_SIZE, receivedAt: lastReceivedAt, - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, } ); const messages = getPlainJS(collection); @@ -842,7 +842,7 @@ async function exportConversations(options) { } const collection = await window.Signal.Data.getAllConversations({ - ConversationCollection: Whisper.ConversationCollection, + ConversationCollection: window.models.Conversation.ConversationCollection, }); const conversations = collection.models; @@ -1117,7 +1117,7 @@ async function importConversations(dir, options) { } function getMessageKey(message) { - const ourNumber = textsecure.storage.user.getNumber(); + const ourNumber = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); const source = message.source || ourNumber; if (source === ourNumber) { return `${source} ${message.timestamp}`; diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index 479f94611a..0b0bbbc5e0 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -183,7 +183,6 @@ export function getAllConversations({ }): Promise>; export function getAllConversationIds(): Promise>; -export function getAllPublicConversations(): Promise>; export function getPublicConversationsByServer( server: string, { ConversationCollection }: { ConversationCollection: any } @@ -254,7 +253,7 @@ export function getMessageBySender( export function getMessagesBySender( { source, sourceDevice }: { source: any; sourceDevice: any }, { Message }: { Message: any } -): Promise; +): Promise; export function getMessageIdsFromServerIds( serverIds: any, conversationId: any diff --git a/js/modules/data.js b/js/modules/data.js index f3661b5fd8..2d6ce7cd56 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -881,7 +881,7 @@ async function saveSeenMessageHash(data) { async function saveMessage(data, { forceSave, Message } = {}) { const id = await channels.saveMessage(_cleanData(data), { forceSave }); - Message.refreshExpirationTimer(); + window.Whisper.ExpiringMessagesListener.update(); return id; } diff --git a/js/modules/indexeddb.js b/js/modules/indexeddb.js index 9bdceb5367..1c74bb92a1 100644 --- a/js/modules/indexeddb.js +++ b/js/modules/indexeddb.js @@ -54,7 +54,7 @@ async function mandatoryMessageUpgrade({ upgradeMessageSchema } = {}) { numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, upgradeMessageSchema, maxVersion: MESSAGE_MINIMUM_VERSION, - BackboneMessage: Whisper.Message, + BackboneMessage: window.models.Message.MessageModel, saveMessage: window.Signal.Data.saveLegacyMessage, } ); @@ -70,8 +70,8 @@ async function mandatoryMessageUpgrade({ upgradeMessageSchema } = {}) { while (!isMigrationWithIndexComplete) { // eslint-disable-next-line no-await-in-loop const batchWithIndex = await MessageDataMigrator.processNext({ - BackboneMessage: Whisper.Message, - BackboneMessageCollection: Whisper.MessageCollection, + BackboneMessage: window.models.Message.MessageModel, + BackboneMessageCollection: window.models.Message.MessageCollection, numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, upgradeMessageSchema, getMessagesNeedingUpgrade: diff --git a/js/modules/loki_app_dot_net_api.d.ts b/js/modules/loki_app_dot_net_api.d.ts index 03945e4c3e..1b5631d92b 100644 --- a/js/modules/loki_app_dot_net_api.d.ts +++ b/js/modules/loki_app_dot_net_api.d.ts @@ -10,6 +10,7 @@ interface UploadResponse { } export interface LokiAppDotNetServerInterface { + setAvatar(url: any, profileKey: any); findOrCreateChannel( api: LokiPublicChatFactoryAPI, channelId: number, @@ -23,6 +24,9 @@ export interface LokiAppDotNetServerInterface { } export interface LokiPublicChannelAPI { + getModerators: () => Promise>; + serverAPI: any; + deleteMessages(arg0: any[]); sendMessage( data: { quote?: Quote; diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 8b2c5f3125..79a853da40 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -596,9 +596,7 @@ class LokiAppDotNetServerAPI { // get our profile name // this should be primaryDevicePubKey // because the rest of the profile system uses that... - const ourNumber = - window.storage.get('primaryDevicePubKey') || - textsecure.storage.user.getNumber(); + const ourNumber = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); const profileConvo = window.getConversationController().get(ourNumber); const profile = profileConvo && profileConvo.getLokiProfile(); const profileName = profile && profile.displayName; @@ -1195,17 +1193,14 @@ class LokiPublicChannelAPI { const res = await this.serverRequest( `loki/v1/channels/${this.channelId}/moderators` ); - const ourNumberDevice = textsecure.storage.user.getNumber(); - const ourNumberProfile = window.storage.get('primaryDevicePubKey'); + const ourNumberDevice = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); // Get the list of moderators if no errors occurred const moderators = !res.err && res.response && res.response.moderators; // if we encountered problems then we'll keep the old mod status if (moderators) { - this.modStatus = - (ourNumberProfile && moderators.includes(ourNumberProfile)) || - moderators.includes(ourNumberDevice); + this.modStatus = moderators.includes(ourNumberDevice); } if (this.running) { @@ -1711,7 +1706,7 @@ class LokiPublicChannelAPI { let pendingMessages = []; // get our profile name - const ourNumberDevice = textsecure.storage.user.getNumber(); + const ourNumberDevice = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); // if no primaryDevicePubKey fall back to ourNumberDevice const ourNumberProfile = window.storage.get('primaryDevicePubKey') || ourNumberDevice; @@ -2021,7 +2016,7 @@ class LokiPublicChannelAPI { // copied from model/message.js copyFromQuotedMessage const collection = await Signal.Data.getMessagesBySentAt(quote.id, { - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, }); const found = collection.find(item => { const messageAuthor = item.getContact(); diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 6bde6dc790..548e62ef81 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -25,13 +25,13 @@ exports.processNext = async ({ } = {}) => { if (!isFunction(BackboneMessage)) { throw new TypeError( - "'BackboneMessage' (Whisper.Message) constructor is required" + "'BackboneMessage' (MessageModel) constructor is required" ); } if (!isFunction(BackboneMessageCollection)) { throw new TypeError( - "'BackboneMessageCollection' (Whisper.MessageCollection)" + + "'BackboneMessageCollection' (window.models.Message.MessageCollection)" + ' constructor is required' ); } diff --git a/js/modules/signal.js b/js/modules/signal.js index c73a5e8f63..8032adf3fa 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -12,16 +12,11 @@ const Util = require('../../ts/util'); const { migrateToSQL } = require('./migrate_to_sql'); const LinkPreviews = require('./link_previews'); const AttachmentDownloads = require('./attachment_downloads'); +const { Message } = require('../../ts/components/conversation/Message'); // Components -const { ContactListItem } = require('../../ts/components/ContactListItem'); -const { ContactName } = require('../../ts/components/conversation/ContactName'); -const { Emojify } = require('../../ts/components/conversation/Emojify'); -const { Lightbox } = require('../../ts/components/Lightbox'); -const { LightboxGallery } = require('../../ts/components/LightboxGallery'); const { EditProfileDialog } = require('../../ts/components/EditProfileDialog'); const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog'); -const { SessionModal } = require('../../ts/components/session/SessionModal'); const { SessionSeedModal, } = require('../../ts/components/session/SessionSeedModal'); @@ -38,10 +33,6 @@ const { const { SessionPasswordModal, } = require('../../ts/components/session/SessionPasswordModal'); -const { - SessionPasswordPrompt, -} = require('../../ts/components/session/SessionPasswordPrompt'); - const { SessionConfirm, } = require('../../ts/components/session/SessionConfirm'); @@ -66,22 +57,6 @@ const { RemoveModeratorsDialog, } = require('../../ts/components/conversation/ModeratorsRemoveDialog'); -const { - GroupInvitation, -} = require('../../ts/components/conversation/GroupInvitation'); -const { - MediaGallery, -} = require('../../ts/components/conversation/media-gallery/MediaGallery'); -const { Message } = require('../../ts/components/conversation/Message'); -const { Quote } = require('../../ts/components/conversation/Quote'); -const { - TypingBubble, -} = require('../../ts/components/conversation/TypingBubble'); - -// State -const conversationsDuck = require('../../ts/state/ducks/conversations'); -const userDuck = require('../../ts/state/ducks/user'); - // Migrations const { getPlaceholderMigrations, @@ -95,7 +70,6 @@ const VisualAttachment = require('./types/visual_attachment'); const Contact = require('../../ts/types/Contact'); const Conversation = require('./types/conversation'); const Errors = require('./types/errors'); -const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message'); const MessageType = require('./types/message'); const MIME = require('../../ts/types/MIME'); const SettingsType = require('../../ts/types/Settings'); @@ -207,11 +181,6 @@ exports.setup = (options = {}) => { }); const Components = { - ContactListItem, - ContactName, - Emojify, - Lightbox, - LightboxGallery, EditProfileDialog, UserDetailsDialog, SessionInboxView, @@ -221,29 +190,12 @@ exports.setup = (options = {}) => { AdminLeaveClosedGroupDialog, AddModeratorsDialog, RemoveModeratorsDialog, - GroupInvitation, SessionConfirm, - SessionModal, SessionSeedModal, SessionIDResetDialog, SessionPasswordModal, - SessionPasswordPrompt, SessionRegistrationView, - MediaGallery, Message, - Quote, - Types: { - Message: MediaGalleryMessage, - }, - TypingBubble, - }; - - const Ducks = { - conversations: conversationsDuck, - user: userDuck, - }; - const State = { - Ducks, }; const Types = { @@ -280,7 +232,6 @@ exports.setup = (options = {}) => { Notifications, OS, Settings, - State, Types, Util, Views, diff --git a/js/modules/types/message.js b/js/modules/types/message.js index 8080099d4a..926dab87df 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -727,5 +727,3 @@ exports.createAttachmentDataWriter = ({ return messageWithoutAttachmentData; }; }; - -exports.hasExpiration = MessageTS.hasExpiration; diff --git a/js/read_receipts.js b/js/read_receipts.js index 1c3ef5ede2..42bc73914f 100644 --- a/js/read_receipts.js +++ b/js/read_receipts.js @@ -47,7 +47,8 @@ } const groups = await window.Signal.Data.getAllGroupsInvolvingId(reader, { - ConversationCollection: Whisper.ConversationCollection, + ConversationCollection: + window.models.Conversation.ConversationCollection, }); const ids = groups.pluck('id'); ids.push(reader); @@ -66,7 +67,7 @@ const messages = await window.Signal.Data.getMessagesBySentAt( receipt.get('timestamp'), { - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, } ); diff --git a/js/read_syncs.js b/js/read_syncs.js index 69ea15196c..9e078f6e1c 100644 --- a/js/read_syncs.js +++ b/js/read_syncs.js @@ -30,7 +30,7 @@ const messages = await window.Signal.Data.getMessagesBySentAt( receipt.get('timestamp'), { - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, } ); diff --git a/js/views/app_view.js b/js/views/app_view.js index 88df46da0c..d6873199a1 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -186,7 +186,7 @@ const title = i18n('leaveGroup'); const message = i18n('leaveGroupConfirmation'); - const ourPK = window.textsecure.storage.user.getNumber(); + const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); const isAdmin = (groupConvo.get('groupAdmins') || []).includes(ourPK); const isClosedGroup = groupConvo.get('is_medium_group') || false; diff --git a/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index 12ab3de72a..0b841dee01 100644 --- a/js/views/invite_contacts_dialog_view.js +++ b/js/views/invite_contacts_dialog_view.js @@ -74,7 +74,7 @@ }); } else { // private group chats - const ourPK = window.textsecure.storage.user.getNumber(); + const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); let existingMembers = this.convo.get('members') || []; // at least make sure it's an array if (!Array.isArray(existingMembers)) { diff --git a/js/views/update_group_dialog_view.js b/js/views/update_group_dialog_view.js index 9d4963573b..cf6bb56f04 100644 --- a/js/views/update_group_dialog_view.js +++ b/js/views/update_group_dialog_view.js @@ -101,7 +101,7 @@ } else { this.titleText = i18n('updateGroupDialogTitle', this.groupName); // anybody can edit a closed group name or members - const ourPK = window.textsecure.storage.user.getNumber(); + const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); this.isAdmin = groupConvo.isMediumGroup() ? true : groupConvo.get('groupAdmins').includes(ourPK); @@ -156,7 +156,7 @@ }, async onSubmit(newMembers) { const _ = window.Lodash; - const ourPK = textsecure.storage.user.getNumber(); + const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); const allMembers = window.Lodash.concat(newMembers, [ourPK]); // We need to NOT trigger an group update if the list of member is the same. diff --git a/package.json b/package.json index ed3e341500..7ff7707556 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "test-electron": "yarn grunt test", "test-integration": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --full-trace --timeout 10000 ts/test/session/integration/integration_itest.js", "test-node": "mocha --recursive --exit --timeout 10000 test/app test/modules \"./ts/test/**/*_test.js\" libloki/test/node ", + "test-audric": "mocha --recursive --exit --timeout 10000 ts/test/session/unit/receiving/", "eslint": "eslint --cache .", "eslint-fix": "eslint --fix .", "eslint-full": "eslint .", diff --git a/preload.js b/preload.js index 0db6799e18..19f3d2a250 100644 --- a/preload.js +++ b/preload.js @@ -55,6 +55,16 @@ window.getStoragePubKey = key => window.getDefaultFileServer = () => config.defaultFileServer; window.initialisedAPI = false; +window.lokiFeatureFlags = { + multiDeviceUnpairing: true, + privateGroupChats: true, + useOnionRequests: true, + useOnionRequestsV2: true, + useFileOnionRequests: true, + useFileOnionRequestsV2: true, // more compact encoding of files in response + onionRequestHops: 3, +}; + if ( typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV.includes('test-integration') @@ -174,13 +184,6 @@ window.setPassword = (passPhrase, oldPhrase) => }); window.passwordUtil = require('./ts/util/passwordUtils'); -window.libsession = require('./ts/session'); - -window.getMessageController = - window.libsession.Messages.MessageController.getInstance; - -window.getConversationController = - window.libsession.Conversations.ConversationController.getInstance; // We never do these in our code, so we'll prevent it everywhere window.open = () => null; @@ -336,14 +339,31 @@ require('./js/logging'); if (config.proxyUrl) { window.log.info('Using provided proxy url'); } - window.nodeSetImmediate = setImmediate; -window.seedNodeList = JSON.parse(config.seedNodeList); +const Signal = require('./js/modules/signal'); +const i18n = require('./js/modules/i18n'); +const Attachments = require('./app/attachments'); -const { OnionAPI } = require('./ts/session/onions'); +const { locale } = config; +window.i18n = i18n.setup(locale, localeMessages); -window.OnionAPI = OnionAPI; +window.moment = require('moment'); + +window.moment.updateLocale(locale, { + relativeTime: { + s: window.i18n('timestamp_s'), + m: window.i18n('timestamp_m'), + h: window.i18n('timestamp_h'), + }, +}); +window.moment.locale(locale); + +window.Signal = Signal.setup({ + Attachments, + userDataPath: app.getPath('userData'), + logger: window.log, +}); if (process.env.USE_STUBBED_NETWORK) { const StubMessageAPI = require('./ts/test/session/integration/stubs/stub_message_api'); @@ -367,6 +387,7 @@ const WorkerInterface = require('./js/modules/util_worker_interface'); // A Worker with a 3 minute timeout const utilWorkerPath = path.join(app.getAppPath(), 'js', 'util_worker.js'); const utilWorker = new WorkerInterface(utilWorkerPath, 3 * 60 * 1000); + window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args); // Linux seems to periodically let the event loop stop, so this is a global workaround @@ -385,30 +406,24 @@ window.profileImages = require('./app/profile_images'); window.React = require('react'); window.ReactDOM = require('react-dom'); -window.moment = require('moment'); window.clipboard = clipboard; -const Signal = require('./js/modules/signal'); -const i18n = require('./js/modules/i18n'); -const Attachments = require('./app/attachments'); +window.seedNodeList = JSON.parse(config.seedNodeList); -const { locale } = config; -window.i18n = i18n.setup(locale, localeMessages); -window.moment.updateLocale(locale, { - relativeTime: { - s: window.i18n('timestamp_s'), - m: window.i18n('timestamp_m'), - h: window.i18n('timestamp_h'), - }, -}); -window.moment.locale(locale); +const { OnionAPI } = require('./ts/session/onions'); -window.Signal = Signal.setup({ - Attachments, - userDataPath: app.getPath('userData'), - logger: window.log, -}); +window.OnionAPI = OnionAPI; + +window.libsession = require('./ts/session'); + +window.models = require('./ts/models'); + +window.getMessageController = () => + window.libsession.Messages.MessageController.getInstance(); + +window.getConversationController = () => + window.libsession.Conversations.ConversationController.getInstance(); // Pulling these in separately since they access filesystem, electron window.Signal.Backup = require('./js/modules/backup'); @@ -439,16 +454,6 @@ if (process.env.USE_STUBBED_NETWORK) { window.SwarmPolling = new SwarmPolling(); } -window.lokiFeatureFlags = { - multiDeviceUnpairing: true, - privateGroupChats: true, - useOnionRequests: true, - useOnionRequestsV2: true, - useFileOnionRequests: true, - useFileOnionRequestsV2: true, // more compact encoding of files in response - onionRequestHops: 3, -}; - // eslint-disable-next-line no-extend-native,func-names Promise.prototype.ignore = function() { // eslint-disable-next-line more/no-then diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 582b5c2aa4..3ede0c1f1b 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -149,7 +149,11 @@ message DataMessage { enum Type { NEW = 1; // publicKey, name, encryptionKeyPair, members, admins UPDATE = 2; // name, members - ENCRYPTION_KEY_PAIR = 3; // wrappers + ENCRYPTION_KEY_PAIR = 3; // wrappers + NAME_CHANGE = 4; // name + MEMBERS_ADDED = 5; // members + MEMBERS_REMOVED = 6; // members + MEMBER_LEFT = 7; } message KeyPair { diff --git a/test/backup_test.js b/test/backup_test.js index 6bd25b0c5c..346d5d4995 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -488,7 +488,7 @@ describe('Backup', () => { console.log('Backup test: Create models, save to db/disk'); const message = await upgradeMessageSchema(messageWithAttachments); await window.Signal.Data.saveMessage(message, { - Message: Whisper.Message, + Message: window.models.Message.MessageModel, forceSave: true, }); @@ -554,7 +554,8 @@ describe('Backup', () => { console.log('Backup test: Check conversations'); const conversationCollection = await window.Signal.Data.getAllConversations( { - ConversationCollection: Whisper.ConversationCollection, + ConversationCollection: + window.models.Conversation.ConversationCollection, } ); assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT); @@ -578,7 +579,7 @@ describe('Backup', () => { console.log('Backup test: Check messages'); const messageCollection = await window.Signal.Data.getAllMessages({ - MessageCollection: Whisper.MessageCollection, + MessageCollection: window.models.Message.MessageCollection, }); assert.strictEqual(messageCollection.length, MESSAGE_COUNT); const messageFromDB = removeId(messageCollection.at(0).attributes); diff --git a/test/fixtures.js b/test/fixtures.js index 8aef0aa32b..1a0cd81629 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -8,7 +8,7 @@ Whisper.Fixtures = () => { const MICHEL_ID = '0505cd123456789abcdef05123456789abcdef05123456789abcdef05123456789'; // twh const now = Date.now(); - const conversationCollection = new Whisper.ConversationCollection([ + const conversationCollection = new window.models.Conversation.ConversationCollection([ { name: 'Vera Zasulich', id: VERA_ID, diff --git a/test/fixtures_test.js b/test/fixtures_test.js index 3916b77515..e0256e62fc 100644 --- a/test/fixtures_test.js +++ b/test/fixtures_test.js @@ -15,7 +15,10 @@ describe('Fixtures', () => { await window .getConversationController() - .getOrCreateAndWait(textsecure.storage.user.getNumber(), 'private'); + .getOrCreateAndWait( + window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(), + 'private' + ); }); it('renders', async () => { diff --git a/test/index.html b/test/index.html index e12b76c589..455d9e8712 100644 --- a/test/index.html +++ b/test/index.html @@ -181,8 +181,6 @@

- - diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index 080f2c1bb5..be5e7a41fc 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -7,17 +7,17 @@ describe('ConversationCollection', () => { // before(clearDatabase); // after(clearDatabase); // it('should be ordered newest to oldest', () => { - // const conversations = new Whisper.ConversationCollection(); + // const conversations = new window.models.Conversation.ConversationCollection(); // // Timestamps // const today = new Date(); // const tomorrow = new Date(); // tomorrow.setDate(today.getDate() + 1); // // Add convos - // conversations.add({ timestamp: today }); - // conversations.add({ timestamp: tomorrow }); + // conversations.add({ active_at: today }); + // conversations.add({ active_at: tomorrow }); // const { models } = conversations; - // const firstTimestamp = models[0].get('timestamp').getTime(); - // const secondTimestamp = models[1].get('timestamp').getTime(); + // const firstTimestamp = models[0].get('active_at').getTime(); + // const secondTimestamp = models[1].get('active_at').getTime(); // // Compare timestamps // assert(firstTimestamp > secondTimestamp); // }); @@ -28,7 +28,7 @@ describe('ConversationCollection', () => { // id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }; // before(async () => { - // const convo = new Whisper.ConversationCollection().add(attributes); + // const convo = new window.models.Conversation.ConversationCollection().add(attributes); // await window.Signal.Data.saveConversation(convo.attributes, { // Conversation: Whisper.Conversation, // }); @@ -43,21 +43,21 @@ describe('ConversationCollection', () => { // }); // after(clearDatabase); // it('contains its own messages', async () => { - // const convo = new Whisper.ConversationCollection().add({ + // const convo = new window.models.Conversation.ConversationCollection().add({ // id: '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); // await convo.fetchMessages(); // assert.notEqual(convo.messageCollection.length, 0); // }); // it('contains only its own messages', async () => { - // const convo = new Whisper.ConversationCollection().add({ + // const convo = new window.models.Conversation.ConversationCollection().add({ // id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); // await convo.fetchMessages(); // assert.strictEqual(convo.messageCollection.length, 0); // }); // it('adds conversation to message collection upon leaving group', async () => { - // const convo = new Whisper.ConversationCollection().add({ + // const convo = new window.models.Conversation.ConversationCollection().add({ // type: 'group', // id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); @@ -65,7 +65,7 @@ describe('ConversationCollection', () => { // assert.notEqual(convo.messageCollection.length, 0); // }); // it('has a title', () => { - // const convos = new Whisper.ConversationCollection(); + // const convos = new window.models.Conversation.ConversationCollection(); // let convo = convos.add(attributes); // assert.equal( // convo.getTitle(), @@ -77,7 +77,7 @@ describe('ConversationCollection', () => { // assert.equal(convo.getTitle(), 'name'); // }); // it('returns the number', () => { - // const convos = new Whisper.ConversationCollection(); + // const convos = new window.models.Conversation.ConversationCollection(); // let convo = convos.add(attributes); // assert.equal( // convo.getNumber(), diff --git a/test/models/messages_test.js b/test/models/messages_test.js index 9e25be9367..8fa8a86522 100644 --- a/test/models/messages_test.js +++ b/test/models/messages_test.js @@ -23,13 +23,13 @@ describe('MessageCollection', () => { }); it('gets outgoing contact', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); const message = messages.add(attributes); message.getContact(); }); it('gets incoming contact', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); const message = messages.add({ type: 'incoming', source, @@ -38,7 +38,7 @@ describe('MessageCollection', () => { }); it('should be ordered oldest to newest', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); // Timestamps const today = new Date(); const tomorrow = new Date(); @@ -57,7 +57,7 @@ describe('MessageCollection', () => { }); it('checks if is incoming message', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); let message = messages.add(attributes); assert.notOk(message.isIncoming()); message = messages.add({ type: 'incoming' }); @@ -65,7 +65,7 @@ describe('MessageCollection', () => { }); it('checks if is outgoing message', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); let message = messages.add(attributes); assert.ok(message.isOutgoing()); message = messages.add({ type: 'incoming' }); @@ -73,7 +73,7 @@ describe('MessageCollection', () => { }); it('checks if is group update', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); let message = messages.add(attributes); assert.notOk(message.isGroupUpdate()); @@ -82,7 +82,7 @@ describe('MessageCollection', () => { }); it('returns an accurate description', () => { - const messages = new Whisper.MessageCollection(); + const messages = new window.models.Message.MessageCollection(); let message = messages.add(attributes); assert.equal( diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 923989c72e..1ea9171701 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -23,36 +23,12 @@ import { createPortal } from 'react-dom'; import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus'; import { DefaultTheme, withTheme } from 'styled-components'; import { PubKey } from '../session/types'; +import { ConversationType } from '../state/ducks/conversations'; -export type ConversationListItemProps = { - id: string; - phoneNumber: string; - color?: string; - profileName?: string; - name?: string; - type: 'group' | 'direct'; - avatarPath?: string; - isMe: boolean; - isPublic?: boolean; - - lastUpdated: number; - unreadCount: number; - mentionedUs: boolean; - isSelected: boolean; - - isTyping: boolean; - lastMessage?: { - status: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; - text: string; - }; - - isBlocked?: boolean; - hasNickname?: boolean; - isGroupInvitation?: boolean; - isKickedFromGroup?: boolean; - left?: boolean; +export interface ConversationListItemProps extends ConversationType { + index: number; // used to force a refresh when one conversation is removed on top of the list memberAvatars?: Array; // this is added by usingClosedConversationDetails -}; +} type PropsHousekeeping = { i18n: LocalizerType; @@ -110,7 +86,7 @@ class ConversationListItem extends React.PureComponent { } public renderHeader() { - const { unreadCount, mentionedUs, i18n, isMe, lastUpdated } = this.props; + const { unreadCount, mentionedUs, activeAt } = this.props; let atSymbol = null; let unreadCountDiv = null; @@ -148,7 +124,7 @@ class ConversationListItem extends React.PureComponent { > { { const viewEdit = this.state.mode === 'edit'; const viewQR = this.state.mode === 'qr'; - /* tslint:disable:no-backbone-get-set-outside-model */ - const sessionID = - window.textsecure.storage.get('primaryDevicePubKey') || - window.textsecure.storage.user.getNumber(); - /* tslint:enable:no-backbone-get-set-outside-model */ + const sessionID = UserUtils.getOurPubKeyStrFromCache(); const backButton = viewEdit || viewQR diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index d4790ba318..7c91a879be 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -6,12 +6,9 @@ import { LeftPaneMessageSection } from './session/LeftPaneMessageSection'; import { ConversationListItemProps } from './ConversationListItem'; import { SearchResultsProps } from './SearchResults'; import { SearchOptions } from '../types/Search'; -import { LeftPaneSectionHeader } from './session/LeftPaneSectionHeader'; - import { ConversationType } from '../state/ducks/conversations'; import { LeftPaneContactSection } from './session/LeftPaneContactSection'; import { LeftPaneSettingSection } from './session/LeftPaneSettingSection'; -import { SessionIconType } from './session/icon'; import { SessionTheme } from '../state/ducks/SessionTheme'; import { DefaultTheme } from 'styled-components'; import { SessionSettingCategory } from './session/settings/SessionSettings'; diff --git a/ts/components/conversation/AddMentions.tsx b/ts/components/conversation/AddMentions.tsx index 81336ad338..f48dafb528 100644 --- a/ts/components/conversation/AddMentions.tsx +++ b/ts/components/conversation/AddMentions.tsx @@ -4,9 +4,9 @@ import { RenderTextCallbackType } from '../../types/Util'; import classNames from 'classnames'; import { FindMember } from '../../util'; import { useInterval } from '../../hooks/useInterval'; -import { ConversationModel } from '../../../js/models/conversations'; -import { isUs } from '../../session/utils/User'; import { PubKey } from '../../session/types'; +import { ConversationModel } from '../../models/conversation'; +import { UserUtils } from '../../session/utils'; interface MentionProps { key: string; @@ -26,7 +26,7 @@ const Mention = (props: MentionProps) => { ); if (foundMember) { - const itsUs = await isUs(foundMember.id); + const itsUs = UserUtils.isUsFromCache(foundMember.id); setUs(itsUs); setFound(foundMember); // FIXME stop this interval once we found it. diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index f96a194e7f..ebe0319463 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -36,10 +36,10 @@ import uuid from 'uuid'; import { InView } from 'react-intersection-observer'; import { withTheme } from 'styled-components'; import { MessageMetadata } from './message/MessageMetadata'; -import { MessageRegularProps } from '../../../js/models/messages'; import { PubKey } from '../../session/types'; -import { ToastUtils } from '../../session/utils'; +import { ToastUtils, UserUtils } from '../../session/utils'; import { ConversationController } from '../../session/conversations'; +import { MessageRegularProps } from '../../models/messageType'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -517,7 +517,6 @@ class MessageInner extends React.PureComponent { public renderText() { const { text, - bodyPending, direction, status, conversationType, @@ -548,7 +547,6 @@ class MessageInner extends React.PureComponent { { // to see if the message mentions us, so we can display the entire // message differently const regex = new RegExp(`@${PubKey.regexForPubkeys}`, 'g'); - const mentions = text ? text.match(regex) : []; + const mentions = (text ? text.match(regex) : []) as Array; const mentionMe = mentions && - mentions.some(m => m.slice(1) === window.lokiPublicChatAPI.ourKey); + mentions.some(m => m.slice(1) === UserUtils.getOurPubKeyStrFromCache()); const isIncoming = direction === 'incoming'; const shouldHightlight = mentionMe && isIncoming && isPublic; @@ -953,6 +951,12 @@ class MessageInner extends React.PureComponent { try { const convo = ConversationController.getInstance().getOrThrow(convoId); const channelAPI = await convo.getPublicSendData(); + if (!channelAPI) { + throw new Error('No channelAPI'); + } + if (!channelAPI.serverAPI) { + throw new Error('No serverAPI'); + } const res = await channelAPI.serverAPI.addModerator([pubkey]); if (!res) { window.log.warn('failed to add moderators:', res); @@ -961,8 +965,8 @@ class MessageInner extends React.PureComponent { } else { window.log.info(`${pubkey} added as moderator...`); // refresh the moderator list. Will trigger a refresh - const modPubKeys = (await channelAPI.getModerators()) as Array; - convo.updateGroupAdmins(modPubKeys); + const modPubKeys = await channelAPI.getModerators(); + await convo.updateGroupAdmins(modPubKeys); ToastUtils.pushUserAddedToModerators(); } @@ -976,6 +980,9 @@ class MessageInner extends React.PureComponent { try { const convo = ConversationController.getInstance().getOrThrow(convoId); const channelAPI = await convo.getPublicSendData(); + if (!channelAPI) { + throw new Error('No channelAPI'); + } const res = await channelAPI.serverAPI.removeModerators([pubkey]); if (!res) { window.log.warn('failed to remove moderators:', res); @@ -983,8 +990,8 @@ class MessageInner extends React.PureComponent { ToastUtils.pushErrorHappenedWhileRemovingModerator(); } else { // refresh the moderator list. Will trigger a refresh - const modPubKeys = (await channelAPI.getModerators()) as Array; - convo.updateGroupAdmins(modPubKeys); + const modPubKeys = await channelAPI.getModerators(); + await convo.updateGroupAdmins(modPubKeys); window.log.info(`${pubkey} removed from moderators...`); ToastUtils.pushUserRemovedToModerators(); diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index b10cf6a807..e4af45af1c 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -10,7 +10,6 @@ import { LocalizerType, RenderTextCallbackType } from '../../types/Util'; interface Props { text: string; - bodyPending?: boolean; /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */ disableJumbomoji?: boolean; /** If set, links will be left alone instead of turned into clickable `` tags. */ @@ -84,25 +83,14 @@ export class MessageBody extends React.Component { }; public addDownloading(jsx: JSX.Element): JSX.Element { - const { i18n, bodyPending } = this.props; + const { i18n } = this.props; - return ( - - {jsx} - {bodyPending ? ( - - {' '} - {i18n('downloading')} - - ) : null} - - ); + return {jsx}; } public render() { const { text, - bodyPending, disableJumbomoji, disableLinks, i18n, @@ -110,23 +98,12 @@ export class MessageBody extends React.Component { convoId, } = this.props; const sizeClass = disableJumbomoji ? undefined : getSizeClass(text); - const textWithPending = bodyPending ? `${text}...` : text; - - const emoji = renderEmoji({ - i18n, - text: textWithPending, - sizeClass, - key: 0, - renderNonEmoji: renderNewLines, - isGroup, - convoId, - }); if (disableLinks) { return this.addDownloading( renderEmoji({ i18n, - text: textWithPending, + text, sizeClass, key: 0, renderNonEmoji: renderNewLines, @@ -138,7 +115,7 @@ export class MessageBody extends React.Component { const bodyContents = this.addDownloading( { return renderEmoji({ i18n, @@ -155,7 +132,7 @@ export class MessageBody extends React.Component { return this.addDownloading( { return renderEmoji({ i18n, diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index cced52ede0..3f7c77f5ea 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -5,7 +5,7 @@ import moment from 'moment'; import { Avatar } from '../Avatar'; import { ContactName } from './ContactName'; import { Message } from './Message'; -import { MessageRegularProps } from '../../../js/models/messages'; +import { MessageRegularProps } from '../../models/messageType'; interface Contact { status: string; diff --git a/ts/components/conversation/ModeratorsAddDialog.tsx b/ts/components/conversation/ModeratorsAddDialog.tsx index c3a41f56ee..1ea17a5c37 100644 --- a/ts/components/conversation/ModeratorsAddDialog.tsx +++ b/ts/components/conversation/ModeratorsAddDialog.tsx @@ -5,12 +5,12 @@ import { SessionButtonType, } from '../session/SessionButton'; import { PubKey } from '../../session/types'; -import { ConversationModel } from '../../../js/models/conversations'; import { ToastUtils } from '../../session/utils'; import { SessionModal } from '../session/SessionModal'; import { DefaultTheme } from 'styled-components'; import { SessionSpinner } from '../session/SessionSpinner'; import { Flex } from '../session/Flex'; +import { ConversationModel } from '../../models/conversation'; interface Props { convo: ConversationModel; onClose: any; diff --git a/ts/components/conversation/ModeratorsRemoveDialog.tsx b/ts/components/conversation/ModeratorsRemoveDialog.tsx index 98327ad50f..31f153a0fe 100644 --- a/ts/components/conversation/ModeratorsRemoveDialog.tsx +++ b/ts/components/conversation/ModeratorsRemoveDialog.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { DefaultTheme } from 'styled-components'; -import { ConversationModel } from '../../../js/models/conversations'; +import { ConversationModel } from '../../models/conversation'; import { ConversationController } from '../../session/conversations'; import { ToastUtils } from '../../session/utils'; import { Flex } from '../session/Flex'; diff --git a/ts/components/conversation/message/MessageMetadata.tsx b/ts/components/conversation/message/MessageMetadata.tsx index dec4ce8fb1..5991c94230 100644 --- a/ts/components/conversation/message/MessageMetadata.tsx +++ b/ts/components/conversation/message/MessageMetadata.tsx @@ -17,7 +17,6 @@ type Props = { isAdmin?: boolean; isDeletable: boolean; text?: string; - bodyPending?: boolean; id: string; collapseMetadata?: boolean; direction: 'incoming' | 'outgoing'; @@ -69,7 +68,6 @@ export const MessageMetadata = (props: Props) => { expirationTimestamp, status, text, - bodyPending, timestamp, serverTimestamp, isShowingImage, @@ -86,7 +84,7 @@ export const MessageMetadata = (props: Props) => { const withImageNoCaption = Boolean(!text && isShowingImage); const showError = status === 'error' && isOutgoing; - const showStatus = Boolean(!bodyPending && status?.length && isOutgoing); + const showStatus = Boolean(status?.length && isOutgoing); const messageStatusColor = withImageNoCaption ? 'white' : props.theme.colors.sentMessageText; @@ -124,7 +122,6 @@ export const MessageMetadata = (props: Props) => { /> ) : null} - {bodyPending ? : null} {showStatus ? ( { if (search) { search(searchTerm, { noteToSelf: window.i18n('noteToSelf').toLowerCase(), - ourNumber: window.textsecure.storage.user.getNumber(), + ourNumber: UserUtils.getOurPubKeyStrFromCache(), }); } } diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index 2b9bd03db0..813f92fb73 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -14,6 +14,7 @@ import { import { SessionSpinner } from './SessionSpinner'; import { PillDivider } from './PillDivider'; import { DefaultTheme } from 'styled-components'; +import { UserUtils } from '../../session/utils'; export enum SessionClosableOverlayType { Contact = 'contact', @@ -159,7 +160,7 @@ export class SessionClosableOverlay extends React.Component { } const { groupName, selectedMembers } = this.state; - const ourSessionID = window.textsecure.storage.user.getNumber(); + const ourSessionID = UserUtils.getOurPubKeyStrFromCache(); const contacts = this.getContacts(); diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index d1b079de67..f8507760da 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { MessageModel } from '../../models/message'; import { getMessageQueue } from '../../session'; -import { ConversationController } from '../../session/conversations/ConversationController'; +import { ConversationController } from '../../session/conversations'; import { MessageController } from '../../session/messages'; import { OpenGroupMessage } from '../../session/messages/outgoing'; import { RawMessage } from '../../session/types'; +import { UserUtils } from '../../session/utils'; import { createStore } from '../../state/createStore'; import { actions as conversationActions } from '../../state/ducks/conversations'; import { actions as userActions } from '../../state/ducks/user'; @@ -114,24 +116,6 @@ export class SessionInboxView extends React.Component { } private async fetchHandleMessageSentData(m: RawMessage | OpenGroupMessage) { - // nobody is listening to this freshly fetched message .trigger calls - const tmpMsg = await window.Signal.Data.getMessageById(m.identifier, { - Message: window.Whisper.Message, - }); - - if (!tmpMsg) { - return null; - } - - // find the corresponding conversation of this message - const conv = ConversationController.getInstance().get( - tmpMsg.get('conversationId') - ); - - if (!conv) { - return null; - } - const msg = window.getMessageController().get(m.identifier); if (!msg || !msg.message) { @@ -183,6 +167,8 @@ export class SessionInboxView extends React.Component { const fullFilledConversations = await Promise.all(filledConversations); + console.warn('fullFilledConversations', fullFilledConversations); + const initialState = { conversations: { conversationLookup: window.Signal.Util.makeLookup( @@ -192,9 +178,7 @@ export class SessionInboxView extends React.Component { }, user: { ourPrimary: window.storage.get('primaryDevicePubKey'), - ourNumber: - window.storage.get('primaryDevicePubKey') || - window.textsecure.storage.user.getNumber(), + ourNumber: UserUtils.getOurPubKeyStrFromCache(), i18n: window.i18n, }, section: { diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index a2c267bd80..8fdcd8428e 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -27,7 +27,7 @@ import { SessionQuotedMessageComposition } from './SessionQuotedMessageCompositi import { Mention, MentionsInput } from 'react-mentions'; import { CaptionEditor } from '../../CaptionEditor'; import { DefaultTheme } from 'styled-components'; -import { ConversationController } from '../../../session/conversations/ConversationController'; +import { ConversationController } from '../../../session/conversations'; import { ConversationType } from '../../../state/ducks/conversations'; import { SessionMemberListItem } from '../SessionMemberListItem'; @@ -467,7 +467,7 @@ export class SessionCompositionBox extends React.Component { const conv = ConversationController.getInstance().get(pubKey); let profileName = 'Anonymous'; if (conv) { - profileName = conv.getProfileName(); + profileName = conv.getProfileName() || 'Anonymous'; } return { id: pubKey, diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 1b3789f8a5..b6a7bb1344 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -31,6 +31,7 @@ import { pushUnblockToSend } from '../../../session/utils/Toast'; import { MessageDetail } from '../../conversation/MessageDetail'; import { ConversationController } from '../../../session/conversations'; import { PubKey } from '../../../session/types'; +import { MessageModel } from '../../../models/message'; interface State { // Message sending progress @@ -260,8 +261,7 @@ export class SessionConversation extends React.Component { attachments: any, quote: any, preview: any, - groupInvitation: any, - otherOptions: any + groupInvitation: any ) => { if (!conversationModel) { return; @@ -271,8 +271,7 @@ export class SessionConversation extends React.Component { attachments, quote, preview, - groupInvitation, - otherOptions + groupInvitation ); if (this.messageContainerRef.current) { // force scrolling to bottom on message sent @@ -436,14 +435,13 @@ export class SessionConversation extends React.Component { hasNickname: !!conversation.getNickname(), selectionMode: !!selectedMessages.length, - onSetDisappearingMessages: (seconds: any) => - conversation.updateExpirationTimer(seconds), - onDeleteMessages: () => conversation.deleteMessages(), + onSetDisappearingMessages: conversation.updateExpirationTimer, + onDeleteMessages: conversation.deleteMessages, onDeleteSelectedMessages: this.deleteSelectedMessages, onCloseOverlay: () => { this.setState({ selectedMessages: [] }); }, - onDeleteContact: () => conversation.deleteContact(), + onDeleteContact: conversation.deleteContact, onGoBack: () => { this.setState({ @@ -456,10 +454,10 @@ export class SessionConversation extends React.Component { }, onBlockUser: () => { - conversation.block(); + void conversation.block(); }, onUnblockUser: () => { - conversation.unblock(); + void conversation.unblock(); }, onCopyPublicKey: () => { conversation.copyPublicKey(); @@ -681,7 +679,7 @@ export class SessionConversation extends React.Component { if (selectedConversation.isPublic) { // Get our Moderator status - const ourDevicePubkey = await UserUtils.getCurrentDevicePubKey(); + const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache(); if (!ourDevicePubkey) { return; } @@ -808,7 +806,7 @@ export class SessionConversation extends React.Component { if (quotedMessage) { const quotedMessageModel = await getMessageById(quotedMessage.id, { - Message: window.Whisper.Message, + Message: MessageModel, }); if (quotedMessageModel) { quotedMessageProps = await conversationModel.makeQuote( @@ -1204,10 +1202,8 @@ export class SessionConversation extends React.Component { const allMembers = allPubKeys.map((pubKey: string) => { const conv = ConversationController.getInstance().get(pubKey); - let profileName = 'Anonymous'; - if (conv) { - profileName = conv.getProfileName(); - } + const profileName = conv?.getProfileName() || 'Anonymous'; + return { id: pubKey, authorPhoneNumber: pubKey, diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 7d7d454cc1..5a9bbb75be 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -11,15 +11,12 @@ import { AttachmentType } from '../../../types/Attachment'; import { GroupNotification } from '../../conversation/GroupNotification'; import { GroupInvitation } from '../../conversation/GroupInvitation'; import { ConversationType } from '../../../state/ducks/conversations'; -import { - MessageModel, - MessageRegularProps, -} from '../../../../js/models/messages'; import { SessionLastSeenIndicator } from './SessionLastSeedIndicator'; import { ToastUtils } from '../../../session/utils'; import { TypingBubble } from '../../conversation/TypingBubble'; import { ConversationController } from '../../../session/conversations'; -import { PubKey } from '../../../session/types'; +import { MessageCollection, MessageModel } from '../../../models/message'; +import { MessageRegularProps } from '../../../models/messageType'; interface State { showScrollButton: boolean; @@ -299,7 +296,7 @@ export class SessionMessagesList extends React.Component { <> {this.renderMessage( messageProps, - message.firstMessageOfSeries, + messageProps.firstMessageOfSeries, multiSelectMode, message )} @@ -560,7 +557,7 @@ export class SessionMessagesList extends React.Component { // some more information then show an informative toast to the user. if (!targetMessage) { const collection = await window.Signal.Data.getMessagesBySentAt(quoteId, { - MessageCollection: window.Whisper.MessageCollection, + MessageCollection, }); const found = Boolean( collection.find((item: MessageModel) => { diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index a488b32a6f..bf0fb87880 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -17,6 +17,7 @@ import { } from '../usingClosedConversationDetails'; import { save } from '../../../types/Attachment'; import { DefaultTheme, withTheme } from 'styled-components'; +import { MessageCollection } from '../../../models/message'; interface Props { id: string; @@ -110,14 +111,14 @@ class SessionRightPanel extends React.Component { conversationId, { limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT, - MessageCollection: window.Whisper.MessageCollection, + MessageCollection, } ); const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments( conversationId, { limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT, - MessageCollection: window.Whisper.MessageCollection, + MessageCollection, } ); diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx index 7f69780dcb..dc0d060987 100644 --- a/ts/components/session/usingClosedConversationDetails.tsx +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -55,9 +55,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { (conversationType === 'group' || type === 'group' || isGroup) ) { const groupId = id || phoneNumber; - const ourPrimary = PubKey.cast( - await UserUtils.getCurrentDevicePubKey() - ); + const ourPrimary = await UserUtils.getOurPubKeyFromCache(); let members = await GroupUtils.getGroupMembers(PubKey.cast(groupId)); const ourself = members.find(m => m.key !== ourPrimary.key); @@ -79,7 +77,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { ); const memberAvatars = memberConvos.map(m => { return { - avatarPath: m.getAvatar()?.url || null, + avatarPath: m.getAvatar()?.url || undefined, id: m.id, name: m.get('name') || m.get('profileName') || m.id, }; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts new file mode 100644 index 0000000000..0b19c2638b --- /dev/null +++ b/ts/models/conversation.ts @@ -0,0 +1,1821 @@ +import Backbone from 'backbone'; +import _ from 'lodash'; +import { ConversationType } from '../receiver/common'; +import { getMessageQueue } from '../session'; +import { ConversationController } from '../session/conversations'; +import { + ChatMessage, + ExpirationTimerUpdateMessage, + GroupInvitationMessage, + OpenGroupMessage, + ReadReceiptMessage, + TypingMessage, +} from '../session/messages/outgoing'; +import { ClosedGroupChatMessage } from '../session/messages/outgoing/content/data/group'; +import { OpenGroup, PubKey } from '../session/types'; +import { ToastUtils, UserUtils } from '../session/utils'; +import { BlockedNumberController } from '../util'; +import { MessageController } from '../session/messages'; +import { leaveClosedGroup } from '../session/group'; +import { SignalService } from '../protobuf'; +import { MessageCollection, MessageModel } from './message'; +import * as Data from '../../js/modules/data'; +import { MessageAttributesOptionals } from './messageType'; + +export interface OurLokiProfile { + displayName: string; + avatarPointer: string; + profileKey: Uint8Array | null; +} + +export interface ConversationAttributes { + profileName?: string; + id: string; + name?: string; + members: Array; + left: boolean; + expireTimer: number; + profileSharing: boolean; + mentionedUs: boolean; + unreadCount: number; + lastMessageStatus: string | null; + active_at: number; + lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group + groupAdmins?: Array; + moderators?: Array; // TODO to merge to groupAdmins with a migration on the db + isKickedFromGroup?: boolean; + avatarPath?: string; + isMe?: boolean; + subscriberCount?: number; + sessionRestoreSeen?: boolean; + is_medium_group?: boolean; + type: string; + lastMessage?: string | null; + avatarPointer?: any; + avatar?: any; + server?: any; + channelId?: any; + nickname?: string; + profile?: any; + lastPublicMessage?: any; + profileAvatar?: any; + profileKey?: string; + accessKey?: any; +} + +export interface ConversationAttributesOptionals { + profileName?: string; + id: string; + name?: string; + members?: Array; + left?: boolean; + expireTimer?: number; + profileSharing?: boolean; + mentionedUs?: boolean; + unreadCount?: number; + lastMessageStatus?: string | null; + active_at?: number; + timestamp?: number; // timestamp of what? + lastJoinedTimestamp?: number; + groupAdmins?: Array; + moderators?: Array; + isKickedFromGroup?: boolean; + avatarPath?: string; + isMe?: boolean; + subscriberCount?: number; + sessionRestoreSeen?: boolean; + is_medium_group?: boolean; + type: string; + lastMessage?: string | null; + avatarPointer?: any; + avatar?: any; + server?: any; + channelId?: any; + nickname?: string; + profile?: any; + lastPublicMessage?: any; + profileAvatar?: any; + profileKey?: string; + accessKey?: any; +} + +/** + * This function mutates optAttributes + * @param optAttributes the entry object attributes to set the defaults to. + */ +export const fillConvoAttributesWithDefaults = ( + optAttributes: ConversationAttributesOptionals +): ConversationAttributes => { + return _.defaults(optAttributes, { + members: [], + left: false, + profileSharing: false, + unreadCount: 0, + lastMessageStatus: null, + lastJoinedTimestamp: new Date('1970-01-01Z00:00:00:000').getTime(), + groupAdmins: [], + moderators: [], + isKickedFromGroup: false, + isMe: false, + subscriberCount: 0, + sessionRestoreSeen: false, + is_medium_group: false, + lastMessage: null, + expireTimer: 0, + mentionedUs: false, + active_at: 0, + }); +}; + +export class ConversationModel extends Backbone.Model { + public updateLastMessage: () => any; + public messageCollection: MessageCollection; + public throttledBumpTyping: any; + public initialPromise: any; + + private typingRefreshTimer?: NodeJS.Timeout | null; + private typingPauseTimer?: NodeJS.Timeout | null; + private typingTimer?: NodeJS.Timeout | null; + + private cachedProps: any; + + private pending: any; + // storeName: 'conversations', + + constructor(attributes: ConversationAttributesOptionals) { + super(fillConvoAttributesWithDefaults(attributes)); + + // This may be overridden by ConversationController.getOrCreate, and signify + // our first save to the database. Or first fetch from the database. + this.initialPromise = Promise.resolve(); + + this.messageCollection = new MessageCollection([], { + conversation: this, + }); + + this.throttledBumpTyping = _.throttle(this.bumpTyping, 300); + this.updateLastMessage = _.throttle( + this.bouncyUpdateLastMessage.bind(this), + 1000 + ); + // this.listenTo( + // this.messageCollection, + // 'add remove destroy', + // debouncedUpdateLastMessage + // ); + // Listening for out-of-band data updates + this.on('delivered', this.updateAndMerge); + this.on('read', this.updateAndMerge); + this.on('expiration-change', this.updateAndMerge); + this.on('expired', this.onExpired); + + this.on('ourAvatarChanged', avatar => + this.updateAvatarOnPublicChat(avatar) + ); + + // Always share profile pics with public chats + if (this.isPublic()) { + this.set('profileSharing', true); + } + this.unset('hasFetchedProfile'); + this.unset('tokens'); + + this.typingRefreshTimer = null; + this.typingPauseTimer = null; + + // Keep props ready + const generateProps = () => { + this.cachedProps = this.getProps(); + }; + this.on('change', generateProps); + generateProps(); + } + + public idForLogging() { + if (this.isPrivate()) { + return this.id; + } + + return `group(${this.id})`; + } + + public isMe() { + return UserUtils.isUsFromCache(this.id); + } + public isPublic() { + return !!(this.id && this.id.match(/^publicChat:/)); + } + public isClosedGroup() { + return this.get('type') === ConversationType.GROUP && !this.isPublic(); + } + + public isBlocked() { + if (!this.id || this.isMe()) { + return false; + } + + if (this.isClosedGroup()) { + return BlockedNumberController.isGroupBlocked(this.id); + } + + if (this.isPrivate()) { + return BlockedNumberController.isBlocked(this.id); + } + + return false; + } + + public isMediumGroup() { + return this.get('is_medium_group'); + } + public async block() { + if (!this.id || this.isPublic()) { + return; + } + + const promise = this.isPrivate() + ? BlockedNumberController.block(this.id) + : BlockedNumberController.blockGroup(this.id); + await promise; + await this.commit(); + } + + public async unblock() { + if (!this.id || this.isPublic()) { + return; + } + const promise = this.isPrivate() + ? BlockedNumberController.unblock(this.id) + : BlockedNumberController.unblockGroup(this.id); + await promise; + await this.commit(); + } + public async bumpTyping() { + if (this.isPublic() || this.isMediumGroup()) { + return; + } + // We don't send typing messages if the setting is disabled + // or we blocked that user + + if (!window.storage.get('typing-indicators-setting') || this.isBlocked()) { + return; + } + + if (!this.typingRefreshTimer) { + const isTyping = true; + this.setTypingRefreshTimer(); + this.sendTypingMessage(isTyping); + } + + this.setTypingPauseTimer(); + } + + public setTypingRefreshTimer() { + if (this.typingRefreshTimer) { + clearTimeout(this.typingRefreshTimer); + } + this.typingRefreshTimer = global.setTimeout( + this.onTypingRefreshTimeout.bind(this), + 10 * 1000 + ); + } + + public onTypingRefreshTimeout() { + const isTyping = true; + this.sendTypingMessage(isTyping); + + // This timer will continue to reset itself until the pause timer stops it + this.setTypingRefreshTimer(); + } + + public setTypingPauseTimer() { + if (this.typingPauseTimer) { + clearTimeout(this.typingPauseTimer); + } + this.typingPauseTimer = global.setTimeout( + this.onTypingPauseTimeout.bind(this), + 10 * 1000 + ); + } + + public onTypingPauseTimeout() { + const isTyping = false; + this.sendTypingMessage(isTyping); + + this.clearTypingTimers(); + } + + public clearTypingTimers() { + if (this.typingPauseTimer) { + clearTimeout(this.typingPauseTimer); + this.typingPauseTimer = null; + } + if (this.typingRefreshTimer) { + clearTimeout(this.typingRefreshTimer); + this.typingRefreshTimer = null; + } + } + + public sendTypingMessage(isTyping: boolean) { + if (!this.isPrivate()) { + return; + } + + const recipientId = this.id; + + if (!recipientId) { + throw new Error('Need to provide either recipientId'); + } + + const primaryDevicePubkey = window.storage.get('primaryDevicePubKey'); + if (recipientId && primaryDevicePubkey === recipientId) { + // note to self + return; + } + + const typingParams = { + timestamp: Date.now(), + isTyping, + typingTimestamp: Date.now(), + }; + const typingMessage = new TypingMessage(typingParams); + + // send the message to a single recipient if this is a session chat + const device = new PubKey(recipientId); + getMessageQueue() + .sendToPubKey(device, typingMessage) + .catch(window.log.error); + } + + public async cleanup() { + const { deleteAttachmentData } = window.Signal.Migrations; + await window.Signal.Types.Conversation.deleteExternalFiles( + this.attributes, + { + deleteAttachmentData, + } + ); + window.profileImages.removeImage(this.id); + } + + public async updateProfileAvatar() { + if (this.isPublic()) { + return; + } + + // Remove old identicons + if (window.profileImages.hasImage(this.id)) { + window.profileImages.removeImage(this.id); + await this.setProfileAvatar(null); + } + } + + public async updateAndMerge(message: any) { + await this.updateLastMessage(); + + const mergeMessage = () => { + const existing = this.messageCollection.get(message.id); + if (!existing) { + return; + } + + existing.merge(message.attributes); + }; + + mergeMessage(); + } + + public async onExpired(message: any) { + await this.updateLastMessage(); + + const removeMessage = () => { + const { id } = message; + const existing = this.messageCollection.get(id); + if (!existing) { + return; + } + + window.log.info('Remove expired message from collection', { + sentAt: existing.get('sent_at'), + }); + + this.messageCollection.remove(id); + existing.trigger('expired'); + }; + + removeMessage(); + } + + // Get messages with the given timestamp + public getMessagesWithTimestamp(pubKey: string, timestamp: number) { + if (this.id !== pubKey) { + return []; + } + + // Go through our messages and find the one that we need to update + return this.messageCollection.models.filter( + (m: any) => m.get('sent_at') === timestamp + ); + } + + public async onCalculatingPoW(pubKey: string, timestamp: number) { + const messages = this.getMessagesWithTimestamp(pubKey, timestamp); + await Promise.all(messages.map((m: any) => m.setCalculatingPoW())); + } + + public async onPublicMessageSent( + identifier: any, + serverId: any, + serverTimestamp: any + ) { + const registeredMessage = window.getMessageController().get(identifier); + + if (!registeredMessage || !registeredMessage.message) { + return null; + } + const model = registeredMessage.message; + await model.setIsPublic(true); + await model.setServerId(serverId); + await model.setServerTimestamp(serverTimestamp); + return undefined; + } + public addSingleMessage( + message: MessageAttributesOptionals, + setToExpire = true + ) { + const model = this.messageCollection.add(message, { merge: true }); + if (setToExpire) { + void model.setToExpire(); + } + return model; + } + public format() { + return this.cachedProps; + } + public getGroupAdmins() { + return this.get('groupAdmins') || this.get('moderators'); + } + public getProps() { + const groupAdmins = this.getGroupAdmins(); + + const members = + this.isGroup() && !this.isPublic() ? this.get('members') : undefined; + + const result = { + id: this.id as string, + activeAt: this.get('active_at'), + avatarPath: this.getAvatarPath(), + type: this.isPrivate() ? 'direct' : 'group', + isMe: this.isMe(), + isPublic: this.isPublic(), + isTyping: !!this.typingTimer, + name: this.getName(), + profileName: this.getProfileName(), + title: this.getTitle(), + unreadCount: this.get('unreadCount') || 0, + mentionedUs: this.get('mentionedUs') || false, + isBlocked: this.isBlocked(), + phoneNumber: this.id, + lastMessage: { + status: this.get('lastMessageStatus'), + text: this.get('lastMessage'), + }, + hasNickname: !!this.getNickname(), + isKickedFromGroup: !!this.get('isKickedFromGroup'), + left: !!this.get('left'), + groupAdmins, + members, + onClick: () => this.trigger('select', this), + onBlockContact: () => this.block(), + onUnblockContact: () => this.unblock(), + onCopyPublicKey: this.copyPublicKey, + onDeleteContact: this.deleteContact, + onLeaveGroup: () => { + window.Whisper.events.trigger('leaveGroup', this); + }, + onDeleteMessages: this.deleteMessages, + onInviteContacts: () => { + window.Whisper.events.trigger('inviteContacts', this); + }, + onClearNickname: () => { + void this.setLokiProfile({ displayName: null }); + }, + }; + + return result; + } + + public async updateGroupAdmins(groupAdmins: Array) { + const existingAdmins = _.sortBy(this.getGroupAdmins()); + const newAdmins = _.sortBy(groupAdmins); + + if (_.isEqual(existingAdmins, newAdmins)) { + window.log.info( + 'Skipping updates of groupAdmins/moderators. No change detected.' + ); + return; + } + this.set({ groupAdmins }); + await this.commit(); + } + + public async onReadMessage(message: any, readAt: any) { + // We mark as read everything older than this message - to clean up old stuff + // still marked unread in the database. If the user generally doesn't read in + // the desktop app, so the desktop app only gets read syncs, we can very + // easily end up with messages never marked as read (our previous early read + // sync handling, read syncs never sent because app was offline) + + // We queue it because we often get a whole lot of read syncs at once, and + // their markRead calls could very easily overlap given the async pull from DB. + + // Lastly, we don't send read syncs for any message marked read due to a read + // sync. That's a notification explosion we don't need. + return this.queueJob(() => + this.markRead(message.get('received_at'), { + sendReadReceipts: false, + readAt, + }) + ); + } + + public async getUnread() { + return window.Signal.Data.getUnreadByConversation(this.id, { + MessageCollection: MessageCollection, + }); + } + + public async getUnreadCount() { + return window.Signal.Data.getUnreadCountByConversation(this.id); + } + + public validate(attributes: any) { + const required = ['id', 'type']; + const missing = _.filter(required, attr => !attributes[attr]); + if (missing.length) { + return `Conversation must have ${missing}`; + } + + if (attributes.type !== 'private' && attributes.type !== 'group') { + return `Invalid conversation type: ${attributes.type}`; + } + + const error = this.validateNumber(); + if (error) { + return error; + } + + return null; + } + + public validateNumber() { + if (!this.id) { + return 'Invalid ID'; + } + if (!this.isPrivate()) { + return null; + } + + // Check if it's hex + const isHex = this.id.replace(/[\s]*/g, '').match(/^[0-9a-fA-F]+$/); + if (!isHex) { + return 'Invalid Hex ID'; + } + + // Check if the pubkey length is 33 and leading with 05 or of length 32 + const len = this.id.length; + if ((len !== 33 * 2 || !/^05/.test(this.id)) && len !== 32 * 2) { + return 'Invalid Pubkey Format'; + } + + this.set({ id: this.id }); + return null; + } + + public queueJob(callback: any) { + // tslint:disable-next-line: no-promise-as-boolean + const previous = this.pending || Promise.resolve(); + + const taskWithTimeout = window.textsecure.createTaskWithTimeout( + callback, + `conversation ${this.idForLogging()}` + ); + + this.pending = previous.then(taskWithTimeout, taskWithTimeout); + const current = this.pending; + + current.then(() => { + if (this.pending === current) { + delete this.pending; + } + }); + + return current; + } + public getRecipients() { + if (this.isPrivate()) { + return [this.id]; + } + const me = UserUtils.getOurPubKeyStrFromCache(); + return _.without(this.get('members'), me); + } + + public async getQuoteAttachment(attachments: any, preview: any) { + const { + loadAttachmentData, + getAbsoluteAttachmentPath, + } = window.Signal.Migrations; + + if (attachments && attachments.length) { + return Promise.all( + attachments + .filter( + (attachment: any) => + attachment && + attachment.contentType && + !attachment.pending && + !attachment.error + ) + .slice(0, 1) + .map(async (attachment: any) => { + const { fileName, thumbnail, contentType } = attachment; + + return { + contentType, + // Our protos library complains about this field being undefined, so we + // force it to null + fileName: fileName || null, + thumbnail: thumbnail + ? { + ...(await loadAttachmentData(thumbnail)), + objectUrl: getAbsoluteAttachmentPath(thumbnail.path), + } + : null, + }; + }) + ); + } + + if (preview && preview.length) { + return Promise.all( + preview + .filter((item: any) => item && item.image) + .slice(0, 1) + .map(async (attachment: any) => { + const { image } = attachment; + const { contentType } = image; + + return { + contentType, + // Our protos library complains about this field being undefined, so we + // force it to null + fileName: null, + thumbnail: image + ? { + ...(await loadAttachmentData(image)), + objectUrl: getAbsoluteAttachmentPath(image.path), + } + : null, + }; + }) + ); + } + + return []; + } + + public async makeQuote(quotedMessage: any) { + const { getName } = window.Signal.Types.Contact; + const contact = quotedMessage.getContact(); + const attachments = quotedMessage.get('attachments'); + const preview = quotedMessage.get('preview'); + + const body = quotedMessage.get('body'); + const embeddedContact = quotedMessage.get('contact'); + const embeddedContactName = + embeddedContact && embeddedContact.length > 0 + ? getName(embeddedContact[0]) + : ''; + + return { + author: contact.id, + id: quotedMessage.get('sent_at'), + text: body || embeddedContactName, + attachments: await this.getQuoteAttachment(attachments, preview), + }; + } + + public toOpenGroup() { + if (!this.isPublic()) { + throw new Error('tried to run toOpenGroup for not public group'); + } + + return new OpenGroup({ + server: this.get('server'), + channel: this.get('channelId'), + conversationId: this.id, + }); + } + public async sendMessageJob(message: any) { + try { + const uploads = await message.uploadData(); + const { id } = message; + const expireTimer = this.get('expireTimer'); + const destination = this.id; + + const chatMessage = new ChatMessage({ + body: uploads.body, + identifier: id, + timestamp: message.get('sent_at'), + attachments: uploads.attachments, + expireTimer, + preview: uploads.preview, + quote: uploads.quote, + lokiProfile: this.getOurProfile(), + }); + + if (this.isPublic()) { + const openGroup = this.toOpenGroup(); + + const openGroupParams = { + body: uploads.body, + timestamp: message.get('sent_at'), + group: openGroup, + attachments: uploads.attachments, + preview: uploads.preview, + quote: uploads.quote, + identifier: id, + }; + const openGroupMessage = new OpenGroupMessage(openGroupParams); + // we need the return await so that errors are caught in the catch {} + await getMessageQueue().sendToGroup(openGroupMessage); + return; + } + + const destinationPubkey = new PubKey(destination); + if (this.isPrivate()) { + // Handle Group Invitation Message + if (message.get('groupInvitation')) { + const groupInvitation = message.get('groupInvitation'); + const groupInvitMessage = new GroupInvitationMessage({ + identifier: id, + timestamp: message.get('sent_at'), + serverName: groupInvitation.name, + channelId: groupInvitation.channelId, + serverAddress: groupInvitation.address, + expireTimer: this.get('expireTimer'), + }); + // we need the return await so that errors are caught in the catch {} + await getMessageQueue().sendToPubKey( + destinationPubkey, + groupInvitMessage + ); + return; + } + // we need the return await so that errors are caught in the catch {} + await getMessageQueue().sendToPubKey(destinationPubkey, chatMessage); + return; + } + + if (this.isMediumGroup()) { + const closedGroupChatMessage = new ClosedGroupChatMessage({ + chatMessage, + groupId: destination, + }); + + // we need the return await so that errors are caught in the catch {} + await getMessageQueue().sendToGroup(closedGroupChatMessage); + return; + } + + if (this.isClosedGroup()) { + throw new Error( + 'Legacy group are not supported anymore. You need to recreate this group.' + ); + } + + throw new TypeError(`Invalid conversation type: '${this.get('type')}'`); + } catch (e) { + await message.saveErrors(e); + return null; + } + } + public async sendMessage( + body: string, + attachments: any, + quote: any, + preview: any, + groupInvitation = null + ) { + this.clearTypingTimers(); + + const destination = this.id; + const expireTimer = this.get('expireTimer'); + const recipients = this.getRecipients(); + + const now = Date.now(); + + window.log.info( + 'Sending message to conversation', + this.idForLogging(), + 'with timestamp', + now + ); + // be sure an empty quote is marked as undefined rather than being empty + // otherwise upgradeMessageSchema() will return an object with an empty array + // and this.get('quote') will be true, even if there is no quote. + const editedQuote = _.isEmpty(quote) ? undefined : quote; + const { upgradeMessageSchema } = window.Signal.Migrations; + + const messageWithSchema = await upgradeMessageSchema({ + type: 'outgoing', + body, + conversationId: destination, + quote: editedQuote, + preview, + attachments, + sent_at: now, + received_at: now, + expireTimer, + recipients, + }); + + if (this.isPublic()) { + // Public chats require this data to detect duplicates + messageWithSchema.source = UserUtils.getOurPubKeyStrFromCache(); + messageWithSchema.sourceDevice = 1; + } else { + messageWithSchema.destination = destination; + } + + const attributes: MessageAttributesOptionals = { + ...messageWithSchema, + groupInvitation, + id: window.getGuid(), + conversationId: this.id, + }; + + const model = this.addSingleMessage(attributes); + MessageController.getInstance().register(model.id, model); + + const id = await model.commit(); + model.set({ id }); + + if (this.isPrivate()) { + model.set({ destination }); + } + if (this.isPublic()) { + await model.setServerTimestamp(new Date().getTime()); + } + + window.Whisper.events.trigger('messageAdded', { + conversationKey: this.id, + messageModel: model, + }); + + this.set({ + lastMessage: model.getNotificationText(), + lastMessageStatus: 'sending', + active_at: now, + }); + await this.commit(); + + // We're offline! + if (!window.textsecure.messaging) { + const error = new Error('Network is not available'); + error.name = 'SendMessageNetworkError'; + (error as any).number = this.id; + await model.saveErrors([error]); + return null; + } + + this.queueJob(async () => { + await this.sendMessageJob(model); + }); + return null; + } + + public async updateAvatarOnPublicChat({ url, profileKey }: any) { + if (!this.isPublic()) { + return; + } + if (!this.get('profileSharing')) { + return; + } + + if (profileKey && typeof profileKey !== 'string') { + // eslint-disable-next-line no-param-reassign + // tslint:disable-next-line: no-parameter-reassignment + profileKey = window.Signal.Crypto.arrayBufferToBase64(profileKey); + } + const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer( + this.get('server') + ); + if (!serverAPI) { + return; + } + await serverAPI.setAvatar(url, profileKey); + } + public async bouncyUpdateLastMessage() { + if (!this.id) { + return; + } + if (!this.get('active_at')) { + window.log.info('Skipping update last message as active_at is falsy'); + return; + } + const messages = await window.Signal.Data.getMessagesByConversation( + this.id, + { limit: 1, MessageCollection: MessageCollection } + ); + const lastMessageModel = messages.at(0); + const lastMessageJSON = lastMessageModel ? lastMessageModel.toJSON() : null; + const lastMessageStatusModel = lastMessageModel + ? lastMessageModel.getMessagePropStatus() + : null; + const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate( + { + currentTimestamp: this.get('active_at') || null, + lastMessage: lastMessageJSON, + lastMessageStatus: lastMessageStatusModel, + lastMessageNotificationText: lastMessageModel + ? lastMessageModel.getNotificationText() + : null, + } + ); + // Because we're no longer using Backbone-integrated saves, we need to manually + // clear the changed fields here so our hasChanged() check below is useful. + (this as any).changed = {}; + this.set(lastMessageUpdate); + if (this.hasChanged()) { + await this.commit(); + } + } + + public async updateExpirationTimer( + providedExpireTimer: any, + providedSource?: string, + receivedAt?: number, + options: any = {} + ) { + let expireTimer = providedExpireTimer; + let source = providedSource; + + _.defaults(options, { fromSync: false, fromGroupUpdate: false }); + + if (!expireTimer) { + expireTimer = null; + } + if ( + this.get('expireTimer') === expireTimer || + (!expireTimer && !this.get('expireTimer')) + ) { + return null; + } + + window.log.info("Update conversation 'expireTimer'", { + id: this.idForLogging(), + expireTimer, + source, + }); + + source = source || UserUtils.getOurPubKeyStrFromCache(); + + // When we add a disappearing messages notification to the conversation, we want it + // to be above the message that initiated that change, hence the subtraction. + const timestamp = (receivedAt || Date.now()) - 1; + + this.set({ expireTimer }); + await this.commit(); + + const message = new MessageModel({ + // Even though this isn't reflected to the user, we want to place the last seen + // indicator above it. We set it to 'unread' to trigger that placement. + unread: true, + conversationId: this.id, + // No type; 'incoming' messages are specially treated by conversation.markRead() + sent_at: timestamp, + received_at: timestamp, + flags: SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, + expirationTimerUpdate: { + expireTimer, + source, + fromSync: options.fromSync, + fromGroupUpdate: options.fromGroupUpdate, + }, + expireTimer: 0, + type: 'incoming', + }); + + message.set({ destination: this.id }); + + if (message.isOutgoing()) { + message.set({ recipients: this.getRecipients() }); + } + + const id = await message.commit(); + + message.set({ id }); + window.Whisper.events.trigger('messageAdded', { + conversationKey: this.id, + messageModel: message, + }); + + await this.commit(); + + // if change was made remotely, don't send it to the number/group + if (receivedAt) { + return message; + } + + let profileKey; + if (this.get('profileSharing')) { + profileKey = window.storage.get('profileKey'); + } + + const expireUpdate = { + identifier: id, + timestamp, + expireTimer, + profileKey, + }; + + if (!expireUpdate.expireTimer) { + delete expireUpdate.expireTimer; + } + + if (this.isMe()) { + const expirationTimerMessage = new ExpirationTimerUpdateMessage( + expireUpdate + ); + return message.sendSyncMessageOnly(expirationTimerMessage); + } + + if (this.get('type') === 'private') { + const expirationTimerMessage = new ExpirationTimerUpdateMessage( + expireUpdate + ); + const pubkey = new PubKey(this.get('id')); + await getMessageQueue().sendToPubKey(pubkey, expirationTimerMessage); + } else { + const expireUpdateForGroup = { + ...expireUpdate, + groupId: this.get('id'), + }; + + const expirationTimerMessage = new ExpirationTimerUpdateMessage( + expireUpdateForGroup + ); + // special case when we are the only member of a closed group + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + + if ( + this.get('members').length === 1 && + this.get('members')[0] === ourNumber + ) { + return message.sendSyncMessageOnly(expirationTimerMessage); + } + await getMessageQueue().sendToGroup(expirationTimerMessage); + } + return message; + } + + public isSearchable() { + return !this.get('left'); + } + + public async commit() { + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: ConversationModel, + }); + this.trigger('change', this); + } + + public async addMessage(messageAttributes: MessageAttributesOptionals) { + const model = new MessageModel(messageAttributes); + + const messageId = await model.commit(); + model.set({ id: messageId }); + window.Whisper.events.trigger('messageAdded', { + conversationKey: this.id, + messageModel: model, + }); + return model; + } + + public async leaveGroup() { + if (this.get('type') !== ConversationType.GROUP) { + window.log.error('Cannot leave a non-group conversation'); + return; + } + + if (this.isMediumGroup()) { + await leaveClosedGroup(this.id); + } else { + throw new Error( + 'Legacy group are not supported anymore. You need to create this group again.' + ); + } + } + + public async markRead(newestUnreadDate: any, providedOptions: any = {}) { + const options = providedOptions || {}; + _.defaults(options, { sendReadReceipts: true }); + + const conversationId = this.id; + window.Whisper.Notifications.remove( + window.Whisper.Notifications.where({ + conversationId, + }) + ); + let unreadMessages = await this.getUnread(); + + const oldUnread = unreadMessages.filter( + (message: any) => message.get('received_at') <= newestUnreadDate + ); + + let read = await Promise.all( + _.map(oldUnread, async providedM => { + const m = MessageController.getInstance().register( + providedM.id, + providedM + ); + + await m.markRead(options.readAt); + const errors = m.get('errors'); + return { + sender: m.get('source'), + timestamp: m.get('sent_at'), + hasErrors: Boolean(errors && errors.length), + }; + }) + ); + + // Some messages we're marking read are local notifications with no sender + read = _.filter(read, m => Boolean(m.sender)); + const realUnreadCount = await this.getUnreadCount(); + if (read.length === 0) { + const cachedUnreadCountOnConvo = this.get('unreadCount'); + if (cachedUnreadCountOnConvo !== read.length) { + // reset the unreadCount on the convo to the real one coming from markRead messages on the db + this.set({ unreadCount: 0 }); + await this.commit(); + } else { + // window.log.info('markRead(): nothing newly read.'); + } + return; + } + unreadMessages = unreadMessages.filter((m: any) => Boolean(m.isIncoming())); + + this.set({ unreadCount: realUnreadCount }); + + const mentionRead = (() => { + const stillUnread = unreadMessages.filter( + (m: any) => m.get('received_at') > newestUnreadDate + ); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + return !stillUnread.some( + (m: any) => + m.propsForMessage && + m.propsForMessage.text && + m.propsForMessage.text.indexOf(`@${ourNumber}`) !== -1 + ); + })(); + + if (mentionRead) { + this.set({ mentionedUs: false }); + } + + await this.commit(); + + // If a message has errors, we don't want to send anything out about it. + // read syncs - let's wait for a client that really understands the message + // to mark it read. we'll mark our local error read locally, though. + // read receipts - here we can run into infinite loops, where each time the + // conversation is viewed, another error message shows up for the contact + read = read.filter(item => !item.hasErrors); + + if (this.isPublic()) { + window.log.debug('public conversation... No need to send read receipt'); + return; + } + + if (this.isPrivate() && read.length && options.sendReadReceipts) { + window.log.info(`Sending ${read.length} read receipts`); + if (window.storage.get('read-receipt-setting')) { + await Promise.all( + _.map(_.groupBy(read, 'sender'), async (receipts, sender) => { + const timestamps = _.map(receipts, 'timestamp').filter( + t => !!t + ) as Array; + const receiptMessage = new ReadReceiptMessage({ + timestamp: Date.now(), + timestamps, + }); + + const device = new PubKey(sender); + await getMessageQueue().sendToPubKey(device, receiptMessage); + }) + ); + } + } + } + + // LOKI PROFILES + public async setNickname(nickname: string) { + const trimmed = nickname && nickname.trim(); + if (this.get('nickname') === trimmed) { + return; + } + + this.set({ nickname: trimmed }); + await this.commit(); + + await this.updateProfileName(); + } + public async setLokiProfile(newProfile: any) { + if (!_.isEqual(this.get('profile'), newProfile)) { + this.set({ profile: newProfile }); + await this.commit(); + } + + // a user cannot remove an avatar. Only change it + // if you change this behavior, double check all setLokiProfile calls (especially the one in EditProfileDialog) + if (newProfile.avatar) { + await this.setProfileAvatar({ path: newProfile.avatar }); + } + + await this.updateProfileName(); + } + public async updateProfileName() { + // Prioritise nickname over the profile display name + const nickname = this.getNickname(); + const profile = this.getLokiProfile(); + const displayName = profile && profile.displayName; + + const profileName = nickname || displayName || null; + await this.setProfileName(profileName); + } + public getLokiProfile() { + return this.get('profile'); + } + public getNickname() { + return this.get('nickname'); + } + // maybe "Backend" instead of "Source"? + public async setPublicSource(newServer: any, newChannelId: any) { + if (!this.isPublic()) { + window.log.warn( + `trying to setPublicSource on non public chat conversation ${this.id}` + ); + return; + } + if ( + this.get('server') !== newServer || + this.get('channelId') !== newChannelId + ) { + // mark active so it's not in the contacts list but in the conversation list + this.set({ + server: newServer, + channelId: newChannelId, + active_at: Date.now(), + }); + await this.commit(); + } + } + public getPublicSource() { + if (!this.isPublic()) { + window.log.warn( + `trying to getPublicSource on non public chat conversation ${this.id}` + ); + return null; + } + return { + server: this.get('server'), + channelId: this.get('channelId'), + conversationId: this.get('id'), + }; + } + public async getPublicSendData() { + const channelAPI = await window.lokiPublicChatAPI.findOrCreateChannel( + this.get('server'), + this.get('channelId'), + this.id + ); + return channelAPI; + } + public getLastRetrievedMessage() { + if (!this.isPublic()) { + return null; + } + const lastMessageId = this.get('lastPublicMessage') || 0; + return lastMessageId; + } + public async setLastRetrievedMessage(newLastMessageId: any) { + if (!this.isPublic()) { + return; + } + if (this.get('lastPublicMessage') !== newLastMessageId) { + this.set({ lastPublicMessage: newLastMessageId }); + await this.commit(); + } + } + public isAdmin(pubKey?: string) { + if (!this.isPublic()) { + return false; + } + if (!pubKey) { + throw new Error('isAdmin() pubKey is falsy'); + } + const groupAdmins = this.getGroupAdmins(); + return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey); + } + // SIGNAL PROFILES + public async getProfiles() { + // request all conversation members' keys + let ids = []; + if (this.isPrivate()) { + ids = [this.id]; + } else { + ids = this.get('members'); + } + return Promise.all(_.map(ids, this.getProfile)); + } + + // This function is wrongly named by signal + // This is basically an `update` function and thus we have overwritten it with such + public async getProfile(id: string) { + const c = await ConversationController.getInstance().getOrCreateAndWait( + id, + 'private' + ); + + // We only need to update the profile as they are all stored inside the conversation + await c.updateProfileName(); + } + public async setProfileName(name: string) { + const profileName = this.get('profileName'); + if (profileName !== name) { + this.set({ profileName: name }); + await this.commit(); + } + } + public async setGroupName(name: string) { + const profileName = this.get('name'); + if (profileName !== name) { + this.set({ name }); + await this.commit(); + } + } + public async setSubscriberCount(count: number) { + this.set({ subscriberCount: count }); + // Not sure if we care about updating the database + } + public async setGroupNameAndAvatar(name: any, avatarPath: any) { + const currentName = this.get('name'); + const profileAvatar = this.get('profileAvatar'); + if (profileAvatar !== avatarPath || currentName !== name) { + // only update changed items + if (profileAvatar !== avatarPath) { + this.set({ profileAvatar: avatarPath }); + } + if (currentName !== name) { + this.set({ name }); + } + // save + await this.commit(); + } + } + public async setProfileAvatar(avatar: any) { + const profileAvatar = this.get('profileAvatar'); + if (profileAvatar !== avatar) { + this.set({ profileAvatar: avatar }); + await this.commit(); + } + } + public async setProfileKey(profileKey: any) { + // profileKey is a string so we can compare it directly + if (this.get('profileKey') !== profileKey) { + this.set({ + profileKey, + accessKey: null, + }); + + await this.deriveAccessKeyIfNeeded(); + + await this.commit(); + } + } + + public async deriveAccessKeyIfNeeded() { + const profileKey = this.get('profileKey'); + if (!profileKey) { + return; + } + if (this.get('accessKey')) { + return; + } + + try { + const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer( + profileKey + ); + const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey( + profileKeyBuffer + ); + const accessKey = window.Signal.Crypto.arrayBufferToBase64( + accessKeyBuffer + ); + this.set({ accessKey }); + } catch (e) { + window.log.warn(`Failed to derive access key for ${this.id}`); + } + } + + public async upgradeMessages(messages: any) { + // tslint:disable-next-line: one-variable-per-declaration + for (let max = messages.length, i = 0; i < max; i += 1) { + const message = messages.at(i); + const { attributes } = message; + const { schemaVersion } = attributes; + + if ( + schemaVersion < window.Signal.Types.Message.VERSION_NEEDED_FOR_DISPLAY + ) { + // Yep, we really do want to wait for each of these + // eslint-disable-next-line no-await-in-loop + const { upgradeMessageSchema } = window.Signal.Migrations; + + const upgradedMessage = await upgradeMessageSchema(attributes); + message.set(upgradedMessage); + // eslint-disable-next-line no-await-in-loop + await upgradedMessage.commit(); + } + } + } + + public hasMember(pubkey: string) { + return _.includes(this.get('members'), pubkey); + } + // returns true if this is a closed/medium or open group + public isGroup() { + return this.get('type') === 'group'; + } + + public copyPublicKey() { + window.clipboard.writeText(this.id); + + ToastUtils.pushCopiedToClipBoard(); + } + + public changeNickname() { + window.Whisper.events.trigger('showNicknameDialog', { + pubKey: this.id, + nickname: this.getNickname(), + onOk: (newName: string) => this.setNickname(newName), + }); + } + + public deleteContact() { + let title = window.i18n('delete'); + let message = window.i18n('deleteContactConfirmation'); + + if (this.isGroup()) { + title = window.i18n('leaveGroup'); + message = window.i18n('leaveGroupConfirmation'); + } + + window.confirmationDialog({ + title, + message, + resolve: () => { + void ConversationController.getInstance().deleteContact(this.id); + }, + }); + } + + public async deletePublicMessages(messages: Array) { + const channelAPI = await this.getPublicSendData(); + + if (!channelAPI) { + throw new Error('Unable to get public channel API'); + } + + const invalidMessages = messages.filter(m => !m.attributes.serverId); + const pendingMessages = messages.filter(m => m.attributes.serverId); + + let deletedServerIds = []; + let ignoredServerIds = []; + + if (pendingMessages.length > 0) { + const result = await channelAPI.deleteMessages( + pendingMessages.map(m => m.attributes.serverId) + ); + deletedServerIds = result.deletedIds; + ignoredServerIds = result.ignoredIds; + } + + const toDeleteLocallyServerIds = _.union( + deletedServerIds, + ignoredServerIds + ); + let toDeleteLocally = messages.filter(m => + toDeleteLocallyServerIds.includes(m.attributes.serverId) + ); + toDeleteLocally = _.union(toDeleteLocally, invalidMessages); + + await Promise.all( + toDeleteLocally.map(async m => { + await this.removeMessage(m.id); + }) + ); + + return toDeleteLocally; + } + + public async removeMessage(messageId: any) { + await Data.removeMessage(messageId, { + Message: MessageModel, + }); + window.Whisper.events.trigger('messageDeleted', { + conversationKey: this.id, + messageId, + }); + } + + public deleteMessages() { + let params; + if (this.isPublic()) { + throw new Error( + 'Called deleteMessages() on an open group. Only leave group is supported.' + ); + } else { + params = { + title: window.i18n('deleteMessages'), + message: window.i18n('deleteConversationConfirmation'), + resolve: () => this.destroyMessages(), + }; + } + + window.confirmationDialog(params); + } + + public async destroyMessages() { + await window.Signal.Data.removeAllMessagesInConversation(this.id, { + MessageCollection, + }); + + window.Whisper.events.trigger('conversationReset', { + conversationKey: this.id, + }); + // destroy message keeps the active timestamp set so the + // conversation still appears on the conversation list but is empty + this.set({ + lastMessage: null, + unreadCount: 0, + mentionedUs: false, + }); + + await this.commit(); + } + + public getName() { + if (this.isPrivate()) { + return this.get('name'); + } + return this.get('name') || window.i18n('unknown'); + } + + public getTitle() { + if (this.isPrivate()) { + const profileName = this.getProfileName(); + const number = this.getNumber(); + const name = profileName + ? `${profileName} (${PubKey.shorten(number)})` + : number; + + return this.get('name') || name; + } + return this.get('name') || 'Unknown group'; + } + + /** + * For a private convo, returns the loki profilename if set, or a shortened + * version of the contact pubkey. + * Throws an error if called on a group convo. + * + */ + public getContactProfileNameOrShortenedPubKey() { + if (!this.isPrivate()) { + throw new Error( + 'getContactProfileNameOrShortenedPubKey() cannot be called with a non private convo.' + ); + } + + const profileName = this.get('profileName'); + const pubkey = this.id; + if (UserUtils.isUsFromCache(pubkey)) { + return window.i18n('you'); + } + return profileName || PubKey.shorten(pubkey); + } + + /** + * For a private convo, returns the loki profilename if set, or a full length + * version of the contact pubkey. + * Throws an error if called on a group convo. + */ + public getContactProfileNameOrFullPubKey() { + if (!this.isPrivate()) { + throw new Error( + 'getContactProfileNameOrFullPubKey() cannot be called with a non private convo.' + ); + } + const profileName = this.get('profileName'); + const pubkey = this.id; + if (UserUtils.isUsFromCache(pubkey)) { + return window.i18n('you'); + } + return profileName || pubkey; + } + + public getProfileName() { + if (this.isPrivate() && !this.get('name')) { + return this.get('profileName'); + } + return null; + } + + /** + * Returns + * displayName: string; + * avatarPointer: string; + * profileKey: Uint8Array; + */ + public getOurProfile(): OurLokiProfile | undefined { + try { + // Secondary devices have their profile stored + // in their primary device's conversation + const ourNumber = window.storage.get('primaryDevicePubKey'); + const ourConversation = ConversationController.getInstance().get( + ourNumber + ); + let profileKey = null; + if (this.get('profileSharing')) { + profileKey = new Uint8Array(window.storage.get('profileKey')); + } + const avatarPointer = ourConversation.get('avatarPointer'); + const { displayName } = ourConversation.getLokiProfile(); + return { displayName, avatarPointer, profileKey }; + } catch (e) { + window.log.error(`Failed to get our profile: ${e}`); + return undefined; + } + } + + public getNumber() { + if (!this.isPrivate()) { + return ''; + } + return this.id; + } + + public isPrivate() { + return this.get('type') === 'private'; + } + + public getAvatarPath() { + const avatar = this.get('avatar') || this.get('profileAvatar'); + if (typeof avatar === 'string') { + return avatar; + } + + if (avatar && avatar.path && typeof avatar.path === 'string') { + const { getAbsoluteAttachmentPath } = window.Signal.Migrations; + + return getAbsoluteAttachmentPath(avatar.path) as string; + } + + return null; + } + public getAvatar() { + const url = this.getAvatarPath(); + + return { url: url || null }; + } + + public async getNotificationIcon() { + return new Promise(resolve => { + const avatar = this.getAvatar(); + if (avatar.url) { + resolve(avatar.url); + } else { + resolve(new window.Whisper.IdenticonSVGView(avatar).getDataUrl()); + } + }); + } + + public async notify(message: any) { + if (!message.isIncoming()) { + return Promise.resolve(); + } + const conversationId = this.id; + + return ConversationController.getInstance() + .getOrCreateAndWait(message.get('source'), 'private') + .then(sender => + sender.getNotificationIcon().then((iconUrl: any) => { + const messageJSON = message.toJSON(); + const messageSentAt = messageJSON.sent_at; + const messageId = message.id; + const isExpiringMessage = this.isExpiringMessage(messageJSON); + + // window.log.info('Add notification', { + // conversationId: this.idForLogging(), + // isExpiringMessage, + // messageSentAt, + // }); + window.Whisper.Notifications.add({ + conversationId, + iconUrl, + isExpiringMessage, + message: message.getNotificationText(), + messageId, + messageSentAt, + title: sender.getTitle(), + }); + }) + ); + } + public async notifyTyping({ isTyping, sender }: any) { + // We don't do anything with typing messages from our other devices + if (UserUtils.isUsFromCache(sender)) { + return; + } + + // typing only works for private chats for now + if (!this.isPrivate()) { + return; + } + + const wasTyping = !!this.typingTimer; + if (this.typingTimer) { + clearTimeout(this.typingTimer); + this.typingTimer = null; + } + + // Note: We trigger two events because: + // 'change' causes a re-render of this conversation's list item in the left pane + + if (isTyping) { + this.typingTimer = global.setTimeout( + this.clearContactTypingTimer.bind(this, sender), + 15 * 1000 + ); + + if (!wasTyping) { + // User was not previously typing before. State change! + await this.commit(); + } + } else { + // tslint:disable-next-line: no-dynamic-delete + this.typingTimer = null; + if (wasTyping) { + // User was previously typing, and is no longer. State change! + await this.commit(); + } + } + } + + public async clearContactTypingTimer(sender: string) { + if (!!this.typingTimer) { + clearTimeout(this.typingTimer); + this.typingTimer = null; + + // User was previously typing, but timed out or we received message. State change! + await this.commit(); + } + } + + private isExpiringMessage(json: any) { + if (json.type === 'incoming') { + return false; + } + + const { expireTimer } = json; + + return typeof expireTimer === 'number' && expireTimer > 0; + } +} + +export class ConversationCollection extends Backbone.Collection< + ConversationModel +> { + constructor(models?: Array) { + super(models); + this.comparator = (m: ConversationModel) => { + return -m.get('active_at'); + }; + } +} +ConversationCollection.prototype.model = ConversationModel; diff --git a/ts/models/index.ts b/ts/models/index.ts new file mode 100644 index 0000000000..274a5f1a74 --- /dev/null +++ b/ts/models/index.ts @@ -0,0 +1,6 @@ +import * as Conversation from './conversation'; + +import * as Message from './message'; +import * as MessageType from './messageType'; + +export { Conversation, Message, MessageType }; diff --git a/ts/models/message.ts b/ts/models/message.ts new file mode 100644 index 0000000000..8a1bb1e277 --- /dev/null +++ b/ts/models/message.ts @@ -0,0 +1,1394 @@ +import Backbone from 'backbone'; +// tslint:disable-next-line: match-default-export-name +import filesize from 'filesize'; +import _ from 'lodash'; +import { SignalService } from '../../ts/protobuf'; +import { getMessageQueue, Types, Utils } from '../../ts/session'; +import { ConversationController } from '../../ts/session/conversations'; +import { MessageController } from '../../ts/session/messages'; +import { + ChatMessage, + DataMessage, + OpenGroupMessage, +} from '../../ts/session/messages/outgoing'; +import { ClosedGroupChatMessage } from '../../ts/session/messages/outgoing/content/data/group'; +import { EncryptionType, PubKey } from '../../ts/session/types'; +import { ToastUtils, UserUtils } from '../../ts/session/utils'; +import { + fillMessageAttributesWithDefaults, + MessageAttributes, + MessageAttributesOptionals, +} from './messageType'; + +export class MessageModel extends Backbone.Model { + public propsForTimerNotification: any; + public propsForGroupNotification: any; + public propsForGroupInvitation: any; + public propsForSearchResult: any; + public propsForMessage: any; + + constructor(attributes: MessageAttributesOptionals) { + const filledAttrs = fillMessageAttributesWithDefaults(attributes); + super(filledAttrs); + + if (_.isObject(filledAttrs)) { + this.set( + window.Signal.Types.Message.initializeSchemaVersion({ + message: filledAttrs, + logger: window.log, + }) + ); + } + + this.on('destroy', this.onDestroy); + this.on('change:expirationStartTimestamp', this.setToExpire); + this.on('change:expireTimer', this.setToExpire); + // this.on('expired', this.onExpired); + void this.setToExpire(); + // Keep props ready + const generateProps = (triggerEvent = true) => { + if (this.isExpirationTimerUpdate()) { + this.propsForTimerNotification = this.getPropsForTimerNotification(); + } else if (this.isGroupUpdate()) { + this.propsForGroupNotification = this.getPropsForGroupNotification(); + } else if (this.isGroupInvitation()) { + this.propsForGroupInvitation = this.getPropsForGroupInvitation(); + } else { + this.propsForSearchResult = this.getPropsForSearchResult(); + this.propsForMessage = this.getPropsForMessage(); + } + if (triggerEvent) { + window.Whisper.events.trigger('messageChanged', this); + } + }; + this.on('change', generateProps); + window.contextMenuShown = false; + + generateProps(false); + } + + public idForLogging() { + return `${this.get('source')} ${this.get('sent_at')}`; + } + + public isExpirationTimerUpdate() { + const expirationTimerFlag = + SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; + const flags = this.get('flags'); + if (!flags) { + return false; + } + // eslint-disable-next-line no-bitwise + // tslint:disable-next-line: no-bitwise + return !!(flags & expirationTimerFlag); + } + + public isGroupUpdate() { + return Boolean(this.get('group_update')); + } + + public isIncoming() { + return this.get('type') === 'incoming'; + } + + public isUnread() { + return !!this.get('unread'); + } + + // Important to allow for this.unset('unread'), save to db, then fetch() + // to propagate. We don't want the unset key in the db so our unread index + // stays small. + public merge(model: any) { + const attributes = model.attributes || model; + + const { unread } = attributes; + if (unread === undefined) { + this.unset('unread'); + } + + this.set(attributes); + } + + // tslint:disable-next-line: cyclomatic-complexity + public getDescription() { + if (this.isGroupUpdate()) { + const groupUpdate = this.get('group_update'); + const ourPrimary = window.textsecure.storage.get('primaryDevicePubKey'); + if ( + groupUpdate.left === 'You' || + (Array.isArray(groupUpdate.left) && + groupUpdate.left.length === 1 && + groupUpdate.left[0] === ourPrimary) + ) { + return window.i18n('youLeftTheGroup'); + } else if (groupUpdate.left) { + return window.i18n( + 'leftTheGroup', + ConversationController.getInstance().getContactProfileNameOrShortenedPubKey( + groupUpdate.left + ) + ); + } + + if (groupUpdate.kicked === 'You') { + return window.i18n('youGotKickedFromGroup'); + } + + const messages = []; + if (!groupUpdate.name && !groupUpdate.joined && !groupUpdate.kicked) { + messages.push(window.i18n('updatedTheGroup')); + } + if (groupUpdate.name) { + messages.push(window.i18n('titleIsNow', groupUpdate.name)); + } + if (groupUpdate.joined && groupUpdate.joined.length) { + const names = groupUpdate.joined.map((pubKey: string) => + ConversationController.getInstance().getContactProfileNameOrFullPubKey( + pubKey + ) + ); + + if (names.length > 1) { + messages.push( + window.i18n('multipleJoinedTheGroup', names.join(', ')) + ); + } else { + messages.push(window.i18n('joinedTheGroup', names[0])); + } + } + + if (groupUpdate.kicked && groupUpdate.kicked.length) { + const names = _.map( + groupUpdate.kicked, + ConversationController.getInstance() + .getContactProfileNameOrShortenedPubKey + ); + + if (names.length > 1) { + messages.push( + window.i18n('multipleKickedFromTheGroup', names.join(', ')) + ); + } else { + messages.push(window.i18n('kickedFromTheGroup', names[0])); + } + } + return messages.join(' '); + } + if (this.isIncoming() && this.hasErrors()) { + return window.i18n('incomingError'); + } + if (this.isGroupInvitation()) { + return `<${window.i18n('groupInvitation')}>`; + } + return this.get('body'); + } + + public isGroupInvitation() { + return !!this.get('groupInvitation'); + } + + public getNotificationText() { + let description = this.getDescription(); + if (description) { + // regex with a 'g' to ignore part groups + const regex = new RegExp(`@${PubKey.regexForPubkeys}`, 'g'); + const pubkeysInDesc = description.match(regex); + (pubkeysInDesc || []).forEach((pubkey: string) => { + const displayName = ConversationController.getInstance().getContactProfileNameOrShortenedPubKey( + pubkey.slice(1) + ); + if (displayName && displayName.length) { + description = description.replace(pubkey, `@${displayName}`); + } + }); + return description; + } + if ((this.get('attachments') || []).length > 0) { + return window.i18n('mediaMessage'); + } + if (this.isExpirationTimerUpdate()) { + const { expireTimer } = this.get('expirationTimerUpdate'); + if (!expireTimer) { + return window.i18n('disappearingMessagesDisabled'); + } + + return window.i18n( + 'timerSetTo', + window.Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0) + ); + } + const contacts = this.get('contact'); + if (contacts && contacts.length) { + return window.Signal.Types.Contact.getName(contacts[0]); + } + + return ''; + } + + public onDestroy() { + void this.cleanup(); + } + + public async cleanup() { + MessageController.getInstance().unregister(this.id); + await window.Signal.Migrations.deleteExternalMessageFiles(this.attributes); + } + + public getPropsForTimerNotification() { + const timerUpdate = this.get('expirationTimerUpdate'); + if (!timerUpdate) { + return null; + } + + const { expireTimer, fromSync, source } = timerUpdate; + const timespan = window.Whisper.ExpirationTimerOptions.getName( + expireTimer || 0 + ); + const disabled = !expireTimer; + + const basicProps = { + type: 'fromOther', + ...this.findAndFormatContact(source), + timespan, + disabled, + }; + + if (fromSync) { + return { + ...basicProps, + type: 'fromSync', + }; + } else if (UserUtils.isUsFromCache(source)) { + return { + ...basicProps, + type: 'fromMe', + }; + } + + return basicProps; + } + + public getPropsForGroupInvitation() { + const invitation = this.get('groupInvitation'); + + let direction = this.get('direction'); + if (!direction) { + direction = this.get('type') === 'outgoing' ? 'outgoing' : 'incoming'; + } + + return { + serverName: invitation.serverName, + serverAddress: invitation.serverAddress, + direction, + onClick: () => { + window.Whisper.events.trigger( + 'publicChatInvitationAccepted', + invitation.serverAddress, + invitation.channelId + ); + }, + }; + } + + public findContact(pubkey: string) { + return ConversationController.getInstance().get(pubkey); + } + + public findAndFormatContact(pubkey: string) { + const contactModel = this.findContact(pubkey); + let profileName; + if (pubkey === window.storage.get('primaryDevicePubKey')) { + profileName = window.i18n('you'); + } else { + profileName = contactModel ? contactModel.getProfileName() : null; + } + + return { + phoneNumber: pubkey, + color: null, + avatarPath: contactModel ? contactModel.getAvatarPath() : null, + name: contactModel ? contactModel.getName() : null, + profileName, + title: contactModel ? contactModel.getTitle() : null, + }; + } + + public getPropsForGroupNotification() { + const groupUpdate = this.get('group_update'); + const changes = []; + + if (!groupUpdate.name && !groupUpdate.left && !groupUpdate.joined) { + changes.push({ + type: 'general', + }); + } + + if (groupUpdate.joined) { + changes.push({ + type: 'add', + contacts: _.map( + Array.isArray(groupUpdate.joined) + ? groupUpdate.joined + : [groupUpdate.joined], + phoneNumber => this.findAndFormatContact(phoneNumber) + ), + }); + } + + if (groupUpdate.kicked === 'You') { + changes.push({ + type: 'kicked', + isMe: true, + }); + } else if (groupUpdate.kicked) { + changes.push({ + type: 'kicked', + contacts: _.map( + Array.isArray(groupUpdate.kicked) + ? groupUpdate.kicked + : [groupUpdate.kicked], + phoneNumber => this.findAndFormatContact(phoneNumber) + ), + }); + } + + if (groupUpdate.left === 'You') { + changes.push({ + type: 'remove', + isMe: true, + }); + } else if (groupUpdate.left) { + if ( + Array.isArray(groupUpdate.left) && + groupUpdate.left.length === 1 && + groupUpdate.left[0] === UserUtils.getOurPubKeyStrFromCache() + ) { + changes.push({ + type: 'remove', + isMe: true, + }); + } else { + changes.push({ + type: 'remove', + contacts: _.map( + Array.isArray(groupUpdate.left) + ? groupUpdate.left + : [groupUpdate.left], + phoneNumber => this.findAndFormatContact(phoneNumber) + ), + }); + } + } + + if (groupUpdate.name) { + changes.push({ + type: 'name', + newName: groupUpdate.name, + }); + } + + return { + changes, + }; + } + + public getMessagePropStatus() { + if (this.hasErrors()) { + return 'error'; + } + + // Only return the status on outgoing messages + if (!this.isOutgoing()) { + return null; + } + + const readBy = this.get('read_by') || []; + if (window.storage.get('read-receipt-setting') && readBy.length > 0) { + return 'read'; + } + const delivered = this.get('delivered'); + const deliveredTo = this.get('delivered_to') || []; + if (delivered || deliveredTo.length > 0) { + return 'delivered'; + } + const sent = this.get('sent'); + const sentTo = this.get('sent_to') || []; + if (sent || sentTo.length > 0) { + return 'sent'; + } + const calculatingPoW = this.get('calculatingPoW'); + if (calculatingPoW) { + return 'pow'; + } + + return 'sending'; + } + + public getPropsForSearchResult() { + const fromNumber = this.getSource(); + const from = this.findAndFormatContact(fromNumber); + if (fromNumber === UserUtils.getOurPubKeyStrFromCache()) { + (from as any).isMe = true; + } + + const toNumber = this.get('conversationId'); + let to = this.findAndFormatContact(toNumber) as any; + if (toNumber === UserUtils.getOurPubKeyStrFromCache()) { + to.isMe = true; + } else if (fromNumber === toNumber) { + to = { + isMe: true, + }; + } + + return { + from, + to, + + // isSelected: this.isSelected, + + id: this.id, + conversationId: this.get('conversationId'), + receivedAt: this.get('received_at'), + snippet: this.get('snippet'), + }; + } + + public getPropsForMessage(options: any = {}) { + const phoneNumber = this.getSource(); + const contact = this.findAndFormatContact(phoneNumber); + const contactModel = this.findContact(phoneNumber); + + const authorAvatarPath = contactModel ? contactModel.getAvatarPath() : null; + + const expirationLength = this.get('expireTimer') * 1000; + const expireTimerStart = this.get('expirationStartTimestamp'); + const expirationTimestamp = + expirationLength && expireTimerStart + ? expireTimerStart + expirationLength + : null; + + // TODO: investigate why conversation is undefined + // for the public group chat + const conversation = this.getConversation(); + + const convoId = conversation ? conversation.id : undefined; + const isGroup = !!conversation && !conversation.isPrivate(); + const isPublic = !!this.get('isPublic'); + + const attachments = this.get('attachments') || []; + + return { + text: this.createNonBreakingLastSeparator(this.get('body')), + id: this.id, + direction: this.isIncoming() ? 'incoming' : 'outgoing', + timestamp: this.get('sent_at'), + serverTimestamp: this.get('serverTimestamp'), + status: this.getMessagePropStatus(), + authorName: contact.name, + authorProfileName: contact.profileName, + authorPhoneNumber: contact.phoneNumber, + conversationType: isGroup ? 'group' : 'direct', + convoId, + attachments: attachments + .filter((attachment: any) => !attachment.error) + .map((attachment: any) => this.getPropsForAttachment(attachment)), + previews: this.getPropsForPreview(), + quote: this.getPropsForQuote(options), + authorAvatarPath, + isUnread: this.isUnread(), + expirationLength, + expirationTimestamp, + isPublic, + isKickedFromGroup: conversation && conversation.get('isKickedFromGroup'), + + onCopyText: this.copyText, + onCopyPubKey: this.copyPubKey, + onBanUser: this.banUser, + onRetrySend: this.retrySend, + markRead: this.markRead, + + onShowUserDetails: (pubkey: string) => + window.Whisper.events.trigger('onShowUserDetails', { + userPubKey: pubkey, + }), + }; + } + + public createNonBreakingLastSeparator(text?: string) { + if (!text) { + return null; + } + + const nbsp = '\xa0'; + const regex = /(\S)( +)(\S+\s*)$/; + return text.replace(regex, (_match, start, spaces, end) => { + const newSpaces = + end.length < 12 + ? _.reduce(spaces, accumulator => accumulator + nbsp, '') + : spaces; + return `${start}${newSpaces}${end}`; + }); + } + + public processQuoteAttachment(attachment: any) { + const { thumbnail } = attachment; + const path = + thumbnail && + thumbnail.path && + window.Signal.Migrations.getAbsoluteAttachmentPath(thumbnail.path); + const objectUrl = thumbnail && thumbnail.objectUrl; + + const thumbnailWithObjectUrl = + !path && !objectUrl + ? null + : // tslint:disable: prefer-object-spread + Object.assign({}, attachment.thumbnail || {}, { + objectUrl: path || objectUrl, + }); + + return Object.assign({}, attachment, { + isVoiceMessage: window.Signal.Types.Attachment.isVoiceMessage(attachment), + thumbnail: thumbnailWithObjectUrl, + }); + // tslint:enable: prefer-object-spread + } + + public getPropsForPreview() { + // Don't generate link previews if user has turned them off + if (!window.storage.get('link-preview-setting', false)) { + return null; + } + + const previews = this.get('preview') || []; + + return previews.map((preview: any) => { + let image = null; + try { + if (preview.image) { + image = this.getPropsForAttachment(preview.image); + } + } catch (e) { + window.log.info('Failed to show preview'); + } + + return { + ...preview, + domain: window.Signal.LinkPreviews.getDomain(preview.url), + image, + }; + }); + } + + public getPropsForQuote(options: any = {}) { + const { noClick } = options; + const quote = this.get('quote'); + + if (!quote) { + return null; + } + + const { author, id, referencedMessageNotFound } = quote; + const contact = author && ConversationController.getInstance().get(author); + + const authorName = contact ? contact.getName() : null; + const isFromMe = contact + ? contact.id === UserUtils.getOurPubKeyStrFromCache() + : false; + const onClick = noClick + ? null + : (event: any) => { + event.stopPropagation(); + this.trigger('scroll-to-message', { + author, + id, + referencedMessageNotFound, + }); + }; + + const firstAttachment = quote.attachments && quote.attachments[0]; + + return { + text: this.createNonBreakingLastSeparator(quote.text), + attachment: firstAttachment + ? this.processQuoteAttachment(firstAttachment) + : null, + isFromMe, + authorPhoneNumber: author, + messageId: id, + authorName, + onClick, + referencedMessageNotFound, + }; + } + + public getPropsForAttachment(attachment: any) { + if (!attachment) { + return null; + } + + const { path, pending, flags, size, screenshot, thumbnail } = attachment; + + return { + ...attachment, + fileSize: size ? filesize(size) : null, + isVoiceMessage: + flags && + // eslint-disable-next-line no-bitwise + // tslint:disable-next-line: no-bitwise + flags & SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, + pending, + url: path + ? window.Signal.Migrations.getAbsoluteAttachmentPath(path) + : null, + screenshot: screenshot + ? { + ...screenshot, + url: window.Signal.Migrations.getAbsoluteAttachmentPath( + screenshot.path + ), + } + : null, + thumbnail: thumbnail + ? { + ...thumbnail, + url: window.Signal.Migrations.getAbsoluteAttachmentPath( + thumbnail.path + ), + } + : null, + }; + } + + public async getPropsForMessageDetail() { + const newIdentity = window.i18n('newIdentity'); + const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; + + // We include numbers we didn't successfully send to so we can display errors. + // Older messages don't have the recipients included on the message, so we fall + // back to the conversation's current recipients + const phoneNumbers = this.isIncoming() + ? [this.get('source')] + : _.union( + this.get('sent_to') || [], + this.get('recipients') || this.getConversation().getRecipients() + ); + + // This will make the error message for outgoing key errors a bit nicer + const allErrors = (this.get('errors') || []).map((error: any) => { + if (error.name === OUTGOING_KEY_ERROR) { + // eslint-disable-next-line no-param-reassign + error.message = newIdentity; + } + + return error; + }); + + // If an error has a specific number it's associated with, we'll show it next to + // that contact. Otherwise, it will be a standalone entry. + const errors = _.reject(allErrors, error => Boolean(error.number)); + const errorsGroupedById = _.groupBy(allErrors, 'number'); + const finalContacts = await Promise.all( + (phoneNumbers || []).map(async id => { + const errorsForContact = errorsGroupedById[id]; + const isOutgoingKeyError = Boolean( + _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) + ); + + const contact = this.findAndFormatContact(id); + return { + ...contact, + // fallback to the message status if we do not have a status with a user + // this is useful for medium groups. + status: this.getStatus(id) || this.getMessagePropStatus(), + errors: errorsForContact, + isOutgoingKeyError, + isPrimaryDevice: true, + profileName: contact.profileName, + }; + }) + ); + + // The prefix created here ensures that contacts with errors are listed + // first; otherwise it's alphabetical + const sortedContacts = _.sortBy( + finalContacts, + contact => `${contact.isPrimaryDevice ? '0' : '1'}${contact.phoneNumber}` + ); + + return { + sentAt: this.get('sent_at'), + receivedAt: this.get('received_at'), + message: { + ...this.propsForMessage, + disableMenu: true, + // To ensure that group avatar doesn't show up + conversationType: 'direct', + }, + errors, + contacts: sortedContacts, + }; + } + + public copyPubKey() { + if (this.isIncoming()) { + window.clipboard.writeText(this.get('source')); + } else { + window.clipboard.writeText(UserUtils.getOurPubKeyStrFromCache()); + } + + ToastUtils.pushCopiedToClipBoard(); + } + + public banUser() { + window.confirmationDialog({ + title: window.i18n('banUser'), + message: window.i18n('banUserConfirm'), + resolve: async () => { + const source = this.get('source'); + const conversation = this.getConversation(); + + const channelAPI = await conversation.getPublicSendData(); + const success = await channelAPI.banUser(source); + + if (success) { + ToastUtils.pushUserBanSuccess(); + } else { + ToastUtils.pushUserBanFailure(); + } + }, + }); + } + + public copyText() { + window.clipboard.writeText(this.get('body')); + + ToastUtils.pushCopiedToClipBoard(); + } + + /** + * Uploads attachments, previews and quotes. + * + * @returns The uploaded data which includes: body, attachments, preview and quote. + */ + public async uploadData() { + // TODO: In the future it might be best if we cache the upload results if possible. + // This way we don't upload duplicated data. + + const attachmentsWithData = await Promise.all( + (this.get('attachments') || []).map( + window.Signal.Migrations.loadAttachmentData + ) + ); + const body = this.get('body'); + const finalAttachments = attachmentsWithData; + + const filenameOverridenAttachments = finalAttachments.map( + (attachment: any) => ({ + ...attachment, + fileName: window.Signal.Types.Attachment.getSuggestedFilenameSending({ + attachment, + timestamp: Date.now(), + }), + }) + ); + + const quoteWithData = await window.Signal.Migrations.loadQuoteData( + this.get('quote') + ); + const previewWithData = await window.Signal.Migrations.loadPreviewData( + this.get('preview') + ); + + const conversation = this.getConversation(); + const openGroup = + conversation && conversation.isPublic() && conversation.toOpenGroup(); + + const { AttachmentUtils } = Utils; + const [attachments, preview, quote] = await Promise.all([ + AttachmentUtils.uploadAttachments( + filenameOverridenAttachments, + openGroup + ), + AttachmentUtils.uploadLinkPreviews(previewWithData, openGroup), + AttachmentUtils.uploadQuoteThumbnails(quoteWithData, openGroup), + ]); + + return { + body, + attachments, + preview, + quote, + }; + } + + // One caller today: event handler for the 'Retry Send' entry in triple-dot menu + public async retrySend() { + if (!window.textsecure.messaging) { + window.log.error('retrySend: Cannot retry since we are offline!'); + return null; + } + + this.set({ errors: null }); + await this.commit(); + try { + const conversation = this.getConversation(); + const intendedRecipients = this.get('recipients') || []; + const successfulRecipients = this.get('sent_to') || []; + const currentRecipients = conversation.getRecipients(); + + if (conversation.isPublic()) { + const openGroup = { + server: conversation.get('server'), + channel: conversation.get('channelId'), + conversationId: conversation.id, + }; + const uploaded = await this.uploadData(); + + const openGroupParams = { + identifier: this.id, + timestamp: Date.now(), + group: openGroup, + ...uploaded, + }; + const openGroupMessage = new OpenGroupMessage(openGroupParams); + return getMessageQueue().sendToGroup(openGroupMessage); + } + + let recipients = _.intersection(intendedRecipients, currentRecipients); + recipients = recipients.filter( + key => !successfulRecipients.includes(key) + ); + + if (!recipients.length) { + window.log.warn('retrySend: Nobody to send to!'); + + return this.commit(); + } + + const { body, attachments, preview, quote } = await this.uploadData(); + const ourNumber = window.storage.get('primaryDevicePubKey'); + const ourConversation = ConversationController.getInstance().get( + ourNumber + ); + + const chatParams = { + identifier: this.id, + body, + timestamp: this.get('sent_at') || Date.now(), + expireTimer: this.get('expireTimer'), + attachments, + preview, + quote, + lokiProfile: + (ourConversation && ourConversation.getOurProfile()) || undefined, + }; + if (!chatParams.lokiProfile) { + delete chatParams.lokiProfile; + } + + const chatMessage = new ChatMessage(chatParams); + + // Special-case the self-send case - we send only a sync message + if (recipients.length === 1) { + const isOurDevice = UserUtils.isUsFromCache(recipients[0]); + if (isOurDevice) { + return this.sendSyncMessageOnly(chatMessage); + } + } + + if (conversation.isPrivate()) { + const [number] = recipients; + const recipientPubKey = new PubKey(number); + + return getMessageQueue().sendToPubKey(recipientPubKey, chatMessage); + } + + // TODO should we handle medium groups message here too? + // Not sure there is the concept of retrySend for those + const closedGroupChatMessage = new ClosedGroupChatMessage({ + identifier: this.id, + chatMessage, + groupId: this.get('conversationId'), + }); + // Because this is a partial group send, we send the message with the groupId field set, but individually + // to each recipient listed + return Promise.all( + recipients.map(async r => { + const recipientPubKey = new PubKey(r); + return getMessageQueue().sendToPubKey( + recipientPubKey, + closedGroupChatMessage + ); + }) + ); + } catch (e) { + await this.saveErrors(e); + return null; + } + } + + // Called when the user ran into an error with a specific user, wants to send to them + public async resend(number: string) { + const error = this.removeOutgoingErrors(number); + if (!error) { + window.log.warn('resend: requested number was not present in errors'); + return null; + } + + try { + const { body, attachments, preview, quote } = await this.uploadData(); + + const chatMessage = new ChatMessage({ + identifier: this.id, + body, + timestamp: this.get('sent_at') || Date.now(), + expireTimer: this.get('expireTimer'), + attachments, + preview, + quote, + }); + + // Special-case the self-send case - we send only a sync message + if (UserUtils.isUsFromCache(number)) { + return this.sendSyncMessageOnly(chatMessage); + } + + const conversation = this.getConversation(); + const recipientPubKey = new PubKey(number); + + if (conversation.isPrivate()) { + return getMessageQueue().sendToPubKey(recipientPubKey, chatMessage); + } + + const closedGroupChatMessage = new ClosedGroupChatMessage({ + chatMessage, + groupId: this.get('conversationId'), + }); + // resend tries to send the message to that specific user only in the context of a closed group + return getMessageQueue().sendToPubKey( + recipientPubKey, + closedGroupChatMessage + ); + } catch (e) { + await this.saveErrors(e); + return null; + } + } + + public removeOutgoingErrors(number: string) { + const errors = _.partition( + this.get('errors'), + e => + e.number === number && + (e.name === 'MessageError' || + e.name === 'SendMessageNetworkError' || + e.name === 'OutgoingIdentityKeyError') + ); + this.set({ errors: errors[1] }); + return errors[0][0]; + } + + /** + * This function is called by inbox_view.js when a message was successfully sent for one device. + * So it might be called several times for the same message + */ + public async handleMessageSentSuccess( + sentMessage: any, + wrappedEnvelope: any + ) { + let sentTo = this.get('sent_to') || []; + + let isOurDevice = false; + if (sentMessage.device) { + isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + } + // FIXME this is not correct and will cause issues with syncing + // At this point the only way to check for medium + // group is by comparing the encryption type + const isClosedGroupMessage = + sentMessage.encryption === EncryptionType.ClosedGroup; + + const isOpenGroupMessage = + !!sentMessage.group && sentMessage.group instanceof Types.OpenGroup; + + // We trigger a sync message only when the message is not to one of our devices, AND + // the message is not for an open group (there is no sync for opengroups, each device pulls all messages), AND + // if we did not sync or trigger a sync message for this specific message already + const shouldTriggerSyncMessage = + !isOurDevice && + !isClosedGroupMessage && + !this.get('synced') && + !this.get('sentSync'); + + // A message is synced if we triggered a sync message (sentSync) + // and the current message was sent to our device (so a sync message) + const shouldMarkMessageAsSynced = isOurDevice && this.get('sentSync'); + + const isSessionOrClosedMessage = !isOpenGroupMessage; + + if (isSessionOrClosedMessage) { + const contentDecoded = SignalService.Content.decode( + sentMessage.plainTextBuffer + ); + const { dataMessage } = contentDecoded; + + /** + * We should hit the notify endpoint for push notification only if: + * • It's a one-to-one chat or a closed group + * • The message has either text or attachments + */ + const hasBodyOrAttachments = Boolean( + dataMessage && + (dataMessage.body || + (dataMessage.attachments && dataMessage.attachments.length)) + ); + const shouldNotifyPushServer = + hasBodyOrAttachments && isSessionOrClosedMessage; + + if (shouldNotifyPushServer) { + // notify the push notification server if needed + if (!wrappedEnvelope) { + window.log.warn('Should send PN notify but no wrapped envelope set.'); + } else { + if (!window.LokiPushNotificationServer) { + window.LokiPushNotificationServer = new window.LokiPushNotificationServerApi(); + } + + window.LokiPushNotificationServer.notify( + wrappedEnvelope, + sentMessage.device + ); + } + } + + // Handle the sync logic here + if (shouldTriggerSyncMessage) { + if (dataMessage) { + await this.sendSyncMessage(dataMessage as DataMessage); + } + } else if (shouldMarkMessageAsSynced) { + this.set({ synced: true }); + } + + sentTo = _.union(sentTo, [sentMessage.device]); + } + + this.set({ + sent_to: sentTo, + sent: true, + expirationStartTimestamp: Date.now(), + }); + + await this.commit(); + + this.getConversation().updateLastMessage(); + + this.trigger('sent', this); + } + + public async handleMessageSentFailure(sentMessage: any, error: any) { + if (error instanceof Error) { + await this.saveErrors(error); + if (error.name === 'OutgoingIdentityKeyError') { + const c = ConversationController.getInstance().get(sentMessage.device); + await c.getProfiles(); + } + } + let isOurDevice = false; + if (sentMessage.device) { + isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + } + + const expirationStartTimestamp = Date.now(); + if (isOurDevice && !this.get('sync')) { + this.set({ sentSync: false }); + } + this.set({ + sent: true, + expirationStartTimestamp, + }); + await this.commit(); + + this.getConversation().updateLastMessage(); + this.trigger('done'); + } + + public getConversation() { + // This needs to be an unsafe call, because this method is called during + // initial module setup. We may be in the middle of the initial fetch to + // the database. + return ConversationController.getInstance().getUnsafe( + this.get('conversationId') + ); + } + + public getQuoteContact() { + const quote = this.get('quote'); + if (!quote) { + return null; + } + const { author } = quote; + if (!author) { + return null; + } + + return ConversationController.getInstance().get(author); + } + + public getSource() { + if (this.isIncoming()) { + return this.get('source'); + } + + return UserUtils.getOurPubKeyStrFromCache(); + } + + public getContact() { + const source = this.getSource(); + + if (!source) { + return null; + } + + return ConversationController.getInstance().getOrCreate(source, 'private'); + } + + public isOutgoing() { + return this.get('type') === 'outgoing'; + } + + public hasErrors() { + return _.size(this.get('errors')) > 0; + } + + public getStatus(pubkey: string) { + const readBy = this.get('read_by') || []; + if (readBy.indexOf(pubkey) >= 0) { + return 'read'; + } + const deliveredTo = this.get('delivered_to') || []; + if (deliveredTo.indexOf(pubkey) >= 0) { + return 'delivered'; + } + const sentTo = this.get('sent_to') || []; + if (sentTo.indexOf(pubkey) >= 0) { + return 'sent'; + } + + return null; + } + + public async setCalculatingPoW() { + if (this.get('calculatingPoW')) { + return; + } + + this.set({ + calculatingPoW: true, + }); + + await this.commit(); + } + + public async setServerId(serverId: number) { + if (_.isEqual(this.get('serverId'), serverId)) { + return; + } + + this.set({ + serverId, + }); + + await this.commit(); + } + + public async setServerTimestamp(serverTimestamp?: number) { + if (_.isEqual(this.get('serverTimestamp'), serverTimestamp)) { + return; + } + + this.set({ + serverTimestamp, + }); + + await this.commit(); + } + + public async setIsPublic(isPublic: boolean) { + if (_.isEqual(this.get('isPublic'), isPublic)) { + return; + } + + this.set({ + isPublic: !!isPublic, + }); + + await this.commit(); + } + + public async sendSyncMessageOnly(dataMessage: any) { + this.set({ + sent_to: [UserUtils.getOurPubKeyStrFromCache()], + sent: true, + expirationStartTimestamp: Date.now(), + }); + + await this.commit(); + + const data = + dataMessage instanceof DataMessage + ? dataMessage.dataProto() + : dataMessage; + await this.sendSyncMessage(data); + } + + public async sendSyncMessage(dataMessage: DataMessage) { + if (this.get('synced') || this.get('sentSync')) { + return; + } + + window.log.error('sendSyncMessage to upgrade to multi device protocol v2'); + + // const data = + // dataMessage instanceof DataMessage + // ? dataMessage.dataProto() + // : dataMessage; + + // const syncMessage = new SentSyncMessage({ + // timestamp: this.get('sent_at'), + // identifier: this.id, + // dataMessage: data, + // destination: this.get('destination'), + // expirationStartTimestamp: this.get('expirationStartTimestamp'), + // sent_to: this.get('sent_to'), + // unidentifiedDeliveries: this.get('unidentifiedDeliveries'), + // }); + + // await sendSyncMessage(syncMessage); + + this.set({ sentSync: true }); + await this.commit(); + } + + public async markMessageSyncOnly(dataMessage: DataMessage) { + this.set({ + // These are the same as a normal send() + dataMessage, + sent_to: [UserUtils.getOurPubKeyStrFromCache()], + sent: true, + expirationStartTimestamp: Date.now(), + }); + + await this.commit(); + } + + public async saveErrors(providedErrors: any) { + let errors = providedErrors; + + if (!(errors instanceof Array)) { + errors = [errors]; + } + errors.forEach((e: any) => { + window.log.error( + 'Message.saveErrors:', + e && e.reason ? e.reason : null, + e && e.stack ? e.stack : e + ); + }); + errors = errors.map((e: any) => { + if ( + e.constructor === Error || + e.constructor === TypeError || + e.constructor === ReferenceError + ) { + return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); + } + return e; + }); + errors = errors.concat(this.get('errors') || []); + + this.set({ errors }); + await this.commit(); + } + + public async commit(forceSave = false) { + // TODO investigate the meaning of the forceSave + const id = await window.Signal.Data.saveMessage(this.attributes, { + forceSave, + Message: MessageModel, + }); + this.trigger('change'); + return id; + } + + public async markRead(readAt: number) { + this.unset('unread'); + + if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { + const expirationStartTimestamp = Math.min( + Date.now(), + readAt || Date.now() + ); + this.set({ expirationStartTimestamp }); + } + + window.Whisper.Notifications.remove( + window.Whisper.Notifications.where({ + messageId: this.id, + }) + ); + + await this.commit(); + } + + public isExpiring() { + return this.get('expireTimer') && this.get('expirationStartTimestamp'); + } + + public isExpired() { + return this.msTilExpire() <= 0; + } + + public msTilExpire() { + if (!this.isExpiring()) { + return Infinity; + } + const now = Date.now(); + const start = this.get('expirationStartTimestamp'); + if (!start) { + return Infinity; + } + const delta = this.get('expireTimer') * 1000; + let msFromNow = start + delta - now; + if (msFromNow < 0) { + msFromNow = 0; + } + return msFromNow; + } + + public async setToExpire(force = false) { + if (this.isExpiring() && (force || !this.get('expires_at'))) { + const start = this.get('expirationStartTimestamp'); + const delta = this.get('expireTimer') * 1000; + if (!start) { + return; + } + const expiresAt = start + delta; + + this.set({ expires_at: expiresAt }); + const id = this.get('id'); + if (id) { + await this.commit(); + } + + window.log.info('Set message expiration', { + expiresAt, + sentAt: this.get('sent_at'), + }); + } + } +} +export class MessageCollection extends Backbone.Collection {} + +MessageCollection.prototype.model = MessageModel; diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts new file mode 100644 index 0000000000..ea7f20f9b4 --- /dev/null +++ b/ts/models/messageType.ts @@ -0,0 +1,213 @@ +import { DefaultTheme } from 'styled-components'; +import _ from 'underscore'; +import { QuotedAttachmentType } from '../components/conversation/Quote'; +import { AttachmentType } from '../types/Attachment'; +import { Contact } from '../types/Contact'; + +export type MessageModelType = 'incoming' | 'outgoing'; +export type MessageDeliveryStatus = + | 'sending' + | 'sent' + | 'delivered' + | 'read' + | 'error'; + +export interface MessageAttributes { + id: string; + source: string; + quote?: any; + expireTimer: number; + received_at?: number; + sent_at?: number; + destination?: string; + preview?: any; + body?: string; + expirationStartTimestamp: number; + read_by: Array; + delivered_to: Array; + decrypted_at: number; + expires_at?: number; + recipients: Array; + delivered?: number; + type: MessageModelType; + group_update?: any; + groupInvitation?: any; + attachments?: any; + contact?: any; + conversationId: any; + errors?: any; + flags?: number; + hasAttachments: boolean; + hasFileAttachments: boolean; + hasVisualMediaAttachments: boolean; + schemaVersion: number; + expirationTimerUpdate?: any; + unread: boolean; + group?: any; + timestamp?: number; + status: MessageDeliveryStatus; + dataMessage: any; + sent_to: any; + sent: boolean; + calculatingPoW: boolean; + serverId?: number; + serverTimestamp?: number; + isPublic: boolean; + sentSync: boolean; + synced: boolean; + sync: boolean; + snippet?: any; + direction: any; +} + +export interface MessageAttributesOptionals { + id?: string; + source?: string; + quote?: any; + expireTimer?: number; + received_at?: number; + sent_at?: number; + destination?: string; + preview?: any; + body?: string; + expirationStartTimestamp?: number; + read_by?: Array; + delivered_to?: Array; + decrypted_at?: number; + expires_at?: number; + recipients?: Array; + delivered?: number; + type: MessageModelType; + group_update?: any; + groupInvitation?: any; + attachments?: any; + contact?: any; + conversationId: any; + errors?: any; + flags?: number; + hasAttachments?: boolean; + hasFileAttachments?: boolean; + hasVisualMediaAttachments?: boolean; + schemaVersion?: number; + expirationTimerUpdate?: any; + unread?: boolean; + group?: any; + timestamp?: number; + status?: MessageDeliveryStatus; + dataMessage?: any; + sent_to?: Array; + sent?: boolean; + calculatingPoW?: boolean; + serverId?: number; + serverTimestamp?: number; + isPublic?: boolean; + sentSync?: boolean; + synced?: boolean; + sync?: boolean; + snippet?: any; + direction?: any; +} + +/** + * This function mutates optAttributes + * @param optAttributes the entry object attributes to set the defaults to. + */ +export const fillMessageAttributesWithDefaults = ( + optAttributes: MessageAttributesOptionals +): MessageAttributes => { + //FIXME audric to do put the default + return _.defaults(optAttributes, { + expireTimer: 0, // disabled + }); +}; + +export interface MessageRegularProps { + disableMenu?: boolean; + isDeletable: boolean; + isAdmin?: boolean; + weAreAdmin?: boolean; + text?: string; + id: string; + collapseMetadata?: boolean; + direction: 'incoming' | 'outgoing'; + timestamp: number; + serverTimestamp?: number; + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'error' | 'pow'; + // What if changed this over to a single contact like quote, and put the events on it? + contact?: Contact & { + onSendMessage?: () => void; + onClick?: () => void; + }; + authorName?: string; + authorProfileName?: string; + /** Note: this should be formatted for display */ + authorPhoneNumber: string; + conversationType: 'group' | 'direct'; + attachments?: Array; + quote?: { + text: string; + attachment?: QuotedAttachmentType; + isFromMe: boolean; + authorPhoneNumber: string; + authorProfileName?: string; + authorName?: string; + messageId?: string; + onClick: (data: any) => void; + referencedMessageNotFound: boolean; + }; + previews: Array; + authorAvatarPath?: string; + isExpired: boolean; + expirationLength?: number; + expirationTimestamp?: number; + convoId: string; + isPublic?: boolean; + selected: boolean; + isKickedFromGroup: boolean; + // whether or not to show check boxes + multiSelectMode: boolean; + firstMessageOfSeries: boolean; + isUnread: boolean; + isQuotedMessageToAnimate?: boolean; + + onClickAttachment?: (attachment: AttachmentType) => void; + onClickLinkPreview?: (url: string) => void; + onCopyText?: () => void; + onSelectMessage: (messageId: string) => void; + onReply?: (messagId: number) => void; + onRetrySend?: () => void; + onDownload?: (attachment: AttachmentType) => void; + onDeleteMessage: (messageId: string) => void; + onCopyPubKey?: () => void; + onBanUser?: () => void; + onShowDetail: () => void; + onShowUserDetails: (userPubKey: string) => void; + markRead: (readAt: number) => Promise; + theme: DefaultTheme; +} + +// export interface MessageModel extends Backbone.Model { +// setServerTimestamp(serverTimestamp: any); +// setServerId(serverId: any); +// setIsPublic(arg0: boolean); +// idForLogging: () => string; +// isGroupUpdate: () => boolean; +// isExpirationTimerUpdate: () => boolean; +// getNotificationText: () => string; +// markRead: (readAt: number) => Promise; +// merge: (other: MessageModel) => void; +// saveErrors: (error: any) => promise; +// sendSyncMessageOnly: (message: any) => void; +// isUnread: () => boolean; +// commit: () => Promise; +// getPropsForMessageDetail: () => any; +// getConversation: () => ConversationModel; +// handleMessageSentSuccess: (sentMessage: any, wrappedEnvelope: any) => any; +// handleMessageSentFailure: (sentMessage: any, error: any) => any; + +// propsForMessage?: MessageRegularProps; +// propsForTimerNotification?: any; +// propsForGroupInvitation?: any; +// propsForGroupNotification?: any; +// firstMessageOfSeries: boolean; +// } diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index a577680dac..624a80af1d 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -1,7 +1,7 @@ -import { MessageModel } from '../../js/models/messages'; import _ from 'lodash'; import * as Data from '../../js/modules/data'; +import { MessageModel } from '../models/message'; export async function downloadAttachment(attachment: any) { const serverUrl = new URL(attachment.url).origin; @@ -75,32 +75,6 @@ export async function downloadAttachment(attachment: any) { }; } -async function processLongAttachments( - message: MessageModel, - attachments: Array -): Promise { - if (attachments.length === 0) { - return false; - } - - if (attachments.length > 1) { - window.log.error( - `Received more than one long message attachment in message ${message.idForLogging()}` - ); - } - - const attachment = attachments[0]; - - message.set({ bodyPending: true }); - await window.Signal.AttachmentDownloads.addJob(attachment, { - messageId: message.id, - type: 'long-message', - index: 0, - }); - - return true; -} - async function processNormalAttachments( message: MessageModel, normalAttachments: Array @@ -247,17 +221,7 @@ export async function queueAttachmentDownloads( let count = 0; - const [longMessageAttachments, normalAttachments] = _.partition( - message.get('attachments') || [], - (attachment: any) => - attachment.contentType === Whisper.Message.LONG_MESSAGE_CONTENT_TYPE - ); - - if (await processLongAttachments(message, longMessageAttachments)) { - count += 1; - } - - count += await processNormalAttachments(message, normalAttachments); + count += await processNormalAttachments(message, message.get('attachments')); count += await processPreviews(message); @@ -271,7 +235,7 @@ export async function queueAttachmentDownloads( if (count > 0) { await Data.saveMessage(message.attributes, { - Message: Whisper.Message, + Message: MessageModel, }); return true; diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 43419f1a28..a2296763f0 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -19,12 +19,12 @@ import { } from '../session/messages/outgoing/content/data/group/ClosedGroupNewMessage'; import { ECKeyPair } from './keypairs'; -import { getOurNumber } from '../session/utils/User'; import { UserUtils } from '../session/utils'; +import { ConversationModel } from '../models/conversation'; -export async function handleClosedGroup( +export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, - groupUpdate: any + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage ) { const { type } = groupUpdate; const { Type } = SignalService.DataMessage.ClosedGroupControlMessage; @@ -36,11 +36,17 @@ export async function handleClosedGroup( } if (type === Type.ENCRYPTION_KEY_PAIR) { - await handleKeyPairClosedGroup(envelope, groupUpdate); + await handleClosedGroupEncryptionKeyPair(envelope, groupUpdate); } else if (type === Type.NEW) { await handleNewClosedGroup(envelope, groupUpdate); - } else if (type === Type.UPDATE) { - await handleUpdateClosedGroup(envelope, groupUpdate); + } else if ( + type === Type.NAME_CHANGE || + type === Type.MEMBERS_REMOVED || + type === Type.MEMBERS_ADDED || + type === Type.MEMBER_LEFT || + type === Type.UPDATE + ) { + await performIfValid(envelope, groupUpdate); } else { window.log.error('Unknown group update type: ', type); } @@ -139,7 +145,7 @@ async function handleNewClosedGroup( const members = membersAsData.map(toHex); const admins = adminsAsData.map(toHex); - const ourPrimary = await UserUtils.getOurNumber(); + const ourPrimary = UserUtils.getOurPubKeyFromCache(); if (!members.includes(ourPrimary.key)) { log.info( 'Got a new group message but apparently we are not a member of it. Dropping it.' @@ -218,66 +224,23 @@ async function handleNewClosedGroup( async function handleUpdateClosedGroup( envelope: EnvelopePlus, - groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + convo: ConversationModel ) { - if ( - groupUpdate.type !== - SignalService.DataMessage.ClosedGroupControlMessage.Type.UPDATE - ) { - return; - } const { name, members: membersBinary } = groupUpdate; const { log } = window; // for a closed group update message, the envelope.source is the groupPublicKey const groupPublicKey = envelope.source; - const convo = ConversationController.getInstance().get(groupPublicKey); - - if (!convo) { - log.warn( - 'Ignoring a closed group update message (INFO) for a non-existing group' - ); - await removeFromCache(envelope); - return; - } - - // Check that the message isn't from before the group was created - let lastJoinedTimestamp = convo.get('lastJoinedTimestamp'); - // might happen for existing groups - if (!lastJoinedTimestamp) { - const aYearAgo = Date.now() - 1000 * 60 * 24 * 365; - convo.set({ - lastJoinedTimestamp: aYearAgo, - }); - lastJoinedTimestamp = aYearAgo; - } - - if (envelope.timestamp <= lastJoinedTimestamp) { - window.log.warn( - 'Got a group update with an older timestamp than when we joined this group last time. Dropping it' - ); - await removeFromCache(envelope); - return; - } const curAdmins = convo.get('groupAdmins'); - // Check that the sender is a member of the group (before the update) - const oldMembers = convo.get('members') || []; - if (!oldMembers.includes(envelope.senderIdentity)) { - log.error( - `Error: closed group: ignoring closed group update message from non-member. ${envelope.senderIdentity} is not a current member.` - ); - await removeFromCache(envelope); - return; - } - // NOTE: admins cannot change with closed groups const members = membersBinary.map(toHex); const diff = ClosedGroup.buildGroupDiff(convo, { name, members }); // Check whether we are still in the group - const ourNumber = await UserUtils.getOurNumber(); + const ourNumber = UserUtils.getOurPubKeyFromCache(); const wasCurrentUserRemoved = !members.includes(ourNumber.key); const isCurrentUserAdmin = curAdmins?.includes(ourNumber.key); @@ -293,8 +256,8 @@ async function handleUpdateClosedGroup( await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs( groupPublicKey ); - convo.set('isKickedFromGroup', true); // Disable typing: + convo.set('isKickedFromGroup', true); window.SwarmPolling.removePubkey(groupPublicKey); } else { if (convo.get('isKickedFromGroup')) { @@ -341,7 +304,7 @@ async function handleUpdateClosedGroup( * In this message, we have n-times the same keypair encoded with n being the number of current members. * One of that encoded keypair is the one for us. We need to find it, decode it, and save it for use with this group. */ -async function handleKeyPairClosedGroup( +async function handleClosedGroupEncryptionKeyPair( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage ) { @@ -351,7 +314,7 @@ async function handleKeyPairClosedGroup( ) { return; } - const ourNumber = await UserUtils.getOurNumber(); + const ourNumber = UserUtils.getOurPubKeyFromCache(); const groupPublicKey = envelope.source; const ourKeyPair = await UserUtils.getIdentityKeyPair(); @@ -451,13 +414,195 @@ async function handleKeyPairClosedGroup( await removeFromCache(envelope); } +async function performIfValid( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage +) { + const { Type } = SignalService.DataMessage.ClosedGroupControlMessage; + + const groupPublicKey = envelope.source; + + const convo = ConversationController.getInstance().get(groupPublicKey); + if (!convo) { + window.log.warn('dropping message for nonexistent group'); + return; + } + + if (!convo) { + window.log.warn( + 'Ignoring a closed group update message (INFO) for a non-existing group' + ); + return removeFromCache(envelope); + } + + // Check that the message isn't from before the group was created + let lastJoinedTimestamp = convo.get('lastJoinedTimestamp'); + // might happen for existing groups + if (!lastJoinedTimestamp) { + const aYearAgo = Date.now() - 1000 * 60 * 24 * 365; + convo.set({ + lastJoinedTimestamp: aYearAgo, + }); + lastJoinedTimestamp = aYearAgo; + } + + if (envelope.timestamp <= lastJoinedTimestamp) { + window.log.warn( + 'Got a group update with an older timestamp than when we joined this group last time. Dropping it.' + ); + return removeFromCache(envelope); + } + + // Check that the sender is a member of the group (before the update) + const oldMembers = convo.get('members') || []; + if (!oldMembers.includes(envelope.senderIdentity)) { + window.log.error( + `Error: closed group: ignoring closed group update message from non-member. ${envelope.senderIdentity} is not a current member.` + ); + await removeFromCache(envelope); + return; + } + + if (groupUpdate.type === Type.UPDATE) { + await handleUpdateClosedGroup(envelope, groupUpdate, convo); + } else if (groupUpdate.type === Type.NAME_CHANGE) { + await handleClosedGroupNameChanged(envelope, groupUpdate, convo); + } else if (groupUpdate.type === Type.MEMBERS_ADDED) { + await handleClosedGroupMembersAdded(envelope, groupUpdate, convo); + } else if (groupUpdate.type === Type.MEMBERS_REMOVED) { + await handleClosedGroupMembersRemoved(envelope, groupUpdate, convo); + } else if (groupUpdate.type === Type.MEMBER_LEFT) { + await handleClosedGroupMemberLeft(envelope, groupUpdate, convo); + } + + return true; +} + +async function handleClosedGroupNameChanged( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + convo: ConversationModel +) { + // Only add update message if we have something to show + const newName = groupUpdate.name; + if (newName !== convo.get('name')) { + const groupDiff: ClosedGroup.GroupDiff = { + newName, + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + convo.set({ name: newName }); + await convo.commit(); + } + + await removeFromCache(envelope); +} + +async function handleClosedGroupMembersAdded( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + convo: ConversationModel +) { + const { members: addedMembersBinary } = groupUpdate; + const addedMembers = (addedMembersBinary || []).map(toHex); + const oldMembers = convo.get('members') || []; + const membersNotAlreadyPresent = addedMembers.filter( + m => !oldMembers.includes(m) + ); + console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent); + + if (membersNotAlreadyPresent.length === 0) { + window.log.info( + 'no new members in this group update compared to what we have already. Skipping update' + ); + await removeFromCache(envelope); + return; + } + + const members = [...oldMembers, ...membersNotAlreadyPresent]; + // Only add update message if we have something to show + + const groupDiff: ClosedGroup.GroupDiff = { + joiningMembers: membersNotAlreadyPresent, + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + + convo.set({ members }); + await convo.commit(); + await removeFromCache(envelope); +} + +async function handleClosedGroupMembersRemoved( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + convo: ConversationModel +) {} + +async function handleClosedGroupMemberLeft( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + convo: ConversationModel +) { + const sender = envelope.senderIdentity; + const groupPublicKey = envelope.source; + const didAdminLeave = convo.get('groupAdmins')?.includes(sender) || false; + // If the admin leaves the group is disbanded + // otherwise, we remove the sender from the list of current members in this group + const oldMembers = convo.get('members') || []; + const leftMemberWasPresent = oldMembers.includes(sender); + const members = didAdminLeave ? [] : oldMembers.filter(s => s !== sender); + // Guard against self-sends + const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); + if (!ourPubkey) { + throw new Error('Could not get user pubkey'); + } + if (sender === ourPubkey) { + window.log.info('self send group update ignored'); + await removeFromCache(envelope); + return; + } + + // Generate and distribute a new encryption key pair if needed + const isCurrentUserAdmin = + convo.get('groupAdmins')?.includes(ourPubkey) || false; + if (isCurrentUserAdmin && !!members.length) { + await ClosedGroup.generateAndSendNewEncryptionKeyPair( + groupPublicKey, + members + ); + } + + if (didAdminLeave) { + await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs( + groupPublicKey + ); + // Disable typing: + convo.set('isKickedFromGroup', true); + window.SwarmPolling.removePubkey(groupPublicKey); + } + // Update the group + + // Only add update message if we have something to show + if (leftMemberWasPresent) { + const groupDiff: ClosedGroup.GroupDiff = { + leavingMembers: didAdminLeave ? oldMembers : [sender], + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + } + + convo.set('members', members); + + await convo.commit(); + + await removeFromCache(envelope); +} + export async function createClosedGroup( groupName: string, members: Array ) { const setOfMembers = new Set(members); - const ourNumber = await getOurNumber(); + const ourNumber = UserUtils.getOurPubKeyFromCache(); // Create Group Identity // Generate the key pair that'll be used for encryption and decryption // Generate the group's public key @@ -504,6 +649,7 @@ export async function createClosedGroup( // the sending pipeline needs to know from GroupUtils when a message is for a medium group await ClosedGroup.updateOrCreateClosedGroup(groupDetails); convo.set('lastJoinedTimestamp', Date.now()); + await convo.commit(); // Send a closed group update message to all members individually const promises = listOfMembers.map(async m => { diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 5d3be8a5c9..047366ec37 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -90,7 +90,7 @@ async function decryptForClosedGroup( ); } window.log.info('ClosedGroup Message decrypted successfully.'); - const ourDevicePubKey = await UserUtils.getCurrentDevicePubKey(); + const ourDevicePubKey = UserUtils.getOurPubKeyStrFromCache(); if ( envelope.senderIdentity && diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 93ac94bc59..9eef43a02d 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -3,7 +3,6 @@ import { removeFromCache } from './cache'; import { EnvelopePlus } from './types'; import { ConversationType, getEnvelopeId } from './common'; -import { MessageModel } from '../../js/models/messages'; import { PubKey } from '../session/types'; import { handleMessageJob } from './queuedJob'; import { downloadAttachment } from './attachments'; @@ -12,8 +11,10 @@ import { StringUtils, UserUtils } from '../session/utils'; import { DeliveryReceiptMessage } from '../session/messages/outgoing'; import { getMessageQueue } from '../session'; import { ConversationController } from '../session/conversations'; -import { handleClosedGroup } from './closedGroups'; -import { isUs } from '../session/utils/User'; +import { handleClosedGroupControlMessage } from './closedGroups'; +import { MessageModel } from '../models/message'; +import { isUsFromCache } from '../session/utils/User'; +import { MessageModelType } from '../models/messageType'; export async function updateProfile( conversation: any, @@ -251,12 +252,15 @@ export async function handleDataMessage( window.log.info('data message from', getEnvelopeId(envelope)); if (dataMessage.closedGroupControlMessage) { - await handleClosedGroup(envelope, dataMessage.closedGroupControlMessage); + await handleClosedGroupControlMessage( + envelope, + dataMessage.closedGroupControlMessage as SignalService.DataMessage.ClosedGroupControlMessage + ); return; } const message = await processDecrypted(envelope, dataMessage); - const ourPubKey = window.textsecure.storage.user.getNumber(); + const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); const source = envelope.source; const senderPubKey = envelope.senderIdentity || envelope.source; const isMe = senderPubKey === ourPubKey; @@ -278,7 +282,7 @@ export async function handleDataMessage( return removeFromCache(envelope); } - const ownDevice = await isUs(senderPubKey); + const ownDevice = isUsFromCache(senderPubKey); const sourceConversation = ConversationController.getInstance().get(source); const ownMessage = sourceConversation?.isMediumGroup() && ownDevice; @@ -331,7 +335,7 @@ async function isMessageDuplicate({ const result = await window.Signal.Data.getMessageBySender( { source, sourceDevice, sent_at: timestamp }, { - Message: window.Whisper.Message, + Message: MessageModel, } ); if (!result) { @@ -383,7 +387,7 @@ async function handleProfileUpdate( await receiver.commit(); // Then we update our own profileKey if it's different from what we have - const ourNumber = window.textsecure.storage.user.getNumber(); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const me = await ConversationController.getInstance().getOrCreate( ourNumber, 'private' @@ -453,7 +457,7 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel { isPublic, // + }; - return new window.Whisper.Message(messageData); + return new MessageModel(messageData); } function createSentMessage(data: MessageCreationData): MessageModel { @@ -492,18 +496,18 @@ function createSentMessage(data: MessageCreationData): MessageModel { ), }; - const messageData: any = { - source: window.textsecure.storage.user.getNumber(), + const messageData = { + source: UserUtils.getOurPubKeyStrFromCache(), sourceDevice, serverTimestamp, sent_at: timestamp, received_at: isPublic ? receivedAt : now, conversationId: destination, // conversation ID will might change later (if it is a group) - type: 'outgoing', + type: 'outgoing' as MessageModelType, ...sentSpecificFields, }; - return new window.Whisper.Message(messageData); + return new MessageModel(messageData); } function createMessage( @@ -576,7 +580,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise { // TODO: this shouldn't be called when source is not a pubkey!!! - const isOurDevice = await UserUtils.isUs(source); + const isOurDevice = UserUtils.isUsFromCache(source); const shouldSendReceipt = isIncoming && !isGroupMessage && !isOurDevice; @@ -601,7 +605,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise { conversationId ); } - const ourNumber = window.textsecure.storage.user.getNumber(); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); // ========================================= diff --git a/ts/receiver/errors.ts b/ts/receiver/errors.ts index 2877179987..7f0c2375fd 100644 --- a/ts/receiver/errors.ts +++ b/ts/receiver/errors.ts @@ -34,14 +34,14 @@ export async function onError(ev: any) { unreadCount: toNumber(conversation.get('unreadCount')) + 1, }); - const conversationTimestamp = conversation.get('timestamp'); - const messageTimestamp = message.get('timestamp'); - if (!conversationTimestamp || messageTimestamp > conversationTimestamp) { - conversation.set({ timestamp: message.get('sent_at') }); + const conversationActiveAt = conversation.get('active_at'); + const messageTimestamp = message.get('timestamp') || 0; + if (!conversationActiveAt || messageTimestamp > conversationActiveAt) { + conversation.set({ active_at: message.get('sent_at') }); } conversation.updateLastMessage(); - conversation.notify(message); + await conversation.notify(message); if (ev.confirm) { ev.confirm(); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index b0cdf59685..8b04e3f1a3 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -1,13 +1,13 @@ import { queueAttachmentDownloads } from './attachments'; import { Quote } from './types'; -import { ConversationModel } from '../../js/models/conversations'; -import { MessageModel } from '../../js/models/messages'; import { PubKey } from '../session/types'; import _ from 'lodash'; import { SignalService } from '../protobuf'; import { StringUtils, UserUtils } from '../session/utils'; import { ConversationController } from '../session/conversations'; +import { ConversationModel } from '../models/conversation'; +import { MessageCollection, MessageModel } from '../models/message'; async function handleGroups( conversation: ConversationModel, @@ -52,10 +52,9 @@ async function handleGroups( // Check if anyone got kicked: const removedMembers = _.difference(oldMembers, attributes.members); - const isOurDeviceMap = await Promise.all( - removedMembers.map(async member => UserUtils.isUs(member)) + const ourDeviceWasRemoved = removedMembers.some(async member => + UserUtils.isUsFromCache(member) ); - const ourDeviceWasRemoved = isOurDeviceMap.includes(true); if (ourDeviceWasRemoved) { groupUpdate.kicked = 'You'; @@ -64,7 +63,7 @@ async function handleGroups( groupUpdate.kicked = removedMembers; } } else if (group.type === GROUP_TYPES.QUIT) { - if (await UserUtils.isUs(source)) { + if (UserUtils.isUsFromCache(source)) { attributes.left = true; groupUpdate = { left: 'You' }; } else { @@ -100,7 +99,7 @@ async function copyFromQuotedMessage( const firstAttachment = attachments[0]; const collection = await window.Signal.Data.getMessagesBySentAt(id, { - MessageCollection: Whisper.MessageCollection, + MessageCollection, }); const found = collection.find((item: any) => { const messageAuthor = item.getContact(); @@ -131,7 +130,7 @@ async function copyFromQuotedMessage( quote.referencedMessageNotFound = false; const queryMessage = getMessageController().register(found.id, found); - quote.text = queryMessage.get('body'); + quote.text = queryMessage.get('body') || ''; if (attemptCount > 1) { // Normally the caller would save the message, but in case we are @@ -149,7 +148,7 @@ async function copyFromQuotedMessage( try { if ( - queryMessage.get('schemaVersion') < + (queryMessage.get('schemaVersion') || 0) < TypedMessage.VERSION_NEEDED_FOR_DISPLAY ) { const upgradedMessage = await upgradeMessageSchema( @@ -253,7 +252,7 @@ async function processProfileKey( sendingDeviceConversation: ConversationModel, profileKeyBuffer: Uint8Array ) { - const ourNumber = window.textsecure.storage.user.getNumber(); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const profileKey = StringUtils.decode(profileKeyBuffer, 'base64'); if (source === ourNumber) { @@ -295,7 +294,7 @@ function updateReadStatus( // This is primarily to allow the conversation to mark all older // messages as read, as is done when we receive a read sync for // a message we already know about. - conversation.onReadMessage(message); + void conversation.onReadMessage(message, Date.now()); } } @@ -422,13 +421,13 @@ async function handleRegularMessage( handleSyncedReceipts(message, conversation); } - const conversationTimestamp = conversation.get('timestamp'); + const conversationActiveAt = conversation.get('active_at'); if ( - !conversationTimestamp || - message.get('sent_at') > conversationTimestamp + !conversationActiveAt || + (message.get('sent_at') || 0) > conversationActiveAt ) { conversation.set({ - timestamp: message.get('sent_at'), + active_at: message.get('sent_at'), lastMessage: message.getNotificationText(), }); } @@ -448,7 +447,7 @@ async function handleRegularMessage( } // we just received a message from that user so we reset the typing indicator for this convo - conversation.notifyTyping({ + await conversation.notifyTyping({ isTyping: false, sender: source, }); @@ -555,7 +554,7 @@ export async function handleMessageJob( const fetched = await window.Signal.Data.getMessageById( message.get('id'), { - Message: Whisper.Message, + Message: MessageModel, } ); @@ -572,7 +571,7 @@ export async function handleMessageJob( // We call markRead() even though the message is already // marked read because we need to start expiration // timers, etc. - message.markRead(); + await message.markRead(Date.now()); } } catch (error) { window.log.warn( @@ -583,7 +582,7 @@ export async function handleMessageJob( } if (message.get('unread')) { - conversation.notify(message); + await conversation.notify(message); } if (confirm) { diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 8c9e3ace94..ef2b90af6a 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -274,7 +274,7 @@ export async function handlePublicMessage(messageData: any) { const { source } = messageData; const { group, profile, profileKey } = messageData.message; - const isMe = await UserUtils.isUs(source); + const isMe = UserUtils.isUsFromCache(source); if (!isMe && profile) { const conversation = await ConversationController.getInstance().getOrCreateAndWait( diff --git a/ts/session/constants.ts b/ts/session/constants.ts index dfd6cb195c..85c0b0dfe2 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -1,9 +1,7 @@ -import { DAYS, MINUTES, SECONDS } from './utils/Number'; +import { DAYS, SECONDS } from './utils/Number'; // tslint:disable: binary-expression-operand-order export const TTL_DEFAULT = { - PAIRING_REQUEST: 2 * MINUTES, - DEVICE_UNPAIRING: 4 * DAYS, TYPING_MESSAGE: 20 * SECONDS, REGULAR_MESSAGE: 2 * DAYS, ENCRYPTION_PAIR_GROUP: 4 * DAYS, @@ -15,7 +13,7 @@ export const CONVERSATION = { DEFAULT_MEDIA_FETCH_COUNT: 50, DEFAULT_DOCUMENTS_FETCH_COUNT: 150, DEFAULT_MESSAGE_FETCH_COUNT: 30, - MAX_MESSAGE_FETCH_COUNT: 500, + MAX_MESSAGE_FETCH_COUNT: 1000, // Maximum voice message duraton of 5 minutes // which equates to 1.97 MB MAX_VOICE_MESSAGE_DURATION: 300, diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts deleted file mode 100644 index ff5683db18..0000000000 --- a/ts/session/conversations/ConversationController.ts +++ /dev/null @@ -1,325 +0,0 @@ -import { - ConversationAttributes, - ConversationModel, -} from '../../../js/models/conversations'; -import { BlockedNumberController } from '../../util'; - -// It's not only data from the db which is stored on the MessageController entries, we could fetch this again. What we cannot fetch from the db and which is stored here is all listeners a particular messages is linked to for instance. We will be able to get rid of this once we don't use backbone models at all -export class ConversationController { - private static instance: ConversationController | null; - private readonly conversations: any; - private _initialFetchComplete: boolean = false; - private _initialPromise?: Promise; - - private constructor() { - this.conversations = new window.Whisper.ConversationCollection(); - } - - public static getInstance() { - if (ConversationController.instance) { - return ConversationController.instance; - } - ConversationController.instance = new ConversationController(); - return ConversationController.instance; - } - - public get(id: string): ConversationModel { - if (!this._initialFetchComplete) { - throw new Error( - 'ConversationController.get() needs complete initial fetch' - ); - } - - return this.conversations.get(id); - } - - public getOrThrow(id: string): ConversationModel { - if (!this._initialFetchComplete) { - throw new Error( - 'ConversationController.get() needs complete initial fetch' - ); - } - - const convo = this.conversations.get(id); - - if (convo) { - return convo; - } - throw new Error( - `Conversation ${id} does not exist on ConversationController.get()` - ); - } - // Needed for some model setup which happens during the initial fetch() call below - public getUnsafe(id: string) { - return this.conversations.get(id); - } - - public dangerouslyCreateAndAdd(attributes: ConversationAttributes) { - return this.conversations.add(attributes); - } - - public getOrCreate(id: string, type: string) { - if (typeof id !== 'string') { - throw new TypeError("'id' must be a string"); - } - - if (type !== 'private' && type !== 'group') { - throw new TypeError( - `'type' must be 'private' or 'group'; got: '${type}'` - ); - } - - if (!this._initialFetchComplete) { - throw new Error( - 'ConversationController.get() needs complete initial fetch' - ); - } - - let conversation = this.conversations.get(id); - if (conversation) { - return conversation; - } - - conversation = this.conversations.add({ - id, - type, - version: 2, - } as any); - - const create = async () => { - if (!conversation.isValid()) { - const validationError = conversation.validationError || {}; - window.log.error( - 'Contact is not valid. Not saving, but adding to collection:', - conversation.idForLogging(), - validationError.stack - ); - - return conversation; - } - - try { - await window.Signal.Data.saveConversation(conversation.attributes, { - Conversation: window.Whisper.Conversation, - }); - } catch (error) { - window.log.error( - 'Conversation save failed! ', - id, - type, - 'Error:', - error && error.stack ? error.stack : error - ); - throw error; - } - - return conversation; - }; - - conversation.initialPromise = create(); - conversation.initialPromise.then(async () => { - if (window.inboxStore) { - conversation.on('change', this.updateReduxConvoChanged); - window.inboxStore.dispatch( - window.actionsCreators.conversationAdded( - conversation.id, - conversation.getProps() - ) - ); - } - if (!conversation.isPublic()) { - await Promise.all([ - conversation.updateProfileAvatar(), - // NOTE: we request snodes updating the cache, but ignore the result - window.SnodePool.getSnodesFor(id), - ]); - } - }); - - return conversation; - } - - public getContactProfileNameOrShortenedPubKey(pubKey: string): string { - const conversation = ConversationController.getInstance().get(pubKey); - if (!conversation) { - return pubKey; - } - return conversation.getContactProfileNameOrShortenedPubKey(); - } - - public getContactProfileNameOrFullPubKey(pubKey: string): string { - const conversation = this.conversations.get(pubKey); - if (!conversation) { - return pubKey; - } - return conversation.getContactProfileNameOrFullPubKey(); - } - - public isMediumGroup(hexEncodedGroupPublicKey: string): boolean { - const convo = this.conversations.get(hexEncodedGroupPublicKey); - if (convo) { - return convo.isMediumGroup(); - } - return false; - } - - public async getOrCreateAndWait( - id: any, - type: 'private' | 'group' - ): Promise { - const initialPromise = - this._initialPromise !== undefined - ? this._initialPromise - : Promise.resolve(); - return initialPromise.then(() => { - if (!id) { - return Promise.reject( - new Error('getOrCreateAndWait: invalid id passed.') - ); - } - const pubkey = id && id.key ? id.key : id; - const conversation = this.getOrCreate(pubkey, type); - - if (conversation) { - return conversation.initialPromise.then(() => conversation); - } - - return Promise.reject( - new Error('getOrCreateAndWait: did not get conversation') - ); - }); - } - - public async getAllGroupsInvolvingId(id: String) { - const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, { - ConversationCollection: window.Whisper.ConversationCollection, - }); - return groups.map((group: any) => this.conversations.add(group)); - } - - public async deleteContact(id: string) { - if (typeof id !== 'string') { - throw new TypeError("'id' must be a string"); - } - - if (!this._initialFetchComplete) { - throw new Error( - 'ConversationController.get() needs complete initial fetch' - ); - } - - const conversation = this.conversations.get(id); - if (!conversation) { - return; - } - - // Close group leaving - if (conversation.isClosedGroup()) { - await conversation.leaveGroup(); - } else if (conversation.isPublic()) { - const channelAPI = await conversation.getPublicSendData(); - if (channelAPI === null) { - window.log.warn(`Could not get API for public conversation ${id}`); - } else { - channelAPI.serverAPI.partChannel(channelAPI.channelId); - } - } - - await conversation.destroyMessages(); - - await window.Signal.Data.removeConversation(id, { - Conversation: window.Whisper.Conversation, - }); - conversation.off('change', this.updateReduxConvoChanged); - this.conversations.remove(conversation); - if (window.inboxStore) { - window.inboxStore.dispatch( - window.actionsCreators.conversationRemoved(conversation.id) - ); - } - } - - public getConversations(): Array { - return Array.from(this.conversations.models.values()); - } - - public async load() { - window.log.info('ConversationController: starting initial fetch'); - - if (this.conversations.length) { - throw new Error('ConversationController: Already loaded!'); - } - - const load = async () => { - try { - const collection = await window.Signal.Data.getAllConversations({ - ConversationCollection: window.Whisper.ConversationCollection, - }); - - this.conversations.add(collection.models); - - this._initialFetchComplete = true; - const promises: any = []; - this.conversations.forEach((conversation: ConversationModel) => { - if (!conversation.get('lastMessage')) { - // tslint:disable-next-line: no-void-expression - promises.push(conversation.updateLastMessage()); - } - - promises.concat([ - conversation.updateProfileName(), - conversation.updateProfileAvatar(), - ]); - }); - this.conversations.forEach((conversation: ConversationModel) => { - // register for change event on each conversation, and forward to redux - conversation.on('change', this.updateReduxConvoChanged); - }); - await Promise.all(promises); - - // Remove any unused images - window.profileImages.removeImagesNotInArray( - this.conversations.map((c: any) => c.id) - ); - window.log.info('ConversationController: done with initial fetch'); - } catch (error) { - window.log.error( - 'ConversationController: initial fetch failed', - error && error.stack ? error.stack : error - ); - throw error; - } - }; - await BlockedNumberController.load(); - - this._initialPromise = load(); - - return this._initialPromise; - } - - public loadPromise() { - return this._initialPromise; - } - public reset() { - this._initialPromise = Promise.resolve(); - this._initialFetchComplete = false; - if (window.inboxStore) { - this.conversations.forEach((convo: ConversationModel) => - convo.off('change', this.updateReduxConvoChanged) - ); - - window.inboxStore.dispatch( - window.actionsCreators.removeAllConversations() - ); - } - this.conversations.reset([]); - } - - private updateReduxConvoChanged(convo: ConversationModel) { - if (window.inboxStore) { - window.inboxStore.dispatch( - window.actionsCreators.conversationChanged(convo.id, convo.getProps()) - ); - } - } -} diff --git a/ts/session/conversations/index.ts b/ts/session/conversations/index.ts index 3982df2e4e..6830585b70 100644 --- a/ts/session/conversations/index.ts +++ b/ts/session/conversations/index.ts @@ -1,3 +1,326 @@ -import { ConversationController } from './ConversationController'; +import { + ConversationAttributes, + ConversationCollection, + ConversationModel, +} from '../../models/conversation'; +import { BlockedNumberController } from '../../util'; -export { ConversationController }; +// It's not only data from the db which is stored on the MessageController entries, we could fetch this again. What we cannot fetch from the db and which is stored here is all listeners a particular messages is linked to for instance. We will be able to get rid of this once we don't use backbone models at all +export class ConversationController { + private static instance: ConversationController | null; + private readonly conversations: any; + private _initialFetchComplete: boolean = false; + private _initialPromise?: Promise; + + private constructor() { + this.conversations = new ConversationCollection(); + } + + public static getInstance() { + if (ConversationController.instance) { + return ConversationController.instance; + } + ConversationController.instance = new ConversationController(); + return ConversationController.instance; + } + + public get(id: string): ConversationModel { + if (!this._initialFetchComplete) { + throw new Error( + 'ConversationController.get() needs complete initial fetch' + ); + } + + return this.conversations.get(id); + } + + public getOrThrow(id: string): ConversationModel { + if (!this._initialFetchComplete) { + throw new Error( + 'ConversationController.get() needs complete initial fetch' + ); + } + + const convo = this.conversations.get(id); + + if (convo) { + return convo; + } + throw new Error( + `Conversation ${id} does not exist on ConversationController.get()` + ); + } + // Needed for some model setup which happens during the initial fetch() call below + public getUnsafe(id: string) { + return this.conversations.get(id); + } + + public dangerouslyCreateAndAdd(attributes: ConversationAttributes) { + return this.conversations.add(attributes); + } + + public getOrCreate(id: string, type: string) { + if (typeof id !== 'string') { + throw new TypeError("'id' must be a string"); + } + + if (type !== 'private' && type !== 'group') { + throw new TypeError( + `'type' must be 'private' or 'group'; got: '${type}'` + ); + } + + if (!this._initialFetchComplete) { + throw new Error( + 'ConversationController.get() needs complete initial fetch' + ); + } + + let conversation = this.conversations.get(id); + if (conversation) { + return conversation; + } + + conversation = this.conversations.add({ + id, + type, + version: 2, + } as any); + + const create = async () => { + if (!conversation.isValid()) { + const validationError = conversation.validationError || {}; + window.log.error( + 'Contact is not valid. Not saving, but adding to collection:', + conversation.idForLogging(), + validationError.stack + ); + + return conversation; + } + + try { + await window.Signal.Data.saveConversation(conversation.attributes, { + Conversation: window.Whisper.Conversation, + }); + } catch (error) { + window.log.error( + 'Conversation save failed! ', + id, + type, + 'Error:', + error && error.stack ? error.stack : error + ); + throw error; + } + + return conversation; + }; + + conversation.initialPromise = create(); + conversation.initialPromise.then(async () => { + if (window.inboxStore) { + conversation.on('change', this.updateReduxConvoChanged); + window.inboxStore.dispatch( + window.actionsCreators.conversationAdded( + conversation.id, + conversation.getProps() + ) + ); + } + if (!conversation.isPublic()) { + await Promise.all([ + conversation.updateProfileAvatar(), + // NOTE: we request snodes updating the cache, but ignore the result + window.SnodePool.getSnodesFor(id), + ]); + } + }); + + return conversation; + } + + public getContactProfileNameOrShortenedPubKey(pubKey: string): string { + const conversation = ConversationController.getInstance().get(pubKey); + if (!conversation) { + return pubKey; + } + return conversation.getContactProfileNameOrShortenedPubKey(); + } + + public getContactProfileNameOrFullPubKey(pubKey: string): string { + const conversation = this.conversations.get(pubKey); + if (!conversation) { + return pubKey; + } + return conversation.getContactProfileNameOrFullPubKey(); + } + + public isMediumGroup(hexEncodedGroupPublicKey: string): boolean { + const convo = this.conversations.get(hexEncodedGroupPublicKey); + if (convo) { + return convo.isMediumGroup(); + } + return false; + } + + public async getOrCreateAndWait( + id: any, + type: 'private' | 'group' + ): Promise { + const initialPromise = + this._initialPromise !== undefined + ? this._initialPromise + : Promise.resolve(); + return initialPromise.then(() => { + if (!id) { + return Promise.reject( + new Error('getOrCreateAndWait: invalid id passed.') + ); + } + const pubkey = id && id.key ? id.key : id; + const conversation = this.getOrCreate(pubkey, type); + + if (conversation) { + return conversation.initialPromise.then(() => conversation); + } + + return Promise.reject( + new Error('getOrCreateAndWait: did not get conversation') + ); + }); + } + + public async getAllGroupsInvolvingId(id: String) { + const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, { + ConversationCollection, + }); + return groups.map((group: any) => this.conversations.add(group)); + } + + public async deleteContact(id: string) { + if (typeof id !== 'string') { + throw new TypeError("'id' must be a string"); + } + + if (!this._initialFetchComplete) { + throw new Error( + 'ConversationController.get() needs complete initial fetch' + ); + } + + const conversation = this.conversations.get(id); + if (!conversation) { + return; + } + + // Close group leaving + if (conversation.isClosedGroup()) { + await conversation.leaveGroup(); + } else if (conversation.isPublic()) { + const channelAPI = await conversation.getPublicSendData(); + if (channelAPI === null) { + window.log.warn(`Could not get API for public conversation ${id}`); + } else { + channelAPI.serverAPI.partChannel(channelAPI.channelId); + } + } + + await conversation.destroyMessages(); + + await window.Signal.Data.removeConversation(id, { + Conversation: window.Whisper.Conversation, + }); + conversation.off('change', this.updateReduxConvoChanged); + this.conversations.remove(conversation); + if (window.inboxStore) { + window.inboxStore.dispatch( + window.actionsCreators.conversationRemoved(conversation.id) + ); + } + } + + public getConversations(): Array { + return Array.from(this.conversations.models); + } + + public async load() { + window.log.info('ConversationController: starting initial fetch'); + + if (this.conversations.length) { + throw new Error('ConversationController: Already loaded!'); + } + + const load = async () => { + try { + const collection = await window.Signal.Data.getAllConversations({ + ConversationCollection, + }); + + this.conversations.add(collection.models); + + this._initialFetchComplete = true; + const promises: any = []; + this.conversations.forEach((conversation: ConversationModel) => { + if (!conversation.get('lastMessage')) { + // tslint:disable-next-line: no-void-expression + promises.push(conversation.updateLastMessage()); + } + + promises.concat([ + conversation.updateProfileName(), + conversation.updateProfileAvatar(), + ]); + }); + this.conversations.forEach((conversation: ConversationModel) => { + // register for change event on each conversation, and forward to redux + conversation.on('change', this.updateReduxConvoChanged); + }); + await Promise.all(promises); + + // Remove any unused images + window.profileImages.removeImagesNotInArray( + this.conversations.map((c: any) => c.id) + ); + window.log.info('ConversationController: done with initial fetch'); + } catch (error) { + window.log.error( + 'ConversationController: initial fetch failed', + error && error.stack ? error.stack : error + ); + throw error; + } + }; + await BlockedNumberController.load(); + + this._initialPromise = load(); + + return this._initialPromise; + } + + public loadPromise() { + return this._initialPromise; + } + public reset() { + this._initialPromise = Promise.resolve(); + this._initialFetchComplete = false; + if (window.inboxStore) { + this.conversations.forEach((convo: ConversationModel) => + convo.off('change', this.updateReduxConvoChanged) + ); + + window.inboxStore.dispatch( + window.actionsCreators.removeAllConversations() + ); + } + this.conversations.reset([]); + } + + private updateReduxConvoChanged(convo: ConversationModel) { + if (window.inboxStore) { + window.inboxStore.dispatch( + window.actionsCreators.conversationChanged(convo.id, convo.getProps()) + ); + } + } +} diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 4d07b074d7..b7f15cb057 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -3,24 +3,28 @@ import * as Data from '../../../js/modules/data'; import _ from 'lodash'; import { fromHex, fromHexToArray, toHex } from '../utils/String'; -import { MessageModel, MessageModelType } from '../../../js/models/messages'; -import { ConversationModel } from '../../../js/models/conversations'; import { BlockedNumberController } from '../../util/blockedNumberController'; import { ConversationController } from '../conversations'; import { updateOpenGroup } from '../../receiver/openGroups'; import { getMessageQueue } from '../instance'; -import { - ClosedGroupEncryptionPairMessage, - ClosedGroupNewMessage, - ClosedGroupUpdateMessage, - ExpirationTimerUpdateMessage, -} from '../messages/outgoing'; +import { ExpirationTimerUpdateMessage } from '../messages/outgoing'; import uuid from 'uuid'; import { SignalService } from '../../protobuf'; import { generateCurve25519KeyPairWithoutPrefix } from '../crypto'; import { encryptUsingSessionProtocol } from '../crypto/MessageEncrypter'; import { ECKeyPair } from '../../receiver/keypairs'; import { UserUtils } from '../utils'; +import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage'; +import { + ClosedGroupAddedMembersMessage, + ClosedGroupEncryptionPairMessage, + ClosedGroupNameChangeMessage, + ClosedGroupNewMessage, + ClosedGroupRemovedMembersMessage, +} from '../messages/outgoing/content/data/group'; +import { ConversationModel } from '../../models/conversation'; +import { MessageModel } from '../../models/message'; +import { MessageModelType } from '../../models/messageType'; export interface GroupInfo { id: string; @@ -106,11 +110,6 @@ export async function initiateGroupUpdate( await updateOrCreateClosedGroup(groupDetails); - if (avatar) { - // would get to download this file on each client in the group - // and reference the local file - } - const updateObj: GroupInfo = { id: groupId, name: groupName, @@ -119,10 +118,51 @@ export async function initiateGroupUpdate( expireTimer: convo.get('expireTimer'), }; - const dbMessage = await addUpdateMessage(convo, diff, 'outgoing'); - window.getMessageController().register(dbMessage.id, dbMessage); + if (diff.newName?.length) { + const nameOnlyDiff: GroupDiff = { newName: diff.newName }; + const dbMessageName = await addUpdateMessage( + convo, + nameOnlyDiff, + 'outgoing' + ); + window.getMessageController().register(dbMessageName.id, dbMessageName); + await sendNewName(convo, diff.newName, dbMessageName.id); + } + + if (diff.joiningMembers?.length) { + const joiningOnlyDiff: GroupDiff = { joiningMembers: diff.joiningMembers }; + const dbMessageAdded = await addUpdateMessage( + convo, + joiningOnlyDiff, + 'outgoing' + ); + window.getMessageController().register(dbMessageAdded.id, dbMessageAdded); + await sendAddedMembers( + convo, + diff.joiningMembers, + dbMessageAdded.id, + updateObj + ); + } - await sendGroupUpdateForClosed(convo, diff, updateObj, dbMessage.id); + if (diff.leavingMembers?.length) { + const leavingOnlyDiff: GroupDiff = { leavingMembers: diff.leavingMembers }; + const dbMessageLeaving = await addUpdateMessage( + convo, + leavingOnlyDiff, + 'outgoing' + ); + window + .getMessageController() + .register(dbMessageLeaving.id, dbMessageLeaving); + const stillMembers = members; + await sendRemovedMembers( + convo, + diff.leavingMembers, + dbMessageLeaving.id, + stillMembers + ); + } } export async function addUpdateMessage( @@ -146,7 +186,7 @@ export async function addUpdateMessage( const now = Date.now(); - const markUnread = type === 'incoming'; + const unread = type === 'incoming'; const message = await convo.addMessage({ conversationId: convo.get('id'), @@ -154,10 +194,11 @@ export async function addUpdateMessage( sent_at: now, received_at: now, group_update: groupUpdate, - unread: markUnread, + unread, + expireTimer: 0, }); - if (markUnread) { + if (unread) { // update the unreadCount for this convo const unreadCount = await convo.getUnreadCount(); convo.set({ @@ -255,38 +296,40 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { if (expireTimer === undefined || typeof expireTimer !== 'number') { return; } - const source = await UserUtils.getCurrentDevicePubKey(); + const source = UserUtils.getOurPubKeyStrFromCache(); await conversation.updateExpirationTimer(expireTimer, source, Date.now(), { fromSync: true, }); } export async function leaveClosedGroup(groupId: string) { - window.SwarmPolling.removePubkey(groupId); - const convo = ConversationController.getInstance().get(groupId); if (!convo) { window.log.error('Cannot leave non-existing group'); return; } - const ourNumber = await UserUtils.getOurNumber(); + const ourNumber = UserUtils.getOurPubKeyFromCache(); const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourNumber.key); const now = Date.now(); let members: Array = []; + let admins: Array = []; - // for now, a destroyed group is one with those 2 flags set to true. - // FIXME audric, add a flag to conversation model when a group is destroyed + // if we are the admin, the group must be destroyed for every members if (isCurrentUserAdmin) { window.log.info('Admin left a closed group. We need to destroy it'); convo.set({ left: true }); members = []; + admins = []; } else { + // otherwise, just the exclude ourself from the members and trigger an update with this convo.set({ left: true }); - members = convo.get('members').filter(m => m !== ourNumber.key); + members = (convo.get('members') || []).filter(m => m !== ourNumber.key); + admins = convo.get('groupAdmins') || []; } convo.set({ members }); + convo.set({ groupAdmins: admins }); await convo.commit(); const dbMessage = await convo.addMessage({ @@ -295,40 +338,66 @@ export async function leaveClosedGroup(groupId: string) { type: 'outgoing', sent_at: now, received_at: now, + expireTimer: 0, }); window.getMessageController().register(dbMessage.id, dbMessage); - const groupUpdate: GroupInfo = { - id: convo.get('id'), - name: convo.get('name'), - members, - admins: convo.get('groupAdmins'), - }; + // Send the update to the group + const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ + timestamp: Date.now(), + groupId, + identifier: dbMessage.id, + expireTimer: 0, + }); - await sendGroupUpdateForClosed( - convo, - { leavingMembers: [ourNumber.key] }, - groupUpdate, - dbMessage.id + window.log.info( + `We are leaving the group ${groupId}. Sending our leaving message.` ); + // sent the message to the group and once done, remove everything related to this group + window.SwarmPolling.removePubkey(groupId); + await getMessageQueue().sendToGroup(ourLeavingMessage, async () => { + window.log.info( + `Leaving message sent ${groupId}. Removing everything related to this group.` + ); + await Data.removeAllClosedGroupEncryptionKeyPairs(groupId); + }); } -export async function sendGroupUpdateForClosed( +async function sendNewName( convo: ConversationModel, - diff: MemberChanges, - groupUpdate: GroupInfo, + name: string, messageId: string ) { - const { id: groupId, members, name: groupName, expireTimer } = groupUpdate; - const ourNumber = await UserUtils.getOurNumber(); + if (name.length === 0) { + window.log.warn('No name given for group update. Skipping'); + return; + } - const removedMembers = diff.leavingMembers || []; - const newMembers = diff.joiningMembers || []; // joining members - const wasAnyUserRemoved = removedMembers.length > 0; - const isUserLeaving = removedMembers.includes(ourNumber.key); - const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourNumber.key); - const expireTimerToShare = expireTimer || 0; + const groupId = convo.get('id'); + + // Send the update to the group + const nameChangeMessage = new ClosedGroupNameChangeMessage({ + timestamp: Date.now(), + groupId, + identifier: messageId, + expireTimer: 0, + name, + }); + await getMessageQueue().sendToGroup(nameChangeMessage); +} + +async function sendAddedMembers( + convo: ConversationModel, + addedMembers: Array, + messageId: string, + groupUpdate: GroupInfo +) { + if (!addedMembers?.length) { + window.log.warn('No addedMembers given for group update. Skipping'); + return; + } + const { id: groupId, members, name: groupName } = groupUpdate; const admins = groupUpdate.admins || []; // Check preconditions @@ -340,103 +409,106 @@ export async function sendGroupUpdateForClosed( } const encryptionKeyPair = ECKeyPair.fromHexKeyPair(hexEncryptionKeyPair); + const expireTimer = convo.get('expireTimer') || 0; - if (removedMembers.includes(admins[0]) && newMembers.length !== 0) { - throw new Error( - "Can't remove admin from closed group without removing everyone." - ); // Error.invalidClosedGroupUpdate - } - - if (isUserLeaving && newMembers.length !== 0) { - if (removedMembers.length !== 1 || newMembers.length !== 0) { - throw new Error( - "Can't remove self and add or remove others simultaneously." - ); - } - } - - // Send the update to the group - const mainClosedGroupUpdate = new ClosedGroupUpdateMessage({ + // Send the Added Members message to the group (only members already in the group will get it) + const closedGroupControlMessage = new ClosedGroupAddedMembersMessage({ timestamp: Date.now(), groupId, + addedMembers, + identifier: messageId, + expireTimer, + }); + await getMessageQueue().sendToGroup(closedGroupControlMessage); + + // Send closed group update messages to any new members individually + const newClosedGroupUpdate = new ClosedGroupNewMessage({ + timestamp: Date.now(), name: groupName, + groupId, + admins, members, + keypair: encryptionKeyPair, identifier: messageId || uuid(), - expireTimer: expireTimerToShare, + expireTimer, + }); + + // if an expire timer is set, we have to send it to the joining members + let expirationTimerMessage: ExpirationTimerUpdateMessage | undefined; + if (expireTimer && expireTimer > 0) { + const expireUpdate = { + timestamp: Date.now(), + expireTimer, + groupId: groupId, + }; + + expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdate); + } + const promises = addedMembers.map(async m => { + await ConversationController.getInstance().getOrCreateAndWait(m, 'private'); + const memberPubKey = PubKey.cast(m); + await getMessageQueue().sendToPubKey(memberPubKey, newClosedGroupUpdate); + + if (expirationTimerMessage) { + await getMessageQueue().sendToPubKey( + memberPubKey, + expirationTimerMessage + ); + } }); + await Promise.all(promises); +} +async function sendRemovedMembers( + convo: ConversationModel, + removedMembers: Array, + messageId: string, + stillMembers: Array +) { + if (!removedMembers?.length) { + window.log.warn('No removedMembers given for group update. Skipping'); + return; + } + const ourNumber = UserUtils.getOurPubKeyFromCache(); + const admins = convo.get('groupAdmins') || []; + const groupId = convo.get('id'); + + const isCurrentUserAdmin = admins.includes(ourNumber.key); + const isUserLeaving = removedMembers.includes(ourNumber.key); if (isUserLeaving) { - window.log.info( - `We are leaving the group ${groupId}. Sending our leaving message.` + throw new Error( + 'Cannot remove members and leave the group at the same time' ); - // sent the message to the group and once done, remove everything related to this group - window.SwarmPolling.removePubkey(groupId); - await getMessageQueue().sendToGroup(mainClosedGroupUpdate, async () => { - window.log.info( - `Leaving message sent ${groupId}. Removing everything related to this group.` - ); - await Data.removeAllClosedGroupEncryptionKeyPairs(groupId); - }); - } else { - // Send the group update, and only once sent, generate and distribute a new encryption key pair if needed - await getMessageQueue().sendToGroup(mainClosedGroupUpdate, async () => { - if (wasAnyUserRemoved && isCurrentUserAdmin) { + } + if (removedMembers.includes(admins[0]) && stillMembers.length !== 0) { + throw new Error( + "Can't remove admin from closed group without removing everyone." + ); + } + const expireTimer = convo.get('expireTimer') || 0; + + // Send the update to the group and generate + distribute a new encryption key pair if needed + const mainClosedGroupControlMessage = new ClosedGroupRemovedMembersMessage({ + timestamp: Date.now(), + groupId, + removedMembers, + identifier: messageId, + expireTimer, + }); + // Send the group update, and only once sent, generate and distribute a new encryption key pair if needed + await getMessageQueue().sendToGroup( + mainClosedGroupControlMessage, + async () => { + if (isCurrentUserAdmin) { // we send the new encryption key only to members already here before the update - const membersNotNew = members.filter(m => !newMembers.includes(m)); window.log.info( `Sending group update: A user was removed from ${groupId} and we are the admin. Generating and sending a new EncryptionKeyPair` ); - await generateAndSendNewEncryptionKeyPair(groupId, membersNotNew); - } - }); - - if (newMembers.length) { - // Send closed group update messages to any new members individually - const newClosedGroupUpdate = new ClosedGroupNewMessage({ - timestamp: Date.now(), - name: groupName, - groupId, - admins, - members, - keypair: encryptionKeyPair, - identifier: messageId || uuid(), - expireTimer: expireTimerToShare, - }); - - // if an expiretimer in this ClosedGroup already, send it in another message - // if an expire timer is set, we have to send it to the joining members - let expirationTimerMessage: ExpirationTimerUpdateMessage | undefined; - if (expireTimer && expireTimer > 0) { - const expireUpdate = { - timestamp: Date.now(), - expireTimer, - groupId: groupId, - }; - - expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdate); + await generateAndSendNewEncryptionKeyPair(groupId, stillMembers); } - const promises = newMembers.map(async m => { - await ConversationController.getInstance().getOrCreateAndWait( - m, - 'private' - ); - const memberPubKey = PubKey.cast(m); - await getMessageQueue().sendToPubKey( - memberPubKey, - newClosedGroupUpdate - ); - - if (expirationTimerMessage) { - await getMessageQueue().sendToPubKey( - memberPubKey, - expirationTimerMessage - ); - } - }); - await Promise.all(promises); } - } + ); } export async function generateAndSendNewEncryptionKeyPair( @@ -461,7 +533,7 @@ export async function generateAndSendNewEncryptionKeyPair( return; } - const ourNumber = await UserUtils.getOurNumber(); + const ourNumber = UserUtils.getOurPubKeyFromCache(); if (!groupConvo.get('groupAdmins')?.includes(ourNumber.key)) { window.log.warn( 'generateAndSendNewEncryptionKeyPair: cannot send it as a non admin' @@ -504,13 +576,13 @@ export async function generateAndSendNewEncryptionKeyPair( }) ); - const expireTimerToShare = groupConvo.get('expireTimer') || 0; + const expireTimer = groupConvo.get('expireTimer') || 0; const keypairsMessage = new ClosedGroupEncryptionPairMessage({ groupId: toHex(groupId), timestamp: Date.now(), encryptedKeyPairs: wrappers, - expireTimer: expireTimerToShare, + expireTimer, }); const messageSentCallback = async () => { @@ -518,7 +590,6 @@ export async function generateAndSendNewEncryptionKeyPair( `KeyPairMessage for ClosedGroup ${groupPublicKey} is sent. Saving the new encryptionKeyPair.` ); - // tslint:disable-next-line: no-non-null-assertion await Data.addClosedGroupEncryptionKeyPair( toHex(groupId), newKeyPair.toHexKeyPair() diff --git a/ts/session/messages/MessageController.ts b/ts/session/messages/MessageController.ts index fc23fa972b..b9ca30491a 100644 --- a/ts/session/messages/MessageController.ts +++ b/ts/session/messages/MessageController.ts @@ -1,8 +1,8 @@ // You can see MessageController for in memory registered messages. // Ee register messages to it everytime we send one, so that when an event happens we can find which message it was based on this id. -import { ConversationModel } from '../../../js/models/conversations'; -import { MessageModel } from '../../../js/models/messages'; +import { ConversationModel } from '../../models/conversation'; +import { MessageCollection, MessageModel } from '../../models/message'; type MessageControllerEntry = { message: MessageModel; @@ -75,7 +75,7 @@ export class MessageController { let messages = []; const messageSet = await window.Signal.Data.getMessagesByConversation(key, { limit: 100, - MessageCollection: window.Whisper.MessageCollection, + MessageCollection, }); messages = messageSet.models.map( diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupAddedMembersMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupAddedMembersMessage.ts new file mode 100644 index 0000000000..5d765685db --- /dev/null +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupAddedMembersMessage.ts @@ -0,0 +1,47 @@ +import { fromHex } from 'bytebuffer'; +import { Constants } from '../../../../..'; +import { SignalService } from '../../../../../../protobuf'; +import { fromHexToArray } from '../../../../../utils/String'; +import { + ClosedGroupMessage, + ClosedGroupMessageParams, +} from './ClosedGroupMessage'; + +interface ClosedGroupAddedMembersMessageParams + extends ClosedGroupMessageParams { + addedMembers: Array; +} + +export class ClosedGroupAddedMembersMessage extends ClosedGroupMessage { + private readonly addedMembers: Array; + + constructor(params: ClosedGroupAddedMembersMessageParams) { + super({ + timestamp: params.timestamp, + identifier: params.identifier, + groupId: params.groupId, + expireTimer: params.expireTimer, + }); + this.addedMembers = params.addedMembers; + if (!this.addedMembers?.length) { + throw new Error('addedMembers cannot be empty'); + } + } + + public dataProto(): SignalService.DataMessage { + const dataMessage = super.dataProto(); + + // tslint:disable: no-non-null-assertion + dataMessage.closedGroupControlMessage!.type = + SignalService.DataMessage.ClosedGroupControlMessage.Type.MEMBERS_ADDED; + dataMessage.closedGroupControlMessage!.members = this.addedMembers.map( + fromHexToArray + ); + + return dataMessage; + } + + public ttl(): number { + return Constants.TTL_DEFAULT.REGULAR_MESSAGE; + } +} diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage.ts new file mode 100644 index 0000000000..e8d9e37dc6 --- /dev/null +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage.ts @@ -0,0 +1,31 @@ +import { Constants } from '../../../../..'; +import { SignalService } from '../../../../../../protobuf'; +import { + ClosedGroupMessage, + ClosedGroupMessageParams, +} from './ClosedGroupMessage'; + +export class ClosedGroupMemberLeftMessage extends ClosedGroupMessage { + constructor(params: ClosedGroupMessageParams) { + super({ + timestamp: params.timestamp, + identifier: params.identifier, + groupId: params.groupId, + expireTimer: params.expireTimer, + }); + } + + public dataProto(): SignalService.DataMessage { + const dataMessage = super.dataProto(); + + // tslint:disable: no-non-null-assertion + dataMessage.closedGroupControlMessage!.type = + SignalService.DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT; + + return dataMessage; + } + + public ttl(): number { + return Constants.TTL_DEFAULT.REGULAR_MESSAGE; + } +} diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupNameChangeMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupNameChangeMessage.ts new file mode 100644 index 0000000000..06d45bcdb9 --- /dev/null +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupNameChangeMessage.ts @@ -0,0 +1,42 @@ +import { Constants } from '../../../../..'; +import { SignalService } from '../../../../../../protobuf'; +import { + ClosedGroupMessage, + ClosedGroupMessageParams, +} from './ClosedGroupMessage'; + +interface ClosedGroupNameChangeMessageParams extends ClosedGroupMessageParams { + name: string; +} + +export class ClosedGroupNameChangeMessage extends ClosedGroupMessage { + private readonly name: string; + + constructor(params: ClosedGroupNameChangeMessageParams) { + super({ + timestamp: params.timestamp, + identifier: params.identifier, + groupId: params.groupId, + expireTimer: params.expireTimer, + }); + this.name = params.name; + if (this.name.length === 0) { + throw new Error('name cannot be empty'); + } + } + + public dataProto(): SignalService.DataMessage { + const dataMessage = super.dataProto(); + + // tslint:disable: no-non-null-assertion + dataMessage.closedGroupControlMessage!.type = + SignalService.DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE; + dataMessage.closedGroupControlMessage!.name = this.name; + + return dataMessage; + } + + public ttl(): number { + return Constants.TTL_DEFAULT.REGULAR_MESSAGE; + } +} diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupRemovedMembersMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupRemovedMembersMessage.ts new file mode 100644 index 0000000000..184bd9c769 --- /dev/null +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupRemovedMembersMessage.ts @@ -0,0 +1,46 @@ +import { Constants } from '../../../../..'; +import { SignalService } from '../../../../../../protobuf'; +import { fromHexToArray } from '../../../../../utils/String'; +import { + ClosedGroupMessage, + ClosedGroupMessageParams, +} from './ClosedGroupMessage'; + +interface ClosedGroupRemovedMembersMessageParams + extends ClosedGroupMessageParams { + removedMembers: Array; +} + +export class ClosedGroupRemovedMembersMessage extends ClosedGroupMessage { + private readonly removedMembers: Array; + + constructor(params: ClosedGroupRemovedMembersMessageParams) { + super({ + timestamp: params.timestamp, + identifier: params.identifier, + groupId: params.groupId, + expireTimer: params.expireTimer, + }); + this.removedMembers = params.removedMembers; + if (!this.removedMembers?.length) { + throw new Error('removedMembers cannot be empty'); + } + } + + public dataProto(): SignalService.DataMessage { + const dataMessage = super.dataProto(); + + // tslint:disable: no-non-null-assertion + dataMessage.closedGroupControlMessage!.type = + SignalService.DataMessage.ClosedGroupControlMessage.Type.MEMBERS_REMOVED; + dataMessage.closedGroupControlMessage!.members = this.removedMembers.map( + fromHexToArray + ); + + return dataMessage; + } + + public ttl(): number { + return Constants.TTL_DEFAULT.REGULAR_MESSAGE; + } +} diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts deleted file mode 100644 index f5a0b2c731..0000000000 --- a/ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { SignalService } from '../../../../../../protobuf'; -import { - ClosedGroupMessage, - ClosedGroupMessageParams, -} from './ClosedGroupMessage'; -import { fromHexToArray } from '../../../../../utils/String'; - -export interface ClosedGroupUpdateMessageParams - extends ClosedGroupMessageParams { - name: string; - members: Array; - expireTimer: number; -} - -export class ClosedGroupUpdateMessage extends ClosedGroupMessage { - private readonly name: string; - private readonly members: Array; - - constructor(params: ClosedGroupUpdateMessageParams) { - super({ - timestamp: params.timestamp, - identifier: params.identifier, - groupId: params.groupId, - expireTimer: params.expireTimer, - }); - this.name = params.name; - this.members = params.members; - - // members can be empty. It means noone is in the group anymore and it happens when an admin leaves the group - if (!params.members) { - throw new Error('Members must be set'); - } - if (!params.name || params.name.length === 0) { - throw new Error('Name must cannot be empty'); - } - } - - public dataProto(): SignalService.DataMessage { - const dataMessage = new SignalService.DataMessage(); - - dataMessage.closedGroupControlMessage = new SignalService.DataMessage.ClosedGroupControlMessage(); - dataMessage.closedGroupControlMessage.type = - SignalService.DataMessage.ClosedGroupControlMessage.Type.UPDATE; - dataMessage.closedGroupControlMessage.name = this.name; - dataMessage.closedGroupControlMessage.members = this.members.map( - fromHexToArray - ); - - return dataMessage; - } -} diff --git a/ts/session/messages/outgoing/content/data/group/index.ts b/ts/session/messages/outgoing/content/data/group/index.ts index a78a92de1b..e9f861ff76 100644 --- a/ts/session/messages/outgoing/content/data/group/index.ts +++ b/ts/session/messages/outgoing/content/data/group/index.ts @@ -1,4 +1,6 @@ export * from './ClosedGroupChatMessage'; export * from './ClosedGroupEncryptionPairMessage'; export * from './ClosedGroupNewMessage'; -export * from './ClosedGroupUpdateMessage'; +export * from './ClosedGroupAddedMembersMessage'; +export * from './ClosedGroupNameChangeMessage'; +export * from './ClosedGroupRemovedMembersMessage'; diff --git a/ts/session/messages/outgoing/content/data/index.ts b/ts/session/messages/outgoing/content/data/index.ts index d7f7ed20ac..0aba320568 100644 --- a/ts/session/messages/outgoing/content/data/index.ts +++ b/ts/session/messages/outgoing/content/data/index.ts @@ -5,6 +5,5 @@ export * from './group/ClosedGroupMessage'; export * from './group/ClosedGroupChatMessage'; export * from './group/ClosedGroupEncryptionPairMessage'; export * from './group/ClosedGroupNewMessage'; -export * from './group/ClosedGroupUpdateMessage'; export * from './group/ClosedGroupMessage'; export * from './ExpirationTimerUpdateMessage'; diff --git a/ts/session/onions/index.ts b/ts/session/onions/index.ts index 53a2c1cd43..b6b9cb467c 100644 --- a/ts/session/onions/index.ts +++ b/ts/session/onions/index.ts @@ -3,6 +3,7 @@ import * as Data from '../../../js/modules/data'; import * as SnodePool from '../snode_api/snodePool'; import _ from 'lodash'; import fetch from 'node-fetch'; +import { UserUtils } from '../utils'; type Snode = SnodePool.Snode; @@ -126,7 +127,7 @@ class OnionPaths { const url = `https://${snode.ip}:${snode.port}${endpoint}`; - const ourPK = window.textsecure.storage.user.getNumber(); + const ourPK = UserUtils.getOurPubKeyStrFromCache(); const pubKey = window.getStoragePubKey(ourPK); // truncate if testnet const method = 'get_snodes_for_pubkey'; diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 8ad9c86970..7e5cc91f94 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -113,12 +113,7 @@ export class MessageQueue implements MessageQueueInterface { if (!message) { return; } - - const ourPubKey = await UserUtils.getCurrentDevicePubKey(); - - if (!ourPubKey) { - throw new Error('ourNumber is not set'); - } + const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); window.log.warn('sendSyncMessage TODO with syncTarget'); await this.sendMessageToDevices([PubKey.cast(ourPubKey)], message, sentCb); @@ -182,7 +177,7 @@ export class MessageQueue implements MessageQueueInterface { sentCb?: (message: RawMessage) => Promise ): Promise { // Don't send to ourselves - const currentDevice = await UserUtils.getCurrentDevicePubKey(); + const currentDevice = UserUtils.getOurPubKeyFromCache(); if (currentDevice && device.isEqual(currentDevice)) { return; } diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index e6a2ad260a..0be3483db8 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -1,4 +1,8 @@ -import { ContentMessage, OpenGroupMessage } from '../messages/outgoing'; +import { + ContentMessage, + ExpirationTimerUpdateMessage, + OpenGroupMessage, +} from '../messages/outgoing'; import { RawMessage } from '../types/RawMessage'; import { TypedEventEmitter } from '../utils'; import { PubKey } from '../types'; @@ -8,7 +12,8 @@ import { ClosedGroupChatMessage } from '../messages/outgoing/content/data/group/ export type GroupMessageType = | OpenGroupMessage | ClosedGroupChatMessage - | ClosedGroupMessage; + | ClosedGroupMessage + | ExpirationTimerUpdateMessage; export interface MessageQueueInterfaceEvents { sendSuccess: ( message: RawMessage | OpenGroupMessage, diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index 6135623662..7aae8989ed 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -7,8 +7,8 @@ import _ from 'lodash'; import * as Data from '../../../js/modules/data'; import { StringUtils } from '../../session/utils'; -import { ConversationController } from '../conversations/ConversationController'; -import { ConversationModel } from '../../../js/models/conversations'; +import { ConversationController } from '../conversations'; +import { ConversationModel } from '../../models/conversation'; type PubkeyToHash = { [key: string]: string }; @@ -155,17 +155,17 @@ export class SwarmPolling { private loadGroupIds() { // Start polling for medium size groups as well (they might be in different swarms) - const convos = ConversationController.getInstance() - .getConversations() - .filter( - (c: ConversationModel) => - c.isMediumGroup() && - !c.isBlocked() && - !c.get('isKickedFromGroup') && - !c.get('left') - ); - - convos.forEach((c: any) => { + const convos = ConversationController.getInstance().getConversations(); + + const mediumGroupsOnly = convos.filter( + (c: ConversationModel) => + c.isMediumGroup() && + !c.isBlocked() && + !c.get('isKickedFromGroup') && + !c.get('left') + ); + + mediumGroupsOnly.forEach((c: any) => { this.addGroupId(new PubKey(c.id)); // TODO: unsubscribe if the group is deleted }); diff --git a/ts/session/types/OpenGroup.ts b/ts/session/types/OpenGroup.ts index e4e9388433..7648e412f2 100644 --- a/ts/session/types/OpenGroup.ts +++ b/ts/session/types/OpenGroup.ts @@ -1,4 +1,4 @@ -import { ConversationModel } from '../../../js/models/conversations'; +import { ConversationModel } from '../../models/conversation'; import { ConversationController } from '../conversations'; import { PromiseUtils } from '../utils'; diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index cf00ad05da..ac27091b49 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -5,13 +5,19 @@ import { KeyPair } from '../../../libtextsecure/libsignal-protocol'; import { PubKey } from '../types'; import { toHex } from './String'; -export async function isUs( - pubKey: string | PubKey | undefined -): Promise { +export type HexKeyPair = { + pubKey: string; + privKey: string; +}; + +/** + * Check if this pubkey is us, using the cache. + */ +export function isUsFromCache(pubKey: string | PubKey | undefined): boolean { if (!pubKey) { throw new Error('pubKey is not set'); } - const ourNumber = await UserUtils.getCurrentDevicePubKey(); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); if (!ourNumber) { throw new Error('ourNumber is not set'); } @@ -19,20 +25,22 @@ export async function isUs( return pubKeyStr === ourNumber; } -export type HexKeyPair = { - pubKey: string; - privKey: string; -}; - /** - * Returns the public key of this current device as a string + * Returns the public key of this current device as a STRING, or throws an error */ -export async function getCurrentDevicePubKey(): Promise { - return window.textsecure.storage.user.getNumber(); +export function getOurPubKeyStrFromCache(): string { + const ourNumber = window.textsecure.storage.user.getNumber(); + if (!ourNumber) { + throw new Error('ourNumber is not set'); + } + return ourNumber; } -export async function getOurNumber(): Promise { - const ourNumber = await UserUtils.getCurrentDevicePubKey(); +/** + * Returns the public key of this current device as a PubKey, or throws an error + */ +export function getOurPubKeyFromCache(): PubKey { + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); if (!ourNumber) { throw new Error('ourNumber is not set'); } diff --git a/ts/shims/Whisper.ts b/ts/shims/Whisper.ts deleted file mode 100644 index 1280f6c004..0000000000 --- a/ts/shims/Whisper.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function getMessageModel(attributes: any) { - // @ts-ignore - return new window.Whisper.Message(attributes); -} diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index cfc98b3dab..f8c2440db7 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -2,8 +2,8 @@ import _, { omit } from 'lodash'; import { Constants } from '../../session'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { MessageModel } from '../../../js/models/messages'; import { ConversationController } from '../../session/conversations'; +import { MessageCollection, MessageModel } from '../../models/message'; // State @@ -48,9 +48,13 @@ export type MessageTypeInConvo = { getPropsForMessageDetail(): Promise; }; -export type ConversationType = { +export interface ConversationType { id: string; name?: string; + profileName?: string; + hasNickname?: boolean; + index?: number; + activeAt?: number; timestamp: number; lastMessage?: { @@ -61,10 +65,10 @@ export type ConversationType = { type: 'direct' | 'group'; isMe: boolean; isPublic?: boolean; - lastUpdated: number; unreadCount: number; mentionedUs: boolean; isSelected: boolean; + isTyping: boolean; isBlocked: boolean; isKickedFromGroup: boolean; @@ -72,7 +76,8 @@ export type ConversationType = { avatarPath?: string; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups members?: Array; // members for closed groups only -}; +} + export type ConversationLookupType = { [key: string]: ConversationType; }; @@ -95,7 +100,7 @@ async function getMessages( window.log.error('Failed to get convo on reducer.'); return []; } - const unreadCount = await conversation.getUnreadCount(); + const unreadCount = (await conversation.getUnreadCount()) as number; let msgCount = numMessages || Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) + unreadCount; @@ -110,7 +115,7 @@ async function getMessages( const messageSet = await window.Signal.Data.getMessagesByConversation( conversationKey, - { limit: msgCount, MessageCollection: window.Whisper.MessageCollection } + { limit: msgCount, MessageCollection } ); // Set first member of series here. @@ -142,7 +147,11 @@ const updateFirstMessageOfSeries = (messageModels: Array) => { if (i >= 0 && currentSender === nextSender) { firstMessageOfSeries = false; } - messageModels[i].firstMessageOfSeries = firstMessageOfSeries; + if (messageModels[i].propsForMessage) { + messageModels[ + i + ].propsForMessage.firstMessageOfSeries = firstMessageOfSeries; + } } return messageModels; }; diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 7e1224faa4..1c5940c23f 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -1,7 +1,6 @@ import { omit, reject } from 'lodash'; import { AdvancedSearchOptions, SearchOptions } from '../../types/Search'; -import { getMessageModel } from '../../shims/Whisper'; import { cleanSearchTerm } from '../../util/cleanSearchTerm'; import { searchConversations, searchMessages } from '../../../js/modules/data'; import { makeLookup } from '../../util/makeLookup'; @@ -14,6 +13,8 @@ import { SelectedConversationChangedActionType, } from './conversations'; import { PubKey } from '../../session/types'; +import { MessageModel } from '../../models/message'; +import { MessageModelType } from '../../models/messageType'; // State @@ -231,7 +232,12 @@ const getMessageProps = (messages: Array) => { } return messages.map(message => { - const model = getMessageModel(message); + const overridenProps = { + ...message, + type: 'incoming' as MessageModelType, + }; + + const model = new MessageModel(overridenProps); return model.propsForSearchResult; }); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index a31294f479..20f5d85da3 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -102,6 +102,8 @@ export const _getLeftPaneLists = ( const conversations: Array = []; const allContacts: Array = []; + let index = 0; + let unreadCount = 0; for (let conversation of sorted) { if (selectedConversation === conversation.id) { @@ -121,6 +123,8 @@ export const _getLeftPaneLists = ( }; } + conversation.index = index; + // Add Open Group to list as soon as the name has been set if ( conversation.isPublic && @@ -152,6 +156,7 @@ export const _getLeftPaneLists = ( } conversations.push(conversation); + index++; } return { diff --git a/ts/test/session/unit/crypto/MessageEncrypter_test.ts b/ts/test/session/unit/crypto/MessageEncrypter_test.ts index 4be830a2e3..c05dfb4b0b 100644 --- a/ts/test/session/unit/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/unit/crypto/MessageEncrypter_test.ts @@ -117,7 +117,7 @@ describe('MessageEncrypter', () => { } as any, }); - sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber); + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); sandbox .stub(UserUtils, 'getUserED25519KeyPair') .resolves(ourUserEd25516Keypair); diff --git a/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts b/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts new file mode 100644 index 0000000000..dcb3024023 --- /dev/null +++ b/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts @@ -0,0 +1,60 @@ +import chai from 'chai'; +import * as sinon from 'sinon'; +import _ from 'lodash'; +import { describe } from 'mocha'; + +import { GroupUtils, PromiseUtils, UserUtils } from '../../../../session/utils'; +import { TestUtils } from '../../../../test/test-utils'; +import { + generateEnvelopePlusClosedGroup, + generateGroupUpdateNameChange, +} from '../../../test-utils/utils/envelope'; +import { handleClosedGroupControlMessage } from '../../../../receiver/closedGroups'; +import { ConversationController } from '../../../../session/conversations'; + +// tslint:disable-next-line: no-require-imports no-var-requires no-implicit-dependencies +const chaiAsPromised = require('chai-as-promised'); +chai.use(chaiAsPromised); + +const { expect } = chai; + +// tslint:disable-next-line: max-func-body-length +describe('ClosedGroupUpdates', () => { + // Initialize new stubbed cache + const sandbox = sinon.createSandbox(); + const ourDevice = TestUtils.generateFakePubKey(); + const ourNumber = ourDevice.key; + const groupId = TestUtils.generateFakePubKey().key; + const members = TestUtils.generateFakePubKeys(10); + const sender = members[3].key; + const getConvo = sandbox.stub(ConversationController.getInstance(), 'get'); + + beforeEach(async () => { + // Utils Stubs + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); + }); + + afterEach(() => { + TestUtils.restoreStubs(); + sandbox.restore(); + }); + + describe('handleClosedGroupControlMessage', () => { + describe('performIfValid', () => { + it('does not perform if convo does not exist', async () => { + const envelope = generateEnvelopePlusClosedGroup(groupId, sender); + const groupUpdate = generateGroupUpdateNameChange(groupId); + getConvo.returns(undefined as any); + await handleClosedGroupControlMessage(envelope, groupUpdate); + }); + }); + + // describe('handleClosedGroupNameChanged', () => { + // it('does not trigger an update of the group if the name is the same', async () => { + // const envelope = generateEnvelopePlusClosedGroup(groupId, sender); + // const groupUpdate = generateGroupUpdateNameChange(groupId); + // await handleClosedGroupControlMessage(envelope, groupUpdate); + // }); + // }); + }); +}); diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index c539740562..a7b5e4a85e 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -37,7 +37,7 @@ describe('MessageQueue', () => { beforeEach(async () => { // Utils Stubs - sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber); + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); TestUtils.stubWindow('libsignal', { SignalProtocolAddress: sandbox.stub(), diff --git a/ts/test/session/unit/sending/MessageSender_test.ts b/ts/test/session/unit/sending/MessageSender_test.ts index 19d1d358e6..7d5accef8a 100644 --- a/ts/test/session/unit/sending/MessageSender_test.ts +++ b/ts/test/session/unit/sending/MessageSender_test.ts @@ -59,7 +59,7 @@ describe('MessageSender', () => { cipherText: crypto.randomBytes(10), }); - sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber); + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); }); describe('retry', () => { diff --git a/ts/test/session/unit/utils/Messages_test.ts b/ts/test/session/unit/utils/Messages_test.ts index 62977f789d..68dfd00036 100644 --- a/ts/test/session/unit/utils/Messages_test.ts +++ b/ts/test/session/unit/utils/Messages_test.ts @@ -7,9 +7,13 @@ import { ClosedGroupChatMessage } from '../../../../session/messages/outgoing/co import { ClosedGroupEncryptionPairMessage, ClosedGroupNewMessage, - ClosedGroupUpdateMessage, } from '../../../../session/messages/outgoing'; import { SignalService } from '../../../../protobuf'; +import { + ClosedGroupAddedMembersMessage, + ClosedGroupNameChangeMessage, + ClosedGroupRemovedMembersMessage, +} from '../../../../session/messages/outgoing/content/data/group'; // tslint:disable-next-line: no-require-imports no-var-requires const chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised); @@ -117,13 +121,38 @@ describe('Message Utils', () => { expect(rawMessage.encryption).to.equal(EncryptionType.Fallback); }); - it('passing ClosedGroupUpdateMessage returns ClosedGroup', async () => { + it('passing ClosedGroupNameChangeMessage returns ClosedGroup', async () => { const device = TestUtils.generateFakePubKey(); - const msg = new ClosedGroupUpdateMessage({ + const msg = new ClosedGroupNameChangeMessage({ timestamp: Date.now(), name: 'df', - members: [TestUtils.generateFakePubKey().key], + groupId: TestUtils.generateFakePubKey().key, + expireTimer: 0, + }); + const rawMessage = await MessageUtils.toRawMessage(device, msg); + expect(rawMessage.encryption).to.equal(EncryptionType.ClosedGroup); + }); + + it('passing ClosedGroupAddedMembersMessage returns ClosedGroup', async () => { + const device = TestUtils.generateFakePubKey(); + + const msg = new ClosedGroupAddedMembersMessage({ + timestamp: Date.now(), + addedMembers: [TestUtils.generateFakePubKey().key], + groupId: TestUtils.generateFakePubKey().key, + expireTimer: 0, + }); + const rawMessage = await MessageUtils.toRawMessage(device, msg); + expect(rawMessage.encryption).to.equal(EncryptionType.ClosedGroup); + }); + + it('passing ClosedGroupRemovedMembersMessage returns ClosedGroup', async () => { + const device = TestUtils.generateFakePubKey(); + + const msg = new ClosedGroupRemovedMembersMessage({ + timestamp: Date.now(), + removedMembers: [TestUtils.generateFakePubKey().key], groupId: TestUtils.generateFakePubKey().key, expireTimer: 0, }); diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts index 9fdd66bb49..35fefeb07b 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test/state/selectors/conversations_test.ts @@ -20,7 +20,6 @@ describe('state/selectors/conversations', () => { type: 'direct', isMe: false, - lastUpdated: Date.now(), unreadCount: 1, mentionedUs: false, isSelected: false, @@ -38,7 +37,6 @@ describe('state/selectors/conversations', () => { type: 'direct', isMe: false, - lastUpdated: Date.now(), unreadCount: 1, mentionedUs: false, isSelected: false, @@ -56,7 +54,6 @@ describe('state/selectors/conversations', () => { type: 'direct', isMe: false, - lastUpdated: Date.now(), unreadCount: 1, mentionedUs: false, isSelected: false, @@ -73,7 +70,6 @@ describe('state/selectors/conversations', () => { phoneNumber: 'notused', type: 'direct', isMe: false, - lastUpdated: Date.now(), unreadCount: 1, mentionedUs: false, isSelected: false, @@ -90,7 +86,6 @@ describe('state/selectors/conversations', () => { phoneNumber: 'notused', type: 'direct', isMe: false, - lastUpdated: Date.now(), unreadCount: 1, mentionedUs: false, isSelected: false, diff --git a/ts/test/test-utils/utils/envelope.ts b/ts/test/test-utils/utils/envelope.ts new file mode 100644 index 0000000000..32690304f1 --- /dev/null +++ b/ts/test/test-utils/utils/envelope.ts @@ -0,0 +1,39 @@ +import { EnvelopePlus } from '../../../receiver/types'; +import { SignalService } from '../../../protobuf'; + +import uuid from 'uuid'; +import { fromHexToArray } from '../../../session/utils/String'; + +export function generateEnvelopePlusClosedGroup( + groupId: string, + sender: string +): EnvelopePlus { + const envelope: EnvelopePlus = { + senderIdentity: sender, + receivedAt: Date.now(), + timestamp: Date.now() - 2000, + id: uuid(), + type: SignalService.Envelope.Type.CLOSED_GROUP_CIPHERTEXT, + source: groupId, + content: new Uint8Array(), + toJSON: () => ['fake'], + }; + + return envelope; +} + +export function generateGroupUpdateNameChange( + groupId: string +): SignalService.DataMessage.ClosedGroupControlMessage { + const update: SignalService.DataMessage.ClosedGroupControlMessage = { + type: SignalService.DataMessage.ClosedGroupControlMessage.Type.NAME_CHANGE, + toJSON: () => ['fake'], + publicKey: fromHexToArray(groupId), + name: 'fakeNewName', + members: [], + admins: [], + wrappers: [], + }; + + return update; +} diff --git a/ts/test/test-utils/utils/index.ts b/ts/test/test-utils/utils/index.ts index 7cfc3adc30..aeea183af7 100644 --- a/ts/test/test-utils/utils/index.ts +++ b/ts/test/test-utils/utils/index.ts @@ -2,3 +2,4 @@ export * from './timeout'; export * from './stubbing'; export * from './pubkey'; export * from './message'; +export * from './envelope'; diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index 5fe96c9879..84be09db10 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -1,12 +1,12 @@ import { ChatMessage, - ClosedGroupChatMessage, OpenGroupMessage, } from '../../../session/messages/outgoing'; import { v4 as uuid } from 'uuid'; import { OpenGroup } from '../../../session/types'; import { generateFakePubKey, generateFakePubKeys } from './pubkey'; -import { ConversationAttributes } from '../../../../js/models/conversations'; +import { ClosedGroupChatMessage } from '../../../session/messages/outgoing/content/data/group'; +import { ConversationAttributes } from '../../../models/conversation'; export function generateChatMessage(identifier?: string): ChatMessage { return new ChatMessage({ @@ -89,8 +89,8 @@ export class MockConversation { mentionedUs: false, unreadCount: 99, active_at: Date.now(), - timestamp: Date.now(), lastJoinedTimestamp: Date.now(), + lastMessageStatus: null, }; } diff --git a/ts/test/util/blockedNumberController_test.ts b/ts/test/util/blockedNumberController_test.ts index b9901882c4..83a7302bab 100644 --- a/ts/test/util/blockedNumberController_test.ts +++ b/ts/test/util/blockedNumberController_test.ts @@ -166,7 +166,9 @@ describe('BlockedNumberController', () => { let ourDevice: PubKey; beforeEach(() => { ourDevice = TestUtils.generateFakePubKey(); - sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourDevice.key); + sandbox + .stub(UserUtils, 'getOurPubKeyStrFromCache') + .returns(ourDevice.key); }); it('should return false for our device', async () => { const isBlocked = await BlockedNumberController.isBlockedAsync(ourDevice); diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 91d495c7df..89d49492fb 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -57,21 +57,8 @@ type MessageSchemaVersion6 = Partial< }> >; -export const isUserMessage = (message: Message): message is UserMessage => - message.type === 'incoming'; - -export const hasExpiration = (message: Message): boolean => { - if (!isUserMessage(message)) { - return false; - } - - const { expireTimer } = message; - - return typeof expireTimer === 'number' && expireTimer > 0; -}; - export type LokiProfile = { displayName: string; avatarPointer: string; - profileKey: Uint8Array; + profileKey: Uint8Array | null; }; diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index a192070f19..97001cd1f7 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -18,7 +18,7 @@ export class BlockedNumberController { */ public static async isBlockedAsync(user: string | PubKey): Promise { await this.load(); - const isOurDevice = await UserUtils.isUs(user); + const isOurDevice = UserUtils.isUsFromCache(user); if (isOurDevice) { return false; } diff --git a/ts/util/findMember.ts b/ts/util/findMember.ts index 49517760d2..d95a4db6fe 100644 --- a/ts/util/findMember.ts +++ b/ts/util/findMember.ts @@ -1,5 +1,5 @@ -import { ConversationModel } from '../../js/models/conversations'; -import { ConversationController } from '../session/conversations/ConversationController'; +import { ConversationModel } from '../models/conversation'; +import { ConversationController } from '../session/conversations'; // tslint:disable: no-unnecessary-class export class FindMember { diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index 48f5e31b74..a1157be416 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -50,7 +50,6 @@ const results: Array = []; const excludedFiles = [ // High-traffic files in our project - '^js/models/messages.js', '^js/background.js', // Generated files diff --git a/ts/window.d.ts b/ts/window.d.ts index 883755c25b..e6adce865f 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -20,6 +20,8 @@ import { Store } from 'redux'; import { MessageController } from './session/messages/MessageController'; import { DefaultTheme } from 'styled-components'; +import { ConversationCollection } from './models/conversation'; + /* We declare window stuff here instead of global.d.ts because we are importing other declarations. If you import anything in global.d.ts, the type system won't work correctly. @@ -90,7 +92,7 @@ declare global { tokenlessFileServerAdnAPI: LokiAppDotNetServerInterface; userConfig: any; versionInfo: any; - getStoragePubKey: any; + getStoragePubKey: (key: string) => string; getConversations: () => ConversationCollection; getGuid: any; SwarmPolling: SwarmPolling; @@ -116,5 +118,7 @@ declare global { openUrl: (string) => void; lightTheme: DefaultTheme; darkTheme: DefaultTheme; + LokiPushNotificationServer: any; + LokiPushNotificationServerApi: any; } } From 0fe026ab152cd472c7ce685364b38cc2d9a05414 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 22 Jan 2021 16:29:02 +1100 Subject: [PATCH 002/109] Fix the password length limit when not setting a new password Relates #1446 --- _locales/en/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/ru/messages.json | 2 +- password_preload.js | 1 - preload.js | 16 +- ts/components/Lightbox.tsx | 5 +- ts/components/session/RegistrationTabs.tsx | 5 +- .../session/SessionPasswordModal.tsx | 223 +++++++++--------- .../session/SessionPasswordPrompt.tsx | 21 -- ts/components/session/SessionSeedModal.tsx | 7 +- .../session/settings/SessionSettings.tsx | 7 +- .../test/session/unit/utils/Password.ts | 37 ++- ts/util/passwordUtils.ts | 9 +- ts/window.d.ts | 1 - 14 files changed, 158 insertions(+), 180 deletions(-) rename test/app/password_util_test.js => ts/test/session/unit/utils/Password.ts (67%) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f82c6c8eb9..d008c88737 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1915,7 +1915,7 @@ "message": "Failed to set password" }, "passwordLengthError": { - "message": "Password must be between 6 and 50 characters long", + "message": "Password must be between 6 and 64 characters long", "description": "Error string shown to the user when password doesn't meet length criteria" }, "passwordTypeError": { diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index e507f73520..1b68565bec 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1698,7 +1698,7 @@ "message": "Échec de la définition du mot de passe" }, "passwordLengthError": { - "message": "Le mot de passe doit avoir une longueur comprise entre 6 et 50 caractères", + "message": "Le mot de passe doit avoir une longueur comprise entre 6 et 64 caractères", "description": "Error string shown to the user when password doesn't meet length criteria" }, "passwordTypeError": { diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 9870f1a0d4..06233c3956 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1698,7 +1698,7 @@ "message": "Failed to set password" }, "passwordLengthError": { - "message": "Password must be between 6 and 50 characters long", + "message": "Password must be between 6 and 64 characters long", "description": "Error string shown to the user when password doesn't meet length criteria" }, "passwordTypeError": { diff --git a/password_preload.js b/password_preload.js index d5c29af89d..2e4f0693e9 100644 --- a/password_preload.js +++ b/password_preload.js @@ -40,7 +40,6 @@ window.CONSTANTS = { MAX_USERNAME_LENGTH: 20, }; -window.passwordUtil = require('./ts/util/passwordUtils'); window.Signal.Logs = require('./js/modules/logs'); window.resetDatabase = () => { diff --git a/preload.js b/preload.js index 19f3d2a250..3108dc2794 100644 --- a/preload.js +++ b/preload.js @@ -87,7 +87,7 @@ window.isBeforeVersion = (toCheck, baseVersion) => { }; // eslint-disable-next-line func-names -window.CONSTANTS = new (function() { +window.CONSTANTS = new (function () { this.MAX_LOGIN_TRIES = 3; this.MAX_PASSWORD_LENGTH = 64; this.MAX_USERNAME_LENGTH = 20; @@ -183,7 +183,13 @@ window.setPassword = (passPhrase, oldPhrase) => ipc.send('set-password', passPhrase, oldPhrase); }); -window.passwordUtil = require('./ts/util/passwordUtils'); +window.libsession = require('./ts/session'); + +window.getMessageController = + window.libsession.Messages.MessageController.getInstance; + +window.getConversationController = + window.libsession.Conversations.ConversationController.getInstance; // We never do these in our code, so we'll prevent it everywhere window.open = () => null; @@ -392,7 +398,7 @@ window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args); // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(() => { - window.nodeSetImmediate(() => {}); + window.nodeSetImmediate(() => { }); }, 1000); const { autoOrientImage } = require('./js/modules/auto_orient_image'); @@ -455,9 +461,9 @@ if (process.env.USE_STUBBED_NETWORK) { } // eslint-disable-next-line no-extend-native,func-names -Promise.prototype.ignore = function() { +Promise.prototype.ignore = function () { // eslint-disable-next-line more/no-then - this.then(() => {}); + this.then(() => { }); }; if ( diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 2a7b32d21b..790ba22952 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -237,8 +237,7 @@ export class Lightbox extends React.Component { } if (current.paused) { - // tslint:disable-next-line no-floating-promises - current.play(); + void current.play(); } else { current.pause(); } @@ -272,7 +271,7 @@ export class Lightbox extends React.Component {
- + { error={this.state.passwordErrorString} type="password" placeholder={window.i18n('enterOptionalPassword')} - maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} onValueChanged={(val: string) => { this.onPasswordChanged(val); }} @@ -470,7 +470,6 @@ export class RegistrationTabs extends React.Component { error={passwordsDoNotMatch} type="password" placeholder={window.i18n('confirmPassword')} - maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} onValueChanged={(val: string) => { this.onPasswordVerifyChanged(val); }} @@ -592,7 +591,7 @@ export class RegistrationTabs extends React.Component { return; } - const error = window.passwordUtil.validatePassword(input, window.i18n); + const error = PasswordUtil.validatePassword(input, window.i18n); if (error) { this.setState({ passwordErrorString: error, diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index b319195129..a6f5a4ace6 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SessionModal } from './SessionModal'; import { SessionButton, SessionButtonColor } from './SessionButton'; -import { PasswordUtil } from '../../util/'; +import { missingCaseError, PasswordUtil } from '../../util/'; import { ToastUtils } from '../../session/utils'; import { toast } from 'react-toastify'; import { SessionToast, SessionToastType } from './SessionToast'; @@ -46,8 +46,6 @@ class SessionPasswordModalInner extends React.Component { this.onPasswordInput = this.onPasswordInput.bind(this); this.onPasswordConfirmInput = this.onPasswordConfirmInput.bind(this); - - this.onPaste = this.onPaste.bind(this); } public componentDidMount() { @@ -86,8 +84,6 @@ class SessionPasswordModalInner extends React.Component { }} placeholder={placeholders[0]} onKeyUp={this.onPasswordInput} - maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} - onPaste={this.onPaste} /> {action !== PasswordAction.Remove && ( { id="password-modal-input-confirm" placeholder={placeholders[1]} onKeyUp={this.onPasswordConfirmInput} - maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} - onPaste={this.onPaste} /> )}
@@ -108,7 +102,7 @@ class SessionPasswordModalInner extends React.Component { this.setPassword(onOk)} + onClick={this.setPassword} /> { ); } - // tslint:disable-next-line: cyclomatic-complexity - private async setPassword(onSuccess?: any) { - const { action } = this.props; - const { - currentPasswordEntered, - currentPasswordConfirmEntered, - } = this.state; - const { Set, Remove, Change } = PasswordAction; - - // Trim leading / trailing whitespace for UX - const enteredPassword = (currentPasswordEntered || '').trim(); - const enteredPasswordConfirm = (currentPasswordConfirmEntered || '').trim(); - + /** + * Returns false and set the state error field in the input is not a valid password + * or returns true + */ + private validatePassword(firstPassword: string) { // if user did not fill the first password field, we can't do anything const errorFirstInput = PasswordUtil.validatePassword( - enteredPassword, + firstPassword, window.i18n ); if (errorFirstInput !== null) { this.setState({ error: errorFirstInput, }); + return false; + } + return true; + } + + private async handleActionSet( + enteredPassword: string, + enteredPasswordConfirm: string + ) { + // be sure both password are valid + if (!this.validatePassword(enteredPassword)) { return; } + // no need to validate second password. we just need to check that enteredPassword is valid, and that both password matches - // if action is Set or Change, we need a valid ConfirmPassword - if (action === Set || action === Change) { - const errorSecondInput = PasswordUtil.validatePassword( - enteredPasswordConfirm, - window.i18n - ); - if (errorSecondInput !== null) { - this.setState({ - error: errorSecondInput, - }); - return; - } + if (enteredPassword !== enteredPasswordConfirm) { + this.setState({ + error: window.i18n('setPasswordInvalid'), + }); + return; } + await window.setPassword(enteredPassword, null); + ToastUtils.pushToastSuccess( + 'setPasswordSuccessToast', + window.i18n('setPasswordTitle'), + window.i18n('setPasswordToastDescription'), + SessionIconType.Lock + ); - // Passwords match or remove password successful - const newPassword = action === Remove ? null : enteredPasswordConfirm; - const oldPassword = action === Set ? null : enteredPassword; + this.props.onOk(this.props.action); + this.closeDialog(); + } - // Check if password match, when setting, changing or removing - let valid; - if (action === Set) { - valid = enteredPassword === enteredPasswordConfirm; - } else { - valid = Boolean(await this.validatePasswordHash(oldPassword)); + private async handleActionChange(oldPassword: string, newPassword: string) { + // We don't validate oldPassword on change: this is validate on the validatePasswordHash below + // we only validate the newPassword here + if (!this.validatePassword(newPassword)) { + return; } - - if (!valid) { - let str; - switch (action) { - case Set: - str = window.i18n('setPasswordInvalid'); - break; - case Change: - str = window.i18n('changePasswordInvalid'); - break; - case Remove: - str = window.i18n('removePasswordInvalid'); - break; - default: - throw new Error(`Invalid action ${action}`); - } + const isValidWithStoredInDB = Boolean( + await this.validatePasswordHash(oldPassword) + ); + if (!isValidWithStoredInDB) { this.setState({ - error: str, + error: window.i18n('changePasswordInvalid'), }); - return; } - await window.setPassword(newPassword, oldPassword); - let title; - let description; - switch (action) { - case Set: - title = window.i18n('setPasswordTitle'); - description = window.i18n('setPasswordToastDescription'); - break; - case Change: - title = window.i18n('changePasswordTitle'); - description = window.i18n('changePasswordToastDescription'); - break; - case Remove: - title = window.i18n('removePasswordTitle'); - description = window.i18n('removePasswordToastDescription'); - break; - default: - throw new Error(`Invalid action ${action}`); - } - if (action !== Remove) { - ToastUtils.pushToastSuccess( - 'setPasswordSuccessToast', - title, - description, - SessionIconType.Lock - ); - } else { - ToastUtils.pushToastWarning( - 'setPasswordSuccessToast', - title, - description - ); - } + ToastUtils.pushToastSuccess( + 'setPasswordSuccessToast', + window.i18n('changePasswordTitle'), + window.i18n('changePasswordToastDescription'), + SessionIconType.Lock + ); - onSuccess(this.props.action); + this.props.onOk(this.props.action); this.closeDialog(); } - private closeDialog() { - if (this.props.onClose) { - this.props.onClose(); + private async handleActionRemove(oldPassword: string) { + // We don't validate oldPassword on change: this is validate on the validatePasswordHash below + const isValidWithStoredInDB = Boolean( + await this.validatePasswordHash(oldPassword) + ); + if (!isValidWithStoredInDB) { + this.setState({ + error: window.i18n('removePasswordInvalid'), + }); + return; } + await window.setPassword(null, oldPassword); + + ToastUtils.pushToastWarning( + 'setPasswordSuccessToast', + window.i18n('removePasswordTitle'), + window.i18n('removePasswordToastDescription') + ); + + this.props.onOk(this.props.action); + this.closeDialog(); } - private onPaste(event: any) { - const clipboard = event.clipboardData.getData('text'); - - if (clipboard.length > window.CONSTANTS.MAX_PASSWORD_LENGTH) { - const title = String( - window.i18n( - 'pasteLongPasswordToastTitle', - window.CONSTANTS.MAX_PASSWORD_LENGTH - ) - ); - ToastUtils.pushToastWarning('passwordModal', title); + // tslint:disable-next-line: cyclomatic-complexity + private async setPassword() { + const { action } = this.props; + const { + currentPasswordEntered, + currentPasswordConfirmEntered, + } = this.state; + const { Set, Remove, Change } = PasswordAction; + + // Trim leading / trailing whitespace for UX + const firstPasswordEntered = (currentPasswordEntered || '').trim(); + const secondPasswordEntered = (currentPasswordConfirmEntered || '').trim(); + + switch (action) { + case Set: { + await this.handleActionSet(firstPasswordEntered, secondPasswordEntered); + return; + } + case Change: { + await this.handleActionChange( + firstPasswordEntered, + secondPasswordEntered + ); + return; + } + case Remove: { + await this.handleActionRemove(firstPasswordEntered); + return; + } + default: + throw missingCaseError(action); } + } - // Prevent pating into input - return false; + private closeDialog() { + if (this.props.onClose) { + this.props.onClose(); + } } private async onPasswordInput(event: any) { if (event.key === 'Enter') { - return this.setPassword(this.props.onOk); + return this.setPassword(); } const currentPasswordEntered = event.target.value; @@ -291,7 +288,7 @@ class SessionPasswordModalInner extends React.Component { private async onPasswordConfirmInput(event: any) { if (event.key === 'Enter') { - return this.setPassword(this.props.onOk); + return this.setPassword(); } const currentPasswordConfirmEntered = event.target.value; diff --git a/ts/components/session/SessionPasswordPrompt.tsx b/ts/components/session/SessionPasswordPrompt.tsx index fa6f4d0619..07b30ff00c 100644 --- a/ts/components/session/SessionPasswordPrompt.tsx +++ b/ts/components/session/SessionPasswordPrompt.tsx @@ -32,7 +32,6 @@ class SessionPasswordPromptInner extends React.PureComponent< }; this.onKeyUp = this.onKeyUp.bind(this); - this.onPaste = this.onPaste.bind(this); this.initLogin = this.initLogin.bind(this); this.initClearDataView = this.initClearDataView.bind(this); @@ -72,8 +71,6 @@ class SessionPasswordPromptInner extends React.PureComponent< defaultValue="" placeholder={' '} onKeyUp={this.onKeyUp} - maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} - onPaste={this.onPaste} ref={this.inputRef} /> ); @@ -135,24 +132,6 @@ class SessionPasswordPromptInner extends React.PureComponent< event.preventDefault(); } - public onPaste(event: any) { - const clipboard = event.clipboardData.getData('text'); - - if (clipboard.length > window.CONSTANTS.MAX_PASSWORD_LENGTH) { - this.setState({ - error: String( - window.i18n( - 'pasteLongPasswordToastTitle', - window.CONSTANTS.MAX_PASSWORD_LENGTH - ) - ), - }); - } - - // Prevent pasting into input - return false; - } - public async onLogin(passPhrase: string) { const passPhraseTrimmed = passPhrase.trim(); diff --git a/ts/components/session/SessionSeedModal.tsx b/ts/components/session/SessionSeedModal.tsx index 207dac9663..3b9f30b04d 100644 --- a/ts/components/session/SessionSeedModal.tsx +++ b/ts/components/session/SessionSeedModal.tsx @@ -4,6 +4,7 @@ import { SessionModal } from './SessionModal'; import { SessionButton } from './SessionButton'; import { ToastUtils } from '../../session/utils'; import { DefaultTheme, withTheme } from 'styled-components'; +import { PasswordUtil } from '../../util'; interface Props { onClose: any; @@ -77,7 +78,6 @@ class SessionSeedModalInner extends React.Component { } private renderPasswordView() { - const maxPasswordLen = 64; const error = this.state.error; const i18n = window.i18n; const { onClose } = this.props; @@ -90,7 +90,6 @@ class SessionSeedModalInner extends React.Component { id="seed-input-password" placeholder={i18n('password')} onKeyUp={this.onEnter} - maxLength={maxPasswordLen} /> {error && ( @@ -143,8 +142,8 @@ class SessionSeedModalInner extends React.Component { private confirmPassword() { const passwordHash = this.state.passwordHash; const passwordValue = jQuery('#seed-input-password').val(); - const isPasswordValid = window.passwordUtil.matchesHash( - passwordValue, + const isPasswordValid = PasswordUtil.matchesHash( + passwordValue as string, passwordHash ); diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 0ea3e995c9..63c5b6a282 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -7,7 +7,7 @@ import { SessionButtonColor, SessionButtonType, } from '../SessionButton'; -import { BlockedNumberController } from '../../../util'; +import { BlockedNumberController, PasswordUtil } from '../../../util'; import { ToastUtils } from '../../../session/utils'; import { ConversationLookupType } from '../../../state/ducks/conversations'; import { StateType } from '../../../state/reducer'; @@ -172,8 +172,7 @@ class SettingsViewInner extends React.Component { type="password" id="password-lock-input" defaultValue="" - placeholder={' '} - maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} + placeholder="Password" />
@@ -211,7 +210,7 @@ class SettingsViewInner extends React.Component { // Check if the password matches the hash we have stored const hash = await window.Signal.Data.getPasswordHash(); - if (hash && !window.passwordUtil.matchesHash(enteredPassword, hash)) { + if (hash && !PasswordUtil.matchesHash(enteredPassword, hash)) { this.setState({ pwdLockError: window.i18n('invalidPassword'), }); diff --git a/test/app/password_util_test.js b/ts/test/session/unit/utils/Password.ts similarity index 67% rename from test/app/password_util_test.js rename to ts/test/session/unit/utils/Password.ts index 7037ec32eb..f52d78e2f4 100644 --- a/test/app/password_util_test.js +++ b/ts/test/session/unit/utils/Password.ts @@ -1,17 +1,16 @@ -const { assert } = require('chai'); - -const passwordUtil = require('../../ts/util/passwordUtils'); +import { assert } from 'chai'; +import { PasswordUtil } from '../../../../util'; describe('Password Util', () => { describe('hash generation', () => { it('generates the same hash for the same phrase', () => { - const first = passwordUtil.generateHash('phrase'); - const second = passwordUtil.generateHash('phrase'); + const first = PasswordUtil.generateHash('phrase'); + const second = PasswordUtil.generateHash('phrase'); assert.strictEqual(first, second); }); it('generates different hashes for different phrases', () => { - const first = passwordUtil.generateHash('0'); - const second = passwordUtil.generateHash('1'); + const first = PasswordUtil.generateHash('0'); + const second = PasswordUtil.generateHash('1'); assert.notStrictEqual(first, second); }); }); @@ -19,12 +18,12 @@ describe('Password Util', () => { describe('hash matching', () => { it('returns true for the same hash', () => { const phrase = 'phrase'; - const hash = passwordUtil.generateHash(phrase); - assert.isTrue(passwordUtil.matchesHash(phrase, hash)); + const hash = PasswordUtil.generateHash(phrase); + assert.isTrue(PasswordUtil.matchesHash(phrase, hash)); }); it('returns false for different hashes', () => { - const hash = passwordUtil.generateHash('phrase'); - assert.isFalse(passwordUtil.matchesHash('phrase2', hash)); + const hash = PasswordUtil.generateHash('phrase'); + assert.isFalse(PasswordUtil.matchesHash('phrase2', hash)); }); }); @@ -44,26 +43,26 @@ describe('Password Util', () => { '#'.repeat(50), ]; valid.forEach(pass => { - assert.isNull(passwordUtil.validatePassword(pass)); + assert.isNull(PasswordUtil.validatePassword(pass)); }); }); it('should return an error if password is not a string', () => { - const invalid = [0, 123456, [], {}, null, undefined]; - invalid.forEach(pass => { + const invalid = [0, 123456, [], {}, null, undefined] as any; + invalid.forEach((pass: any) => { assert.strictEqual( - passwordUtil.validatePassword(pass), + PasswordUtil.validatePassword(pass), 'Password must be a string' ); }); }); - it('should return an error if password is not between 6 and 50 characters', () => { + it('should return an error if password is not between 6 and 64 characters', () => { const invalid = ['a', 'abcde', '#'.repeat(51), '#'.repeat(100)]; invalid.forEach(pass => { assert.strictEqual( - passwordUtil.validatePassword(pass), - 'Password must be between 6 and 50 characters long' + PasswordUtil.validatePassword(pass), + 'Password must be between 6 and 64 characters long' ); }); }); @@ -82,7 +81,7 @@ describe('Password Util', () => { ]; invalid.forEach(pass => { assert.strictEqual( - passwordUtil.validatePassword(pass), + PasswordUtil.validatePassword(pass), 'Password must only contain letters, numbers and symbols' ); }); diff --git a/ts/util/passwordUtils.ts b/ts/util/passwordUtils.ts index ea2ca0d24f..48ba154a33 100644 --- a/ts/util/passwordUtils.ts +++ b/ts/util/passwordUtils.ts @@ -3,7 +3,7 @@ import { LocalizerType } from '../types/Util'; const ERRORS = { TYPE: 'Password must be a string', - LENGTH: 'Password must be between 6 and 50 characters long', + LENGTH: 'Password must be between 6 and 64 characters long', CHARACTER: 'Password must only contain letters, numbers and symbols', }; @@ -17,7 +17,7 @@ export const generateHash = (phrase: string) => phrase && sha512(phrase.trim()); export const matchesHash = (phrase: string | null, hash: string) => phrase && sha512(phrase.trim()) === hash.trim(); -export const validatePassword = (phrase: string, i18n: LocalizerType) => { +export const validatePassword = (phrase: string, i18n?: LocalizerType) => { if (typeof phrase !== 'string') { return i18n ? i18n('passwordTypeError') : ERRORS.TYPE; } @@ -27,7 +27,10 @@ export const validatePassword = (phrase: string, i18n: LocalizerType) => { return i18n ? i18n('noGivenPassword') : ERRORS.LENGTH; } - if (trimmed.length < 6 || trimmed.length > 50) { + if ( + trimmed.length < 6 || + trimmed.length > window.CONSTANTS.MAX_PASSWORD_LENGTH + ) { return i18n ? i18n('passwordLengthError') : ERRORS.LENGTH; } diff --git a/ts/window.d.ts b/ts/window.d.ts index e6adce865f..3e2d060968 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -74,7 +74,6 @@ declare global { lokiSnodeAPI: LokiSnodeAPI; mnemonic: RecoveryPhraseUtil; onLogin: any; - passwordUtil: any; resetDatabase: any; restart: any; seedNodeList: any; From 7054385d4ab0defcde449d399bc62b294eef4ad7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 27 Jan 2021 11:57:38 +1100 Subject: [PATCH 003/109] remove body pending loading of message > 2000. Also remove the limit when sending text messages --- ts/components/conversation/MessageBody.tsx | 25 +++-------------- .../conversation/message/MessageMetadata.tsx | 17 ++++++------ .../conversation/SessionCompositionBox.tsx | 27 ++++--------------- ts/session/constants.ts | 2 +- 4 files changed, 17 insertions(+), 54 deletions(-) diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index e4af45af1c..b81acd62d7 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -82,9 +82,7 @@ export class MessageBody extends React.Component { isGroup: false, }; - public addDownloading(jsx: JSX.Element): JSX.Element { - const { i18n } = this.props; - + public renderJsxSelectable(jsx: JSX.Element): JSX.Element { return {jsx}; } @@ -100,7 +98,7 @@ export class MessageBody extends React.Component { const sizeClass = disableJumbomoji ? undefined : getSizeClass(text); if (disableLinks) { - return this.addDownloading( + return this.renderJsxSelectable( renderEmoji({ i18n, text, @@ -113,24 +111,7 @@ export class MessageBody extends React.Component { ); } - const bodyContents = this.addDownloading( - { - return renderEmoji({ - i18n, - text: nonLinkText, - sizeClass, - key, - renderNonEmoji: renderNewLines, - isGroup, - convoId, - }); - }} - /> - ); - - return this.addDownloading( + return this.renderJsxSelectable( { diff --git a/ts/components/conversation/message/MessageMetadata.tsx b/ts/components/conversation/message/MessageMetadata.tsx index 5991c94230..ea1f62e7a3 100644 --- a/ts/components/conversation/message/MessageMetadata.tsx +++ b/ts/components/conversation/message/MessageMetadata.tsx @@ -96,14 +96,14 @@ export const MessageMetadata = (props: Props) => { theme={theme} /> ) : ( - - )} + + )} { /> ) : null} - {showStatus ? ( { spellCheck={true} inputRef={this.textarea} disabled={!typingEnabled} - maxLength={Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH} + // maxLength={Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH} rows={1} style={sendMessageStyle} suggestionsPortalHost={this.container} @@ -751,25 +751,8 @@ export class SessionCompositionBox extends React.Component { // tslint:disable-next-line: cyclomatic-complexity private async onSendMessage() { - const toUnicode = (str: string) => { - return str - .split('') - .map(value => { - const temp = value - .charCodeAt(0) - .toString(16) - .toUpperCase(); - if (temp.length > 2) { - return `\\u${temp}`; - } - return value; - }) - .join(''); - }; - // this is dirty but we have to replace all @(xxx) by @xxx manually here const cleanMentions = (text: string): string => { - const textUnicode = toUnicode(text); const matches = text.match(this.mentionsRegex); let replacedMentions = text; (matches || []).forEach(match => { @@ -808,10 +791,10 @@ export class SessionCompositionBox extends React.Component { } // Verify message length const msgLen = messagePlaintext?.length || 0; - if (msgLen > Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH) { - ToastUtils.pushMessageBodyTooLong(); - return; - } + // if (msgLen > Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH) { + // ToastUtils.pushMessageBodyTooLong(); + // return; + // } if (msgLen === 0 && this.props.stagedAttachments?.length === 0) { ToastUtils.pushMessageBodyMissing(); return; diff --git a/ts/session/constants.ts b/ts/session/constants.ts index 85c0b0dfe2..06c57392b5 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -9,7 +9,7 @@ export const TTL_DEFAULT = { // User Interface export const CONVERSATION = { - MAX_MESSAGE_BODY_LENGTH: 2000, + // MAX_MESSAGE_BODY_LENGTH: 2000, DEFAULT_MEDIA_FETCH_COUNT: 50, DEFAULT_DOCUMENTS_FETCH_COUNT: 150, DEFAULT_MESSAGE_FETCH_COUNT: 30, From 1d85a6dc5ffec78fb35f164989dad816c436eb92 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 29 Jan 2021 11:44:22 +1100 Subject: [PATCH 004/109] trigger new message onError while handling a request --- ts/receiver/errors.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ts/receiver/errors.ts b/ts/receiver/errors.ts index 7f0c2375fd..dc51bf4b98 100644 --- a/ts/receiver/errors.ts +++ b/ts/receiver/errors.ts @@ -1,6 +1,7 @@ import { initIncomingMessage } from './dataMessage'; import { toNumber } from 'lodash'; import { ConversationController } from '../session/conversations'; +import { MessageController } from '../session/messages'; export async function onError(ev: any) { const { error } = ev; @@ -10,19 +11,11 @@ export async function onError(ev: any) { ); if (ev.proto) { - if (error && error.name === 'MessageCounterError') { - if (ev.confirm) { - ev.confirm(); - } - // Ignore this message. It is likely a duplicate delivery - // because the server lost our ack the first time. - return; - } const envelope = ev.proto; const message = initIncomingMessage(envelope); - message.saveErrors(error || new Error('Error was null')); + await message.saveErrors(error || new Error('Error was null')); const id = message.get('conversationId'); const conversation = await ConversationController.getInstance().getOrCreateAndWait( id, @@ -42,6 +35,12 @@ export async function onError(ev: any) { conversation.updateLastMessage(); await conversation.notify(message); + MessageController.getInstance().register(message.id, message); + window.Whisper.events.trigger('messageAdded', { + conversationKey: conversation.id, + messageModel: message, + }); + if (ev.confirm) { ev.confirm(); From 3342c7fd268e5f0c65ede4471a2165a422d51555 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 29 Jan 2021 11:29:24 +1100 Subject: [PATCH 005/109] lint --- ts/receiver/closedGroups.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index a2296763f0..a594676e99 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -508,7 +508,6 @@ async function handleClosedGroupMembersAdded( const membersNotAlreadyPresent = addedMembers.filter( m => !oldMembers.includes(m) ); - console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent); if (membersNotAlreadyPresent.length === 0) { window.log.info( @@ -535,7 +534,7 @@ async function handleClosedGroupMembersRemoved( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, convo: ConversationModel -) {} +) { } async function handleClosedGroupMemberLeft( envelope: EnvelopePlus, From 3b3378a2ee785274b377d3a21329bcdbd7f711ce Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 11:35:06 +1100 Subject: [PATCH 006/109] finish explicit group updates --- app/sql.js | 8 ++- ts/receiver/closedGroups.ts | 91 ++++++++++++++++++++++++++- ts/receiver/contentMessage.ts | 18 ++++-- ts/session/crypto/MessageEncrypter.ts | 2 +- ts/session/group/index.ts | 4 +- 5 files changed, 113 insertions(+), 10 deletions(-) diff --git a/app/sql.js b/app/sql.js index cfbabe14af..b9a1128ced 100644 --- a/app/sql.js +++ b/app/sql.js @@ -3215,6 +3215,12 @@ async function updateExistingClosedGroupToClosedGroup(instance) { * @param {*} groupPublicKey string | PubKey */ async function getAllEncryptionKeyPairsForGroup(groupPublicKey) { + const rows = await getAllEncryptionKeyPairsForGroupRaw(groupPublicKey); + + return map(rows, row => jsonToObject(row.json)); +} + +async function getAllEncryptionKeyPairsForGroupRaw(groupPublicKey) { const pubkeyAsString = groupPublicKey.key ? groupPublicKey.key : groupPublicKey; @@ -3225,7 +3231,7 @@ async function getAllEncryptionKeyPairsForGroup(groupPublicKey) { } ); - return map(rows, row => jsonToObject(row.json)); + return rows; } async function getLatestClosedGroupEncryptionKeyPair(groupPublicKey) { diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index a594676e99..4f5d12dd9a 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -21,6 +21,7 @@ import { import { ECKeyPair } from './keypairs'; import { UserUtils } from '../session/utils'; import { ConversationModel } from '../models/conversation'; +import _ from 'lodash'; export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, @@ -295,6 +296,7 @@ async function handleUpdateClosedGroup( convo.set('members', members); await convo.commit(); + convo.updateLastMessage(); await removeFromCache(envelope); } @@ -315,6 +317,9 @@ async function handleClosedGroupEncryptionKeyPair( return; } const ourNumber = UserUtils.getOurPubKeyFromCache(); + window.log.info( + `Got a group update for group ${envelope.source}, type: ENCRYPTION_KEY_PAIR` + ); const groupPublicKey = envelope.source; const ourKeyPair = await UserUtils.getIdentityKeyPair(); @@ -485,12 +490,17 @@ async function handleClosedGroupNameChanged( ) { // Only add update message if we have something to show const newName = groupUpdate.name; + window.log.info( + `Got a group update for group ${envelope.source}, type: NAME_CHANGED` + ); + if (newName !== convo.get('name')) { const groupDiff: ClosedGroup.GroupDiff = { newName, }; await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); convo.set({ name: newName }); + convo.updateLastMessage(); await convo.commit(); } @@ -508,6 +518,10 @@ async function handleClosedGroupMembersAdded( const membersNotAlreadyPresent = addedMembers.filter( m => !oldMembers.includes(m) ); + console.warn('membersNotAlreadyPresent', membersNotAlreadyPresent); + window.log.info( + `Got a group update for group ${envelope.source}, type: MEMBERS_ADDED` + ); if (membersNotAlreadyPresent.length === 0) { window.log.info( @@ -526,6 +540,7 @@ async function handleClosedGroupMembersAdded( await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); convo.set({ members }); + convo.updateLastMessage(); await convo.commit(); await removeFromCache(envelope); } @@ -534,7 +549,77 @@ async function handleClosedGroupMembersRemoved( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, convo: ConversationModel -) { } +) { + // Check that the admin wasn't removed + const currentMembers = convo.get('members'); + // removedMembers are all members in the diff + const removedMembers = groupUpdate.members.map(toHex); + // effectivelyRemovedMembers are the members which where effectively on this group before the update + // and is used for the group update message only + const effectivelyRemovedMembers = removedMembers.filter(m => + currentMembers.includes(m) + ); + const groupPubKey = envelope.source; + window.log.info( + `Got a group update for group ${envelope.source}, type: MEMBERS_REMOVED` + ); + + const membersAfterUpdate = _.difference(currentMembers, removedMembers); + const groupAdmins = convo.get('groupAdmins'); + if (!groupAdmins?.length) { + throw new Error('No admins found for closed group member removed update.'); + } + const firstAdmin = groupAdmins[0]; + + if (removedMembers.includes(firstAdmin)) { + window.log.warn( + 'Ignoring invalid closed group update: trying to remove the admin.' + ); + await removeFromCache(envelope); + return; + } + + // If the current user was removed: + // • Stop polling for the group + // • Remove the key pairs associated with the group + const ourPubKey = UserUtils.getOurPubKeyFromCache(); + const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); + if (wasCurrentUserRemoved) { + await window.Signal.Data.removeAllClosedGroupEncryptionKeyPairs( + groupPubKey + ); + // Disable typing: + convo.set('isKickedFromGroup', true); + window.SwarmPolling.removePubkey(groupPubKey); + } + // Generate and distribute a new encryption key pair if needed + const isCurrentUserAdmin = firstAdmin === ourPubKey.key; + if (isCurrentUserAdmin) { + try { + await ClosedGroup.generateAndSendNewEncryptionKeyPair( + groupPubKey, + membersAfterUpdate + ); + } catch (e) { + window.log.warn('Could not distribute new encryption keypair.'); + } + } + + // Only add update message if we have something to show + if (membersAfterUpdate.length !== currentMembers.length) { + const groupDiff: ClosedGroup.GroupDiff = { + leavingMembers: effectivelyRemovedMembers, + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + convo.updateLastMessage(); + } + + // Update the group + convo.set({ members: membersAfterUpdate }); + + await convo.commit(); + await removeFromCache(envelope); +} async function handleClosedGroupMemberLeft( envelope: EnvelopePlus, @@ -578,7 +663,6 @@ async function handleClosedGroupMemberLeft( convo.set('isKickedFromGroup', true); window.SwarmPolling.removePubkey(groupPublicKey); } - // Update the group // Only add update message if we have something to show if (leftMemberWasPresent) { @@ -586,6 +670,7 @@ async function handleClosedGroupMemberLeft( leavingMembers: didAdminLeave ? oldMembers : [sender], }; await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming'); + convo.updateLastMessage(); } convo.set('members', members); @@ -648,7 +733,9 @@ export async function createClosedGroup( // the sending pipeline needs to know from GroupUtils when a message is for a medium group await ClosedGroup.updateOrCreateClosedGroup(groupDetails); convo.set('lastJoinedTimestamp', Date.now()); + convo.set('active_at', Date.now()); await convo.commit(); + convo.updateLastMessage(); // Send a closed group update message to all members individually const promises = listOfMembers.map(async m => { diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 047366ec37..5146390ed3 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -1,6 +1,5 @@ import { EnvelopePlus } from './types'; import { handleDataMessage } from './dataMessage'; -import { getEnvelopeId } from './common'; import { removeFromCache, updateCache } from './cache'; import { SignalService } from '../protobuf'; @@ -75,6 +74,9 @@ async function decryptForClosedGroup( encryptionKeyPair, true ); + if (decryptedContent?.byteLength) { + break; + } keyIndex++; } catch (e) { window.log.info( @@ -83,13 +85,21 @@ async function decryptForClosedGroup( } } while (encryptionKeyPairs.length > 0); - if (!decryptedContent) { + if (!decryptedContent?.byteLength) { await removeFromCache(envelope); throw new Error( `Could not decrypt message for closed group with any of the ${encryptionKeyPairsCount} keypairs.` ); } - window.log.info('ClosedGroup Message decrypted successfully.'); + if (keyIndex !== 0) { + window.log.warn( + 'Decrypted a closed group message with not the latest encryptionkeypair we have' + ); + } + window.log.info( + 'ClosedGroup Message decrypted successfully with keyIndex:', + keyIndex + ); const ourDevicePubKey = UserUtils.getOurPubKeyStrFromCache(); if ( @@ -483,7 +493,7 @@ async function handleTypingMessage( const started = action === SignalService.TypingMessage.Action.STARTED; if (conversation) { - conversation.notifyTyping({ + await conversation.notifyTyping({ isTyping: started, sender: source, }); diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 6cc04e6dec..28e32ff5b8 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -114,7 +114,7 @@ export async function encryptUsingSessionProtocol( window?.log?.info( 'encryptUsingSessionProtocol for ', - recipientHexEncodedX25519PublicKey + recipientHexEncodedX25519PublicKey.key ); const recipientX25519PublicKey = recipientHexEncodedX25519PublicKey.withoutPrefixToArray(); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index b7f15cb057..088fb4a249 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -341,13 +341,13 @@ export async function leaveClosedGroup(groupId: string) { expireTimer: 0, }); window.getMessageController().register(dbMessage.id, dbMessage); - + const existingExpireTimer = convo.get('expireTimer') || 0; // Send the update to the group const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ timestamp: Date.now(), groupId, identifier: dbMessage.id, - expireTimer: 0, + expireTimer: existingExpireTimer, }); window.log.info( From ebd94ce15cfca641971f4f1027778b4ece704c1a Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 13:12:44 +1100 Subject: [PATCH 007/109] fix order of added new group in leftPane --- ts/receiver/closedGroups.ts | 23 ++++++++++++----------- ts/session/group/index.ts | 3 +++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 4f5d12dd9a..1bebd4e7d5 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -196,16 +196,19 @@ async function handleNewClosedGroup( 'incoming' ); - convo.set('name', name); - convo.set('members', members); - // mark a closed group as a medium group. - // this field is used to poll for this groupPubKey on the swarm nodes, among other things - convo.set('is_medium_group', true); - convo.set('active_at', Date.now()); - convo.set('lastJoinedTimestamp', Date.now()); - // We only set group admins on group creation - convo.set('groupAdmins', admins); + const groupDetails = { + id: groupId, + name: name, + members: members, + admins, + active: true, + }; + + // be sure to call this before sending the message. + // the sending pipeline needs to know from GroupUtils when a message is for a medium group + await ClosedGroup.updateOrCreateClosedGroup(groupDetails); + await convo.commit(); // sanity checks validate this // tslint:disable: no-non-null-assertion @@ -732,8 +735,6 @@ export async function createClosedGroup( // be sure to call this before sending the message. // the sending pipeline needs to know from GroupUtils when a message is for a medium group await ClosedGroup.updateOrCreateClosedGroup(groupDetails); - convo.set('lastJoinedTimestamp', Date.now()); - convo.set('active_at', Date.now()); await convo.commit(); convo.updateLastMessage(); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 088fb4a249..b2c2b058b4 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -258,8 +258,11 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { // activeAt is null, then this group has been purposefully hidden. if (activeAt !== null) { updates.active_at = activeAt || Date.now(); + updates.timestamp = updates.active_at; } updates.left = false; + updates.lastJoinedTimestamp = updates.active_at; + } else { updates.left = true; } From 267a3e6bf6f95ea0822b4fd48a363572a0642950 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 13:14:19 +1100 Subject: [PATCH 008/109] disable closedgroup update groups for now --- ts/session/group/index.ts | 1 - .../unit/receiving/ClosedGroupUpdates_test.ts | 69 +++++++++---------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index b2c2b058b4..c1fac30143 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -262,7 +262,6 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { } updates.left = false; updates.lastJoinedTimestamp = updates.active_at; - } else { updates.left = true; } diff --git a/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts b/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts index dcb3024023..560b21c941 100644 --- a/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts +++ b/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts @@ -20,41 +20,38 @@ const { expect } = chai; // tslint:disable-next-line: max-func-body-length describe('ClosedGroupUpdates', () => { + //FIXME AUDRIC TODO // Initialize new stubbed cache - const sandbox = sinon.createSandbox(); - const ourDevice = TestUtils.generateFakePubKey(); - const ourNumber = ourDevice.key; - const groupId = TestUtils.generateFakePubKey().key; - const members = TestUtils.generateFakePubKeys(10); - const sender = members[3].key; - const getConvo = sandbox.stub(ConversationController.getInstance(), 'get'); - - beforeEach(async () => { - // Utils Stubs - sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); - }); - - afterEach(() => { - TestUtils.restoreStubs(); - sandbox.restore(); - }); - - describe('handleClosedGroupControlMessage', () => { - describe('performIfValid', () => { - it('does not perform if convo does not exist', async () => { - const envelope = generateEnvelopePlusClosedGroup(groupId, sender); - const groupUpdate = generateGroupUpdateNameChange(groupId); - getConvo.returns(undefined as any); - await handleClosedGroupControlMessage(envelope, groupUpdate); - }); - }); - - // describe('handleClosedGroupNameChanged', () => { - // it('does not trigger an update of the group if the name is the same', async () => { - // const envelope = generateEnvelopePlusClosedGroup(groupId, sender); - // const groupUpdate = generateGroupUpdateNameChange(groupId); - // await handleClosedGroupControlMessage(envelope, groupUpdate); - // }); - // }); - }); + // const sandbox = sinon.createSandbox(); + // const ourDevice = TestUtils.generateFakePubKey(); + // const ourNumber = ourDevice.key; + // const groupId = TestUtils.generateFakePubKey().key; + // const members = TestUtils.generateFakePubKeys(10); + // const sender = members[3].key; + // const getConvo = sandbox.stub(ConversationController.getInstance(), 'get'); + // beforeEach(async () => { + // // Utils Stubs + // sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber); + // }); + // afterEach(() => { + // TestUtils.restoreStubs(); + // sandbox.restore(); + // }); + // describe('handleClosedGroupControlMessage', () => { + // describe('performIfValid', () => { + // it('does not perform if convo does not exist', async () => { + // const envelope = generateEnvelopePlusClosedGroup(groupId, sender); + // const groupUpdate = generateGroupUpdateNameChange(groupId); + // getConvo.returns(undefined as any); + // await handleClosedGroupControlMessage(envelope, groupUpdate); + // }); + // }); + // // describe('handleClosedGroupNameChanged', () => { + // // it('does not trigger an update of the group if the name is the same', async () => { + // // const envelope = generateEnvelopePlusClosedGroup(groupId, sender); + // // const groupUpdate = generateGroupUpdateNameChange(groupId); + // // await handleClosedGroupControlMessage(envelope, groupUpdate); + // // }); + // // }); + // }); }); From 89ea946b3fdfc72c32c6cea60ef63ebe1378d1fa Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 16:40:23 +1100 Subject: [PATCH 009/109] disable sending of explicit group updates for now - only receiving is ON --- preload.js | 6 +- ts/session/group/index.ts | 134 +++++++++++++++++- .../data/group/ClosedGroupUpdateMessage.ts | 51 +++++++ .../outgoing/content/data/group/index.ts | 1 + ts/window.d.ts | 5 +- 5 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts diff --git a/preload.js b/preload.js index 3108dc2794..2061fc19df 100644 --- a/preload.js +++ b/preload.js @@ -56,13 +56,12 @@ window.getDefaultFileServer = () => config.defaultFileServer; window.initialisedAPI = false; window.lokiFeatureFlags = { - multiDeviceUnpairing: true, - privateGroupChats: true, useOnionRequests: true, useOnionRequestsV2: true, useFileOnionRequests: true, useFileOnionRequestsV2: true, // more compact encoding of files in response onionRequestHops: 3, + useExplicitGroupUpdatesSending: false, }; if ( @@ -487,11 +486,10 @@ if ( } if (config.environment.includes('test-integration')) { window.lokiFeatureFlags = { - multiDeviceUnpairing: true, - privateGroupChats: true, useOnionRequests: false, useFileOnionRequests: false, useOnionRequestsV2: false, + useExplicitGroupUpdatesSending: false, }; /* eslint-disable global-require, import/no-extraneous-dependencies */ window.sinon = require('sinon'); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index c1fac30143..e498fb8ed9 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -21,6 +21,7 @@ import { ClosedGroupNameChangeMessage, ClosedGroupNewMessage, ClosedGroupRemovedMembersMessage, + ClosedGroupUpdateMessage, } from '../messages/outgoing/content/data/group'; import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; @@ -76,6 +77,8 @@ export async function syncMediumGroups(groups: Array) { // await Promise.all(groups.map(syncMediumGroup)); } +// tslint:disable: max-func-body-length +// tslint:disable: cyclomatic-complexity export async function initiateGroupUpdate( groupId: string, groupName: string, @@ -118,6 +121,111 @@ export async function initiateGroupUpdate( expireTimer: convo.get('expireTimer'), }; + if (!window.lokiFeatureFlags.useExplicitGroupUpdatesSending) { + // we still don't send any explicit group updates for now - only the receiving side is enabled + const dbMessageAdded = await addUpdateMessage(convo, diff, 'outgoing'); + window.getMessageController().register(dbMessageAdded.id, dbMessageAdded); + // Check preconditions + const hexEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair( + groupId + ); + if (!hexEncryptionKeyPair) { + throw new Error("Couldn't get key pair for closed group"); + } + + const encryptionKeyPair = ECKeyPair.fromHexKeyPair(hexEncryptionKeyPair); + const removedMembers = diff.leavingMembers || []; + const newMembers = diff.joiningMembers || []; // joining members + const wasAnyUserRemoved = removedMembers.length > 0; + const ourPrimary = await UserUtils.getOurNumber(); + const isUserLeaving = removedMembers.includes(ourPrimary.key); + const isCurrentUserAdmin = convo + .get('groupAdmins') + ?.includes(ourPrimary.key); + const expireTimerToShare = groupDetails.expireTimer || 0; + + const admins = convo.get('groupAdmins') || []; + if (removedMembers.includes(admins[0]) && newMembers.length !== 0) { + throw new Error( + "Can't remove admin from closed group without removing everyone." + ); // Error.invalidClosedGroupUpdate + } + + if (isUserLeaving && newMembers.length !== 0) { + if (removedMembers.length !== 1 || newMembers.length !== 0) { + throw new Error( + "Can't remove self and add or remove others simultaneously." + ); + } + } + + // Send the update to the group + const mainClosedGroupUpdate = new ClosedGroupUpdateMessage({ + timestamp: Date.now(), + groupId, + name: groupName, + members, + identifier: dbMessageAdded.id || uuid(), + expireTimer: expireTimerToShare, + }); + + if (isUserLeaving) { + window.log.info( + `We are leaving the group ${groupId}. Sending our leaving message.` + ); + // sent the message to the group and once done, remove everything related to this group + window.SwarmPolling.removePubkey(groupId); + await getMessageQueue().sendToGroup(mainClosedGroupUpdate, async () => { + window.log.info( + `Leaving message sent ${groupId}. Removing everything related to this group.` + ); + await Data.removeAllClosedGroupEncryptionKeyPairs(groupId); + }); + } else { + // Send the group update, and only once sent, generate and distribute a new encryption key pair if needed + await getMessageQueue().sendToGroup(mainClosedGroupUpdate, async () => { + if (wasAnyUserRemoved && isCurrentUserAdmin) { + // we send the new encryption key only to members already here before the update + const membersNotNew = members.filter(m => !newMembers.includes(m)); + window.log.info( + `Sending group update: A user was removed from ${groupId} and we are the admin. Generating and sending a new EncryptionKeyPair` + ); + + await generateAndSendNewEncryptionKeyPair(groupId, membersNotNew); + } + }); + + if (newMembers.length) { + // Send closed group update messages to any new members individually + const newClosedGroupUpdate = new ClosedGroupNewMessage({ + timestamp: Date.now(), + name: groupName, + groupId, + admins, + members, + keypair: encryptionKeyPair, + identifier: dbMessageAdded.id || uuid(), + expireTimer: expireTimerToShare, + }); + + const promises = newMembers.map(async m => { + await ConversationController.getInstance().getOrCreateAndWait( + m, + 'private' + ); + const memberPubKey = PubKey.cast(m); + await getMessageQueue().sendToPubKey( + memberPubKey, + newClosedGroupUpdate + ); + }); + await Promise.all(promises); + } + } + + return; + } + if (diff.newName?.length) { const nameOnlyDiff: GroupDiff = { newName: diff.newName }; const dbMessageName = await addUpdateMessage( @@ -345,12 +453,26 @@ export async function leaveClosedGroup(groupId: string) { window.getMessageController().register(dbMessage.id, dbMessage); const existingExpireTimer = convo.get('expireTimer') || 0; // Send the update to the group - const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ - timestamp: Date.now(), - groupId, - identifier: dbMessage.id, - expireTimer: existingExpireTimer, - }); + let ourLeavingMessage; + + if (window.lokiFeatureFlags.useExplicitGroupUpdatesSending) { + ourLeavingMessage = new ClosedGroupMemberLeftMessage({ + timestamp: Date.now(), + groupId, + identifier: dbMessage.id, + expireTimer: existingExpireTimer, + }); + } else { + const ourPubkey = await UserUtils.getOurNumber(); + ourLeavingMessage = new ClosedGroupUpdateMessage({ + timestamp: Date.now(), + groupId, + identifier: dbMessage.id, + expireTimer: existingExpireTimer, + name: convo.get('name'), + members: convo.get('members').filter(m => m !== ourPubkey.key), + }); + } window.log.info( `We are leaving the group ${groupId}. Sending our leaving message.` diff --git a/ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts b/ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts new file mode 100644 index 0000000000..f5a0b2c731 --- /dev/null +++ b/ts/session/messages/outgoing/content/data/group/ClosedGroupUpdateMessage.ts @@ -0,0 +1,51 @@ +import { SignalService } from '../../../../../../protobuf'; +import { + ClosedGroupMessage, + ClosedGroupMessageParams, +} from './ClosedGroupMessage'; +import { fromHexToArray } from '../../../../../utils/String'; + +export interface ClosedGroupUpdateMessageParams + extends ClosedGroupMessageParams { + name: string; + members: Array; + expireTimer: number; +} + +export class ClosedGroupUpdateMessage extends ClosedGroupMessage { + private readonly name: string; + private readonly members: Array; + + constructor(params: ClosedGroupUpdateMessageParams) { + super({ + timestamp: params.timestamp, + identifier: params.identifier, + groupId: params.groupId, + expireTimer: params.expireTimer, + }); + this.name = params.name; + this.members = params.members; + + // members can be empty. It means noone is in the group anymore and it happens when an admin leaves the group + if (!params.members) { + throw new Error('Members must be set'); + } + if (!params.name || params.name.length === 0) { + throw new Error('Name must cannot be empty'); + } + } + + public dataProto(): SignalService.DataMessage { + const dataMessage = new SignalService.DataMessage(); + + dataMessage.closedGroupControlMessage = new SignalService.DataMessage.ClosedGroupControlMessage(); + dataMessage.closedGroupControlMessage.type = + SignalService.DataMessage.ClosedGroupControlMessage.Type.UPDATE; + dataMessage.closedGroupControlMessage.name = this.name; + dataMessage.closedGroupControlMessage.members = this.members.map( + fromHexToArray + ); + + return dataMessage; + } +} diff --git a/ts/session/messages/outgoing/content/data/group/index.ts b/ts/session/messages/outgoing/content/data/group/index.ts index e9f861ff76..c16a052c16 100644 --- a/ts/session/messages/outgoing/content/data/group/index.ts +++ b/ts/session/messages/outgoing/content/data/group/index.ts @@ -4,3 +4,4 @@ export * from './ClosedGroupNewMessage'; export * from './ClosedGroupAddedMembersMessage'; export * from './ClosedGroupNameChangeMessage'; export * from './ClosedGroupRemovedMembersMessage'; +export * from './ClosedGroupUpdateMessage'; diff --git a/ts/window.d.ts b/ts/window.d.ts index 3e2d060968..a3029a7ace 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -12,7 +12,7 @@ import { LibTextsecure } from '../libtextsecure'; import { ConversationType } from '../js/modules/data'; import { RecoveryPhraseUtil } from '../libloki/modules/mnemonic'; import { ConfirmationDialogParams } from '../background'; -import {} from 'styled-components/cssprop'; +import { } from 'styled-components/cssprop'; import { ConversationControllerType } from '../js/ConversationController'; import { any } from 'underscore'; @@ -60,12 +60,11 @@ declare global { libsignal: LibsignalProtocol; log: any; lokiFeatureFlags: { - multiDeviceUnpairing: boolean; - privateGroupChats: boolean; useOnionRequests: boolean; useOnionRequestsV2: boolean; useFileOnionRequests: boolean; useFileOnionRequestsV2: boolean; + useExplicitGroupUpdatesSending: boolean; onionRequestHops: number; }; lokiFileServerAPI: LokiFileServerInstance; From 1d5d098b069a2de965e8e9ea0cb8fbe2d9657bad Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 15:07:59 +1100 Subject: [PATCH 010/109] be able to remove a closed group once we left it already --- .../session/menu/ConversationHeaderMenu.tsx | 1 + .../menu/ConversationListItemContextMenu.tsx | 1 + ts/components/session/menu/Menu.tsx | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index caa2746226..f0a0454a97 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -125,6 +125,7 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isMe, isGroup, isPublic, + left, onDeleteContact, window.i18n )} diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index f18bb260d2..5d0ea6ff29 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -92,6 +92,7 @@ export const ConversationListItemContextMenu = ( isMe, type === 'group', isPublic, + left, onDeleteContact, window.i18n )} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 87072aad13..bb0be3d158 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -39,9 +39,11 @@ function showCopyId(isPublic: boolean, isGroup: boolean): boolean { function showDeleteContact( isMe: boolean, isGroup: boolean, - isPublic: boolean + isPublic: boolean, + isGroupLeft: boolean ): boolean { - return !isMe && Boolean(!isGroup || isPublic); + // you need to have left a closed group first to be able to delete it completely. + return (!isMe && !isGroup) || (isGroup && isGroupLeft); } function showAddModerators( @@ -97,10 +99,18 @@ export function getDeleteContactMenuItem( isMe: boolean | undefined, isGroup: boolean | undefined, isPublic: boolean | undefined, + isLeft: boolean | undefined, action: any, i18n: LocalizerType ): JSX.Element | null { - if (showDeleteContact(Boolean(isMe), Boolean(isGroup), Boolean(isPublic))) { + if ( + showDeleteContact( + Boolean(isMe), + Boolean(isGroup), + Boolean(isPublic), + Boolean(isLeft) + ) + ) { if (isPublic) { return {i18n('leaveGroup')}; } From 874e3f863b1a638dd79e3a33549edbf7629f639e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 15:43:05 +1100 Subject: [PATCH 011/109] remove unused worker --- js/background.js | 2 - libtextsecure/libsignal-protocol.js | 71 ----------------------------- libtextsecure/protocol_wrapper.js | 5 +- ts/util/lint/linter.ts | 1 - 4 files changed, 1 insertion(+), 78 deletions(-) diff --git a/js/background.js b/js/background.js index 5aefe70d06..b503a15b50 100644 --- a/js/background.js +++ b/js/background.js @@ -106,8 +106,6 @@ window.document.title = window.getTitle(); - // start a background worker for ecc - textsecure.startWorker('js/libsignal-protocol-worker.js'); let messageReceiver; Whisper.events = _.clone(Backbone.Events); Whisper.events.isListenedTo = eventName => diff --git a/libtextsecure/libsignal-protocol.js b/libtextsecure/libsignal-protocol.js index f34a981a13..dfda09ecc7 100644 --- a/libtextsecure/libsignal-protocol.js +++ b/libtextsecure/libsignal-protocol.js @@ -25316,77 +25316,6 @@ })(); - ; (function () { - - 'use strict'; - - // I am the...workee? - var origCurve25519 = Internal.curve25519_async; - - Internal.startWorker = function (url) { - Internal.stopWorker(); // there can be only one - - Internal.curve25519_async = new Curve25519Worker(url); - Internal.Curve.async = Internal.wrapCurve25519(Internal.curve25519_async); - libsignal.Curve.async = Internal.wrapCurve(Internal.Curve.async); - }; - - Internal.stopWorker = function () { - if (Internal.curve25519_async instanceof Curve25519Worker) { - var worker = Internal.curve25519_async.worker; - - Internal.curve25519_async = origCurve25519; - Internal.Curve.async = Internal.wrapCurve25519(Internal.curve25519_async); - libsignal.Curve.async = Internal.wrapCurve(Internal.Curve.async); - - worker.terminate(); - } - }; - - libsignal.worker = { - startWorker: Internal.startWorker, - stopWorker: Internal.stopWorker, - }; - - function Curve25519Worker(url) { - this.jobs = {}; - this.jobId = 0; - this.worker = new Worker(url); - this.worker.onmessage = function (e) { - var job = this.jobs[e.data.id]; - if (e.data.error && typeof job.onerror === 'function') { - job.onerror(new Error(e.data.error)); - } else if (typeof job.onsuccess === 'function') { - job.onsuccess(e.data.result); - } - delete this.jobs[e.data.id]; - }.bind(this); - } - - Curve25519Worker.prototype = { - constructor: Curve25519Worker, - postMessage: function (methodName, args, onsuccess, onerror) { - return new Promise(function (resolve, reject) { - this.jobs[this.jobId] = { onsuccess: resolve, onerror: reject }; - this.worker.postMessage({ id: this.jobId, methodName: methodName, args: args }); - this.jobId++; - }.bind(this)); - }, - keyPair: function (privKey) { - return this.postMessage('keyPair', [privKey]); - }, - sharedSecret: function (pubKey, privKey) { - return this.postMessage('sharedSecret', [pubKey, privKey]); - }, - sign: function (privKey, message) { - return this.postMessage('sign', [privKey, message]); - }, - verify: function (pubKey, message, sig) { - return this.postMessage('verify', [pubKey, message, sig]); - } - }; - - })(); /* Copyright 2013 Daniel Wirtz diff --git a/libtextsecure/protocol_wrapper.js b/libtextsecure/protocol_wrapper.js index 1846f26b9a..73391ae4e9 100644 --- a/libtextsecure/protocol_wrapper.js +++ b/libtextsecure/protocol_wrapper.js @@ -1,11 +1,8 @@ -/* global window, textsecure, SignalProtocolStore, libsignal */ +/* global window, textsecure, SignalProtocolStore */ // eslint-disable-next-line func-names (function() { window.textsecure = window.textsecure || {}; window.textsecure.storage = window.textsecure.storage || {}; textsecure.storage.protocol = new SignalProtocolStore(); - - textsecure.startWorker = libsignal.worker.startWorker; - textsecure.stopWorker = libsignal.worker.stopWorker; })(); diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index a1157be416..fc4734ba10 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -63,7 +63,6 @@ const excludedFiles = [ '^test/test.js', // From libsignal-protocol-javascript project - '^js/libsignal-protocol-worker.js', '^libtextsecure/libsignal-protocol.js', // Test files From 846f39654541c05949c85632e86f3d63a5c3da5c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 17:07:44 +1100 Subject: [PATCH 012/109] show Delete conversation menu when we got removed from the group too --- ts/components/session/menu/ConversationHeaderMenu.tsx | 1 + .../session/menu/ConversationListItemContextMenu.tsx | 1 + ts/components/session/menu/Menu.tsx | 9 ++++++--- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index f0a0454a97..9dcd0a37e9 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -126,6 +126,7 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isGroup, isPublic, left, + isKickedFromGroup, onDeleteContact, window.i18n )} diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index 5d0ea6ff29..7c3eadfb5f 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -93,6 +93,7 @@ export const ConversationListItemContextMenu = ( type === 'group', isPublic, left, + isKickedFromGroup, onDeleteContact, window.i18n )} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index bb0be3d158..25b83ae7d3 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -40,10 +40,11 @@ function showDeleteContact( isMe: boolean, isGroup: boolean, isPublic: boolean, - isGroupLeft: boolean + isGroupLeft: boolean, + isKickedFromGroup: boolean ): boolean { // you need to have left a closed group first to be able to delete it completely. - return (!isMe && !isGroup) || (isGroup && isGroupLeft); + return (!isMe && !isGroup) || (isGroup && (isGroupLeft || isKickedFromGroup)); } function showAddModerators( @@ -100,6 +101,7 @@ export function getDeleteContactMenuItem( isGroup: boolean | undefined, isPublic: boolean | undefined, isLeft: boolean | undefined, + isKickedFromGroup: boolean | undefined, action: any, i18n: LocalizerType ): JSX.Element | null { @@ -108,7 +110,8 @@ export function getDeleteContactMenuItem( Boolean(isMe), Boolean(isGroup), Boolean(isPublic), - Boolean(isLeft) + Boolean(isLeft), + Boolean(isKickedFromGroup) ) ) { if (isPublic) { From 5a7c8ffa35ecd032d2685b13802d3d7d878e6629 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 1 Feb 2021 14:45:46 +1100 Subject: [PATCH 013/109] fix previews sent on next message if they are resolved too late --- .../conversation/SessionCompositionBox.tsx | 110 ++++++++++++------ ts/session/utils/Promise.ts | 8 ++ ts/util/linkPreviewFetch.ts | 1 + 3 files changed, 83 insertions(+), 36 deletions(-) diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index bf595ef6bf..71ee59a1d9 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -165,6 +165,7 @@ export class SessionCompositionBox extends React.Component { this.onChooseAttachment = this.onChooseAttachment.bind(this); this.onClickAttachment = this.onClickAttachment.bind(this); this.renderCaptionEditor = this.renderCaptionEditor.bind(this); + this.abortLinkPreviewFetch = this.abortLinkPreviewFetch.bind(this); // On Sending this.onSendMessage = this.onSendMessage.bind(this); @@ -183,7 +184,7 @@ export class SessionCompositionBox extends React.Component { } public componentWillUnmount() { - this.linkPreviewAbortController?.abort(); + this.abortLinkPreviewFetch(); this.linkPreviewAbortController = undefined; } public componentDidUpdate(prevProps: Props, _prevState: State) { @@ -566,7 +567,7 @@ export class SessionCompositionBox extends React.Component { }, }); const abortController = new AbortController(); - this.linkPreviewAbortController?.abort(); + this.abortLinkPreviewFetch(); this.linkPreviewAbortController = abortController; setTimeout(() => { abortController.abort(); @@ -590,31 +591,64 @@ export class SessionCompositionBox extends React.Component { } } } - this.setState({ - stagedLinkPreview: { - isLoaded: true, - title: ret?.title || null, - description: ret?.description || '', - url: ret?.url || null, - domain: - (ret?.url && window.Signal.LinkPreviews.getDomain(ret.url)) || '', - image, - }, - }); + // we finished loading the preview, and checking the abortConrtoller, we are still not aborted. + // => update the staged preview + if ( + this.linkPreviewAbortController && + !this.linkPreviewAbortController.signal.aborted + ) { + this.setState({ + stagedLinkPreview: { + isLoaded: true, + title: ret?.title || null, + description: ret?.description || '', + url: ret?.url || null, + domain: + (ret?.url && window.Signal.LinkPreviews.getDomain(ret.url)) || + '', + image, + }, + }); + } else if (this.linkPreviewAbortController) { + this.setState({ + stagedLinkPreview: { + isLoaded: false, + title: null, + description: null, + url: null, + domain: null, + image: undefined, + }, + }); + this.linkPreviewAbortController = undefined; + } }) .catch(err => { window.log.warn('fetch link preview: ', err); - abortController.abort(); - this.setState({ - stagedLinkPreview: { - isLoaded: true, - title: null, - domain: null, - description: null, - url: firstLink, - image: undefined, - }, - }); + const aborted = this.linkPreviewAbortController?.signal.aborted; + this.linkPreviewAbortController = undefined; + // if we were aborted, it either means the UI was unmount, or more probably, + // than the message was sent without the link preview. + // So be sure to reset the staged link preview so it is not sent with the next message. + + // if we were not aborted, it's probably just an error on the fetch. Nothing to do excpet mark the fetch as done (with errors) + + if (aborted) { + this.setState({ + stagedLinkPreview: undefined, + }); + } else { + this.setState({ + stagedLinkPreview: { + isLoaded: true, + title: null, + description: null, + url: firstLink, + domain: null, + image: undefined, + }, + }); + } }); } @@ -751,6 +785,8 @@ export class SessionCompositionBox extends React.Component { // tslint:disable-next-line: cyclomatic-complexity private async onSendMessage() { + this.abortLinkPreviewFetch(); + // this is dirty but we have to replace all @(xxx) by @xxx manually here const cleanMentions = (text: string): string => { const matches = text.match(this.mentionsRegex); @@ -835,10 +871,13 @@ export class SessionCompositionBox extends React.Component { 'attachments' ); + // we consider that a link previews without a title at least is not a preview const linkPreviews = - (stagedLinkPreview && [ - _.pick(stagedLinkPreview, 'url', 'image', 'title'), - ]) || + (stagedLinkPreview && + stagedLinkPreview.isLoaded && + stagedLinkPreview.title?.length && [ + _.pick(stagedLinkPreview, 'url', 'image', 'title'), + ]) || []; try { @@ -854,20 +893,15 @@ export class SessionCompositionBox extends React.Component { // Message sending sucess this.props.onMessageSuccess(); + this.props.clearAttachments(); - // Empty composition box + // Empty composition box and stagedAttachments this.setState({ message: '', showEmojiPanel: false, + stagedLinkPreview: undefined, + ignoredLink: undefined, }); - // Empty stagedAttachments - this.props.clearAttachments(); - if (stagedLinkPreview && stagedLinkPreview.url) { - this.setState({ - stagedLinkPreview: undefined, - ignoredLink: undefined, - }); - } } catch (e) { // Message sending failed window.log.error(e); @@ -983,4 +1017,8 @@ export class SessionCompositionBox extends React.Component { // Focus the textarea when user clicks anywhere in the composition box this.textarea.current?.focus(); } + + private abortLinkPreviewFetch() { + this.linkPreviewAbortController?.abort(); + } } diff --git a/ts/session/utils/Promise.ts b/ts/session/utils/Promise.ts index 237938e774..506d0ef515 100644 --- a/ts/session/utils/Promise.ts +++ b/ts/session/utils/Promise.ts @@ -131,3 +131,11 @@ export async function timeout( return Promise.race([timeoutPromise, promise]); } + +export async function delay(timeoutMs: number = 2000): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, timeoutMs); + }); +} diff --git a/ts/util/linkPreviewFetch.ts b/ts/util/linkPreviewFetch.ts index 2f3900247f..6193db7b06 100644 --- a/ts/util/linkPreviewFetch.ts +++ b/ts/util/linkPreviewFetch.ts @@ -9,6 +9,7 @@ import { IMAGE_WEBP, MIMEType, } from '../types/MIME'; +import { PromiseUtils } from '../session/utils'; const MAX_REQUEST_COUNT_WITH_REDIRECTS = 20; // tslint:disable: prefer-for-of From 85b9f22b04fd4f1a6b8b62b1c9d430336327ec43 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 2 Feb 2021 11:09:13 +1100 Subject: [PATCH 014/109] allow back to leave an open group --- ts/components/session/menu/Menu.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 25b83ae7d3..ff9a0725aa 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -33,7 +33,7 @@ function showDeleteMessages(isPublic: boolean): boolean { } function showCopyId(isPublic: boolean, isGroup: boolean): boolean { - return !isGroup || isPublic; + return !isGroup; // || isPublic; } function showDeleteContact( @@ -44,7 +44,10 @@ function showDeleteContact( isKickedFromGroup: boolean ): boolean { // you need to have left a closed group first to be able to delete it completely. - return (!isMe && !isGroup) || (isGroup && (isGroupLeft || isKickedFromGroup)); + return ( + (!isMe && !isGroup) || + (isGroup && (isGroupLeft || isKickedFromGroup || isPublic)) + ); } function showAddModerators( From 4d0c7aad061a54656f0f8d19cd942c92ed27cbcb Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 2 Feb 2021 13:05:54 +1100 Subject: [PATCH 015/109] Bump to v1.4.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ff7707556..19a9a04770 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.4.7", + "version": "1.4.8", "license": "GPL-3.0", "author": { "name": "Loki Project", From 49ca1a0f82ea85b6bbbd80233d38b14c2b330412 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 2 Feb 2021 16:04:50 +1100 Subject: [PATCH 016/109] fix registration continue your session button not shown for recovery --- ts/components/session/RegistrationTabs.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index 6737b92efa..2f5bc34e17 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -506,7 +506,7 @@ export class RegistrationTabs extends React.Component { private renderSignInButtons() { const { signInMode } = this.state; - const or = window.i18n('or'); + // const or = window.i18n('or'); if (signInMode === SignInMode.Default) { return ( @@ -519,7 +519,14 @@ export class RegistrationTabs extends React.Component { ); } - return <>; + return ( + + ); } private renderTermsConditionAgreement() { From ad1d54ad5e8cdf0ef871e358a1caf562d1c6b165 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 3 Feb 2021 11:48:54 +1100 Subject: [PATCH 017/109] force ubuntu-18.04 for github actions building on ubuntu 20.04 was causing issue with a too recent glibc for some users dependent on ubuntu 18.04 or similar (elementary OS) The issue arise as github action ubuntu-latest was upgraded recently too ubuntu 20.04. This fix forces the app to be built on ubuntu 18.04 Relates #1471 --- .github/workflows/build-binaries.yml | 2 +- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 77530ff140..45587fb2c4 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2016, macos-latest, ubuntu-latest] + os: [windows-2016, macos-latest, ubuntu-18.04] env: SIGNAL_ENV: production GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6e88e14350..78779acaa6 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2016, macos-latest, ubuntu-latest] + os: [windows-2016, macos-latest, ubuntu-18.04] env: SIGNAL_ENV: production GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69b325462d..3ea28d4a2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - os: [windows-2016, macos-latest, ubuntu-latest] + os: [windows-2016, macos-latest, ubuntu-18.04] env: SIGNAL_ENV: production GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9ffe8a84d3e8269d0f20cd02dcbb653258b070e4 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 3 Feb 2021 11:51:06 +1100 Subject: [PATCH 018/109] bump to v1.4.9 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 19a9a04770..3ee366c69f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.4.8", + "version": "1.4.9", "license": "GPL-3.0", "author": { "name": "Loki Project", @@ -333,4 +333,4 @@ "!dev-app-update.yml" ] } -} \ No newline at end of file +} From 608345f7f0d9acb0081b7e441a6abbdfa77500b4 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 3 Feb 2021 13:17:27 +1100 Subject: [PATCH 019/109] Revert "bump to v1.4.9" This reverts commit 258a7b83e88a5de10c3167c2939877d8ecef5f18. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3ee366c69f..19a9a04770 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.4.9", + "version": "1.4.8", "license": "GPL-3.0", "author": { "name": "Loki Project", @@ -333,4 +333,4 @@ "!dev-app-update.yml" ] } -} +} \ No newline at end of file From 0d3e515843f72ac74e4bc3562633052226579d50 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 5 Feb 2021 13:30:26 +1100 Subject: [PATCH 020/109] fix max size of closed group back to 100 --- preload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/preload.js b/preload.js index 2061fc19df..35bf3bcebf 100644 --- a/preload.js +++ b/preload.js @@ -94,7 +94,7 @@ window.CONSTANTS = new (function () { this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); this.MAX_LINKED_DEVICES = 1; this.MAX_CONNECTION_DURATION = 5000; - this.CLOSED_GROUP_SIZE_LIMIT = 20; + this.CLOSED_GROUP_SIZE_LIMIT = 100; // Number of seconds to turn on notifications after reconnect/start of app this.NOTIFICATION_ENABLE_TIMEOUT_SECONDS = 10; this.SESSION_ID_LENGTH = 66; From 60afbe7b06ea4771b8220d40a4245807a1bc6f3d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 4 Feb 2021 09:27:40 +1100 Subject: [PATCH 021/109] fallback to es for moment when given locale is es-419 --- preload.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/preload.js b/preload.js index 35bf3bcebf..7e5741145e 100644 --- a/preload.js +++ b/preload.js @@ -350,20 +350,6 @@ const Signal = require('./js/modules/signal'); const i18n = require('./js/modules/i18n'); const Attachments = require('./app/attachments'); -const { locale } = config; -window.i18n = i18n.setup(locale, localeMessages); - -window.moment = require('moment'); - -window.moment.updateLocale(locale, { - relativeTime: { - s: window.i18n('timestamp_s'), - m: window.i18n('timestamp_m'), - h: window.i18n('timestamp_h'), - }, -}); -window.moment.locale(locale); - window.Signal = Signal.setup({ Attachments, userDataPath: app.getPath('userData'), @@ -418,6 +404,20 @@ window.seedNodeList = JSON.parse(config.seedNodeList); const { OnionAPI } = require('./ts/session/onions'); +const { locale } = config; +window.i18n = i18n.setup(locale, localeMessages); +// moment does not support es-419 correctly (and cause white screen on app start) +const localeForMoment = locale === 'es-419' ? 'es' : locale; + +window.moment.updateLocale(localeForMoment, { + relativeTime: { + s: window.i18n('timestamp_s'), + m: window.i18n('timestamp_m'), + h: window.i18n('timestamp_h'), + }, +}); +window.moment.locale(localeForMoment); + window.OnionAPI = OnionAPI; window.libsession = require('./ts/session'); From 65ed81e980d9e32d3ed3e2bcf6b1e9b8d9142e5b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 8 Feb 2021 14:04:34 +1100 Subject: [PATCH 022/109] enable explicit group updates on the sending side --- preload.js | 3 +- ts/receiver/closedGroups.ts | 3 + ts/session/group/index.ts | 131 ++---------------------------------- ts/window.d.ts | 1 - 4 files changed, 10 insertions(+), 128 deletions(-) diff --git a/preload.js b/preload.js index 7e5741145e..6224bfd2c8 100644 --- a/preload.js +++ b/preload.js @@ -61,7 +61,6 @@ window.lokiFeatureFlags = { useFileOnionRequests: true, useFileOnionRequestsV2: true, // more compact encoding of files in response onionRequestHops: 3, - useExplicitGroupUpdatesSending: false, }; if ( @@ -459,6 +458,7 @@ if (process.env.USE_STUBBED_NETWORK) { window.SwarmPolling = new SwarmPolling(); } + // eslint-disable-next-line no-extend-native,func-names Promise.prototype.ignore = function () { // eslint-disable-next-line more/no-then @@ -489,7 +489,6 @@ if (config.environment.includes('test-integration')) { useOnionRequests: false, useFileOnionRequests: false, useOnionRequestsV2: false, - useExplicitGroupUpdatesSending: false, }; /* eslint-disable global-require, import/no-extraneous-dependencies */ window.sinon = require('sinon'); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 1bebd4e7d5..2ba001321f 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -472,6 +472,9 @@ async function performIfValid( } if (groupUpdate.type === Type.UPDATE) { + window.log.warn( + 'Received a groupUpdate non explicit. This should not happen anymore.' + ); await handleUpdateClosedGroup(envelope, groupUpdate, convo); } else if (groupUpdate.type === Type.NAME_CHANGE) { await handleClosedGroupNameChanged(envelope, groupUpdate, convo); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index e498fb8ed9..8a2c76565a 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -121,111 +121,6 @@ export async function initiateGroupUpdate( expireTimer: convo.get('expireTimer'), }; - if (!window.lokiFeatureFlags.useExplicitGroupUpdatesSending) { - // we still don't send any explicit group updates for now - only the receiving side is enabled - const dbMessageAdded = await addUpdateMessage(convo, diff, 'outgoing'); - window.getMessageController().register(dbMessageAdded.id, dbMessageAdded); - // Check preconditions - const hexEncryptionKeyPair = await Data.getLatestClosedGroupEncryptionKeyPair( - groupId - ); - if (!hexEncryptionKeyPair) { - throw new Error("Couldn't get key pair for closed group"); - } - - const encryptionKeyPair = ECKeyPair.fromHexKeyPair(hexEncryptionKeyPair); - const removedMembers = diff.leavingMembers || []; - const newMembers = diff.joiningMembers || []; // joining members - const wasAnyUserRemoved = removedMembers.length > 0; - const ourPrimary = await UserUtils.getOurNumber(); - const isUserLeaving = removedMembers.includes(ourPrimary.key); - const isCurrentUserAdmin = convo - .get('groupAdmins') - ?.includes(ourPrimary.key); - const expireTimerToShare = groupDetails.expireTimer || 0; - - const admins = convo.get('groupAdmins') || []; - if (removedMembers.includes(admins[0]) && newMembers.length !== 0) { - throw new Error( - "Can't remove admin from closed group without removing everyone." - ); // Error.invalidClosedGroupUpdate - } - - if (isUserLeaving && newMembers.length !== 0) { - if (removedMembers.length !== 1 || newMembers.length !== 0) { - throw new Error( - "Can't remove self and add or remove others simultaneously." - ); - } - } - - // Send the update to the group - const mainClosedGroupUpdate = new ClosedGroupUpdateMessage({ - timestamp: Date.now(), - groupId, - name: groupName, - members, - identifier: dbMessageAdded.id || uuid(), - expireTimer: expireTimerToShare, - }); - - if (isUserLeaving) { - window.log.info( - `We are leaving the group ${groupId}. Sending our leaving message.` - ); - // sent the message to the group and once done, remove everything related to this group - window.SwarmPolling.removePubkey(groupId); - await getMessageQueue().sendToGroup(mainClosedGroupUpdate, async () => { - window.log.info( - `Leaving message sent ${groupId}. Removing everything related to this group.` - ); - await Data.removeAllClosedGroupEncryptionKeyPairs(groupId); - }); - } else { - // Send the group update, and only once sent, generate and distribute a new encryption key pair if needed - await getMessageQueue().sendToGroup(mainClosedGroupUpdate, async () => { - if (wasAnyUserRemoved && isCurrentUserAdmin) { - // we send the new encryption key only to members already here before the update - const membersNotNew = members.filter(m => !newMembers.includes(m)); - window.log.info( - `Sending group update: A user was removed from ${groupId} and we are the admin. Generating and sending a new EncryptionKeyPair` - ); - - await generateAndSendNewEncryptionKeyPair(groupId, membersNotNew); - } - }); - - if (newMembers.length) { - // Send closed group update messages to any new members individually - const newClosedGroupUpdate = new ClosedGroupNewMessage({ - timestamp: Date.now(), - name: groupName, - groupId, - admins, - members, - keypair: encryptionKeyPair, - identifier: dbMessageAdded.id || uuid(), - expireTimer: expireTimerToShare, - }); - - const promises = newMembers.map(async m => { - await ConversationController.getInstance().getOrCreateAndWait( - m, - 'private' - ); - const memberPubKey = PubKey.cast(m); - await getMessageQueue().sendToPubKey( - memberPubKey, - newClosedGroupUpdate - ); - }); - await Promise.all(promises); - } - } - - return; - } - if (diff.newName?.length) { const nameOnlyDiff: GroupDiff = { newName: diff.newName }; const dbMessageName = await addUpdateMessage( @@ -453,26 +348,12 @@ export async function leaveClosedGroup(groupId: string) { window.getMessageController().register(dbMessage.id, dbMessage); const existingExpireTimer = convo.get('expireTimer') || 0; // Send the update to the group - let ourLeavingMessage; - - if (window.lokiFeatureFlags.useExplicitGroupUpdatesSending) { - ourLeavingMessage = new ClosedGroupMemberLeftMessage({ - timestamp: Date.now(), - groupId, - identifier: dbMessage.id, - expireTimer: existingExpireTimer, - }); - } else { - const ourPubkey = await UserUtils.getOurNumber(); - ourLeavingMessage = new ClosedGroupUpdateMessage({ - timestamp: Date.now(), - groupId, - identifier: dbMessage.id, - expireTimer: existingExpireTimer, - name: convo.get('name'), - members: convo.get('members').filter(m => m !== ourPubkey.key), - }); - } + const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ + timestamp: Date.now(), + groupId, + identifier: dbMessage.id, + expireTimer: existingExpireTimer, + }); window.log.info( `We are leaving the group ${groupId}. Sending our leaving message.` diff --git a/ts/window.d.ts b/ts/window.d.ts index a3029a7ace..6374415654 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -64,7 +64,6 @@ declare global { useOnionRequestsV2: boolean; useFileOnionRequests: boolean; useFileOnionRequestsV2: boolean; - useExplicitGroupUpdatesSending: boolean; onionRequestHops: number; }; lokiFileServerAPI: LokiFileServerInstance; From b76ce0f2ff928202dd8f6d47f9cf4e8d30321c75 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 8 Feb 2021 16:18:36 +1100 Subject: [PATCH 023/109] update models to TS part2 --- _locales/en/messages.json | 4 --- _locales/fr/messages.json | 4 --- _locales/ru/messages.json | 4 --- js/modules/attachment_downloads.js | 2 +- js/modules/backup.js | 2 +- libtextsecure/account_manager.js | 2 +- preload.js | 14 ++++----- test/backup_test.js | 2 +- test/fixtures.js | 2 +- test/models/conversations_test.js | 14 ++++----- ts/components/conversation/Message.tsx | 3 +- .../conversation/message/MessageMetadata.tsx | 16 +++++----- ts/components/session/SessionInboxView.tsx | 31 +++++++++++-------- .../conversation/SessionCompositionBox.tsx | 5 --- ts/models/conversation.ts | 2 +- ts/receiver/closedGroups.ts | 3 +- ts/receiver/errors.ts | 1 - ts/receiver/queuedJob.ts | 20 ++++++------ ts/receiver/receiver.ts | 2 +- ts/session/constants.ts | 1 - ts/session/conversations/index.ts | 4 +-- ts/session/group/index.ts | 16 ++++++---- ts/session/messages/MessageController.ts | 4 ++- ts/session/utils/Toast.tsx | 4 --- ts/session/utils/User.ts | 4 +-- ts/window.d.ts | 3 +- 26 files changed, 76 insertions(+), 93 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d008c88737..eb3da2a750 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1251,10 +1251,6 @@ "message": "MMM D", "description": "Timestamp format string for displaying month and day (but not the year) of a date within the current year, ex: use 'MMM D' for 'Aug 8', or 'D MMM' for '8 Aug'." }, - "messageBodyTooLong": { - "message": "Message body is too long.", - "description": "Shown if the user tries to send more than MAX_MESSAGE_BODY_LENGTH chars" - }, "messageBodyMissing": { "message": "Please enter a message body.", "description": "Shown if the user tries to send an empty message (and no attachments)" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 1b68565bec..b31f5bf75d 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1519,10 +1519,6 @@ "message": "Votre horloge est désynchronisée. Merci de mettre à jour votre horloge et de réessayer.", "description": "Notifcation that user's clock is out of sync with Loki's servers." }, - "messageBodyTooLong": { - "message": "Corps de message trop long.", - "description": "Shown if the user tries to send more than 64kb of text" - }, "changeNickname": { "message": "Changer le surnom", "description": "Conversation menu option to change user nickname" diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 06233c3956..d44d669ffb 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1519,10 +1519,6 @@ "message": "Your clock is out of sync. Please update your clock and try again.", "description": "Notifcation that user's clock is out of sync with Loki's servers." }, - "messageBodyTooLong": { - "message": "Message body is too long.", - "description": "Shown if the user tries to send more than 64kb of text" - }, "changeNickname": { "message": "Change Nickname", "description": "Conversation menu option to change user nickname" diff --git a/js/modules/attachment_downloads.js b/js/modules/attachment_downloads.js index 88a38de385..a0ece355a3 100644 --- a/js/modules/attachment_downloads.js +++ b/js/modules/attachment_downloads.js @@ -1,4 +1,4 @@ -/* global Whisper, Signal, setTimeout, clearTimeout, getMessageController, NewReceiver */ +/* global Signal, setTimeout, clearTimeout, getMessageController, NewReceiver */ const { isNumber, omit } = require('lodash'); const getGuid = require('uuid/v4'); diff --git a/js/modules/backup.js b/js/modules/backup.js index 0675978ddf..833e80f913 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -198,7 +198,7 @@ async function importConversationsFromJSON(conversations, options) { ); // eslint-disable-next-line no-await-in-loop await window.Signal.Data.saveConversation(migrated, { - Conversation: Whisper.Conversation, + Conversation: window.Wh, }); } diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index ee94a13ad6..c6d699f4e3 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -170,7 +170,7 @@ this.dispatchEvent(new Event('registration')); }, validatePubKeyHex(pubKey) { - const c = new Whisper.Conversation({ + const c = new window.models.Conversation({ id: pubKey, type: 'private', }); diff --git a/preload.js b/preload.js index 6224bfd2c8..60b87b9f22 100644 --- a/preload.js +++ b/preload.js @@ -85,7 +85,7 @@ window.isBeforeVersion = (toCheck, baseVersion) => { }; // eslint-disable-next-line func-names -window.CONSTANTS = new (function () { +window.CONSTANTS = new (function() { this.MAX_LOGIN_TRIES = 3; this.MAX_PASSWORD_LENGTH = 64; this.MAX_USERNAME_LENGTH = 20; @@ -183,9 +183,6 @@ window.setPassword = (passPhrase, oldPhrase) => window.libsession = require('./ts/session'); -window.getMessageController = - window.libsession.Messages.MessageController.getInstance; - window.getConversationController = window.libsession.Conversations.ConversationController.getInstance; @@ -382,7 +379,7 @@ window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args); // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(() => { - window.nodeSetImmediate(() => { }); + window.nodeSetImmediate(() => {}); }, 1000); const { autoOrientImage } = require('./js/modules/auto_orient_image'); @@ -408,6 +405,8 @@ window.i18n = i18n.setup(locale, localeMessages); // moment does not support es-419 correctly (and cause white screen on app start) const localeForMoment = locale === 'es-419' ? 'es' : locale; +window.moment = require('moment'); + window.moment.updateLocale(localeForMoment, { relativeTime: { s: window.i18n('timestamp_s'), @@ -458,11 +457,10 @@ if (process.env.USE_STUBBED_NETWORK) { window.SwarmPolling = new SwarmPolling(); } - // eslint-disable-next-line no-extend-native,func-names -Promise.prototype.ignore = function () { +Promise.prototype.ignore = function() { // eslint-disable-next-line more/no-then - this.then(() => { }); + this.then(() => {}); }; if ( diff --git a/test/backup_test.js b/test/backup_test.js index 346d5d4995..97d8eeff5b 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -513,7 +513,7 @@ describe('Backup', () => { }; console.log({ conversation }); await window.Signal.Data.saveConversation(conversation, { - Conversation: Whisper.Conversation, + Conversation: window.models.Conversation.ConversationModel, }); console.log( diff --git a/test/fixtures.js b/test/fixtures.js index 1a0cd81629..fef03d72ed 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -235,7 +235,7 @@ Whisper.Fixtures = () => { Promise.all( this.map(async (convo) => { await window.Signal.Data.saveConversation(convo.attributes, { - Conversation: Whisper.Conversation, + Conversation: window.models.Conversation.ConversationModel, }); await Promise.all( diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index be5e7a41fc..203c47749e 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -30,7 +30,7 @@ describe('ConversationCollection', () => { // before(async () => { // const convo = new window.models.Conversation.ConversationCollection().add(attributes); // await window.Signal.Data.saveConversation(convo.attributes, { - // Conversation: Whisper.Conversation, + // Conversation: window.models.Conversation.ConversationModel, // }); // // const message = convo.messageCollection.add({ // // body: 'hello world', @@ -88,12 +88,12 @@ describe('ConversationCollection', () => { // }); // describe('when set to private', () => { // it('correctly validates hex numbers', () => { - // const regularId = new Whisper.Conversation({ + // const regularId = new window.models.Conversation.ConversationModel({ // type: 'private', // id: // '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); - // const invalidId = new Whisper.Conversation({ + // const invalidId = new window.models.Conversation.ConversationModel({ // type: 'private', // id: // 'j71d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', @@ -102,20 +102,20 @@ describe('ConversationCollection', () => { // assert.notOk(invalidId.isValid()); // }); // it('correctly validates length', () => { - // const regularId33 = new Whisper.Conversation({ + // const regularId33 = new window.models.Conversation.ConversationModel({ // type: 'private', // id: // '051d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); - // const regularId32 = new Whisper.Conversation({ + // const regularId32 = new window.models.Conversation.ConversationModel({ // type: 'private', // id: '1d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); - // const shortId = new Whisper.Conversation({ + // const shortId = new window.models.Conversation.ConversationModel({ // type: 'private', // id: '771d11d', // }); - // const longId = new Whisper.Conversation({ + // const longId = new window.models.Conversation.ConversationModel({ // type: 'private', // id: // '771d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94abaa', diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index ebe0319463..81c41f9c81 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -767,8 +767,7 @@ class MessageInner extends React.PureComponent { const regex = new RegExp(`@${PubKey.regexForPubkeys}`, 'g'); const mentions = (text ? text.match(regex) : []) as Array; const mentionMe = - mentions && - mentions.some(m => m.slice(1) === UserUtils.getOurPubKeyStrFromCache()); + mentions && mentions.some(m => UserUtils.isUsFromCache(m.slice(1))); const isIncoming = direction === 'incoming'; const shouldHightlight = mentionMe && isIncoming && isPublic; diff --git a/ts/components/conversation/message/MessageMetadata.tsx b/ts/components/conversation/message/MessageMetadata.tsx index ea1f62e7a3..f11b87b060 100644 --- a/ts/components/conversation/message/MessageMetadata.tsx +++ b/ts/components/conversation/message/MessageMetadata.tsx @@ -96,14 +96,14 @@ export const MessageMetadata = (props: Props) => { theme={theme} /> ) : ( - - )} + + )} { } private async fetchHandleMessageSentData(m: RawMessage | OpenGroupMessage) { - const msg = window.getMessageController().get(m.identifier); + // if a message was sent and this message was created after the last app restart, + // this message is still in memory in the MessageController + const msg = MessageController.getInstance().get(m.identifier); if (!msg || !msg.message) { - return null; + // otherwise, look for it in the database + // nobody is listening to this freshly fetched message .trigger calls + const dbMessage = await getMessageById(m.identifier, { + Message: MessageModel, + }); + + if (!dbMessage) { + return null; + } + return { msg: dbMessage }; } return { msg: msg.message }; @@ -158,23 +171,15 @@ export class SessionInboxView extends React.Component { (conversation: any) => conversation.cachedProps ); - const filledConversations = conversations.map(async (conv: any) => { - const messages = await MessageController.getInstance().getMessagesByKeyFromDb( - conv.id - ); - return { ...conv, messages }; + const filledConversations = conversations.map((conv: any) => { + return { ...conv, messages: [] }; }); const fullFilledConversations = await Promise.all(filledConversations); - console.warn('fullFilledConversations', fullFilledConversations); - const initialState = { conversations: { - conversationLookup: window.Signal.Util.makeLookup( - fullFilledConversations, - 'id' - ), + conversationLookup: makeLookup(fullFilledConversations, 'id'), }, user: { ourPrimary: window.storage.get('primaryDevicePubKey'), diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 71ee59a1d9..fb3b723c54 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -375,7 +375,6 @@ export class SessionCompositionBox extends React.Component { spellCheck={true} inputRef={this.textarea} disabled={!typingEnabled} - // maxLength={Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH} rows={1} style={sendMessageStyle} suggestionsPortalHost={this.container} @@ -827,10 +826,6 @@ export class SessionCompositionBox extends React.Component { } // Verify message length const msgLen = messagePlaintext?.length || 0; - // if (msgLen > Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH) { - // ToastUtils.pushMessageBodyTooLong(); - // return; - // } if (msgLen === 0 && this.props.stagedAttachments?.length === 0) { ToastUtils.pushMessageBodyMissing(); return; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 0b19c2638b..46bb00d233 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -428,7 +428,7 @@ export class ConversationModel extends Backbone.Model { serverId: any, serverTimestamp: any ) { - const registeredMessage = window.getMessageController().get(identifier); + const registeredMessage = MessageController.getInstance().get(identifier); if (!registeredMessage || !registeredMessage.message) { return null; diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 2ba001321f..60a29d6362 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -22,6 +22,7 @@ import { ECKeyPair } from './keypairs'; import { UserUtils } from '../session/utils'; import { ConversationModel } from '../models/conversation'; import _ from 'lodash'; +import { MessageController } from '../session/messages'; export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, @@ -733,7 +734,7 @@ export async function createClosedGroup( groupDiff, 'outgoing' ); - window.getMessageController().register(dbMessage.id, dbMessage); + MessageController.getInstance().register(dbMessage.id, dbMessage); // be sure to call this before sending the message. // the sending pipeline needs to know from GroupUtils when a message is for a medium group diff --git a/ts/receiver/errors.ts b/ts/receiver/errors.ts index dc51bf4b98..63515a6890 100644 --- a/ts/receiver/errors.ts +++ b/ts/receiver/errors.ts @@ -41,7 +41,6 @@ export async function onError(ev: any) { messageModel: message, }); - if (ev.confirm) { ev.confirm(); } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 8b04e3f1a3..2e3e166d98 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -8,6 +8,8 @@ import { StringUtils, UserUtils } from '../session/utils'; import { ConversationController } from '../session/conversations'; import { ConversationModel } from '../models/conversation'; import { MessageCollection, MessageModel } from '../models/message'; +import { MessageController } from '../session/messages'; +import { getMessageById } from '../../js/modules/data'; async function handleGroups( conversation: ConversationModel, @@ -87,7 +89,6 @@ async function copyFromQuotedMessage( quote: Quote, attemptCount: number = 1 ): Promise { - const { Whisper, getMessageController } = window; const { upgradeMessageSchema } = window.Signal.Migrations; const { Message: TypedMessage, Errors } = window.Signal.Types; @@ -129,7 +130,10 @@ async function copyFromQuotedMessage( window.log.info(`Found quoted message id: ${id}`); quote.referencedMessageNotFound = false; - const queryMessage = getMessageController().register(found.id, found); + const queryMessage = MessageController.getInstance().register( + found.id, + found + ); quote.text = queryMessage.get('body') || ''; if (attemptCount > 1) { @@ -526,14 +530,13 @@ export async function handleMessageJob( ourNumber ); } - const { Whisper, getMessageController } = window; const id = await message.commit(); message.set({ id }); window.Whisper.events.trigger('messageAdded', { conversationKey: conversation.id, messageModel: message, }); - getMessageController().register(message.id, message); + MessageController.getInstance().register(message.id, message); // Note that this can save the message again, if jobs were queued. We need to // call it after we have an id for this message, because the jobs refer back @@ -551,12 +554,9 @@ export async function handleMessageJob( // We go to the database here because, between the message save above and // the previous line's trigger() call, we might have marked all messages // unread in the database. This message might already be read! - const fetched = await window.Signal.Data.getMessageById( - message.get('id'), - { - Message: MessageModel, - } - ); + const fetched = await getMessageById(message.get('id'), { + Message: MessageModel, + }); const previousUnread = message.get('unread'); diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index ef2b90af6a..a2ef5ecee5 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -111,7 +111,7 @@ async function handleRequestDetail( // The message is for a medium size group if (options.conversationId) { - const ourNumber = textsecure.storage.user.getNumber(); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const senderIdentity = envelope.source; if (senderIdentity === ourNumber) { diff --git a/ts/session/constants.ts b/ts/session/constants.ts index 06c57392b5..9a0b53daaa 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -9,7 +9,6 @@ export const TTL_DEFAULT = { // User Interface export const CONVERSATION = { - // MAX_MESSAGE_BODY_LENGTH: 2000, DEFAULT_MEDIA_FETCH_COUNT: 50, DEFAULT_DOCUMENTS_FETCH_COUNT: 150, DEFAULT_MESSAGE_FETCH_COUNT: 30, diff --git a/ts/session/conversations/index.ts b/ts/session/conversations/index.ts index 6830585b70..6694847e4f 100644 --- a/ts/session/conversations/index.ts +++ b/ts/session/conversations/index.ts @@ -101,7 +101,7 @@ export class ConversationController { try { await window.Signal.Data.saveConversation(conversation.attributes, { - Conversation: window.Whisper.Conversation, + Conversation: ConversationModel, }); } catch (error) { window.log.error( @@ -229,7 +229,7 @@ export class ConversationController { await conversation.destroyMessages(); await window.Signal.Data.removeConversation(id, { - Conversation: window.Whisper.Conversation, + Conversation: ConversationModel, }); conversation.off('change', this.updateReduxConvoChanged); this.conversations.remove(conversation); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 8a2c76565a..8867961ef5 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -26,6 +26,7 @@ import { import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; import { MessageModelType } from '../../models/messageType'; +import { MessageController } from '../messages'; export interface GroupInfo { id: string; @@ -100,6 +101,8 @@ export async function initiateGroupUpdate( throw new Error('Legacy group are not supported anymore.'); } + // do not give an admins field here. We don't want to be able to update admins and + // updateOrCreateClosedGroup() will update them if given the choice. const groupDetails = { id: groupId, name: groupName, @@ -128,7 +131,7 @@ export async function initiateGroupUpdate( nameOnlyDiff, 'outgoing' ); - window.getMessageController().register(dbMessageName.id, dbMessageName); + MessageController.getInstance().register(dbMessageName.id, dbMessageName); await sendNewName(convo, diff.newName, dbMessageName.id); } @@ -139,7 +142,7 @@ export async function initiateGroupUpdate( joiningOnlyDiff, 'outgoing' ); - window.getMessageController().register(dbMessageAdded.id, dbMessageAdded); + MessageController.getInstance().register(dbMessageAdded.id, dbMessageAdded); await sendAddedMembers( convo, diff.joiningMembers, @@ -155,9 +158,10 @@ export async function initiateGroupUpdate( leavingOnlyDiff, 'outgoing' ); - window - .getMessageController() - .register(dbMessageLeaving.id, dbMessageLeaving); + MessageController.getInstance().register( + dbMessageLeaving.id, + dbMessageLeaving + ); const stillMembers = members; await sendRemovedMembers( convo, @@ -345,7 +349,7 @@ export async function leaveClosedGroup(groupId: string) { received_at: now, expireTimer: 0, }); - window.getMessageController().register(dbMessage.id, dbMessage); + MessageController.getInstance().register(dbMessage.id, dbMessage); const existingExpireTimer = convo.get('expireTimer') || 0; // Send the update to the group const ourLeavingMessage = new ClosedGroupMemberLeftMessage({ diff --git a/ts/session/messages/MessageController.ts b/ts/session/messages/MessageController.ts index b9ca30491a..0aa6c9031e 100644 --- a/ts/session/messages/MessageController.ts +++ b/ts/session/messages/MessageController.ts @@ -51,7 +51,9 @@ export class MessageController { } public cleanup() { - window.log.warn('Cleaning up getMessageController() oldest messages...'); + window.log.warn( + 'Cleaning up MessageController singleton oldest messages...' + ); const now = Date.now(); (this.messageLookup || []).forEach(messageEntry => { diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx index 0ffcf7a59e..50570251d4 100644 --- a/ts/session/utils/Toast.tsx +++ b/ts/session/utils/Toast.tsx @@ -116,10 +116,6 @@ export function pushMaximumAttachmentsError() { pushToastError('maximumAttachments', window.i18n('maximumAttachments')); } -export function pushMessageBodyTooLong() { - pushToastError('messageBodyTooLong', window.i18n('messageBodyTooLong')); -} - export function pushMessageBodyMissing() { pushToastError('messageBodyMissing', window.i18n('messageBodyMissing')); } diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index ac27091b49..ab3478d7b4 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -12,15 +12,13 @@ export type HexKeyPair = { /** * Check if this pubkey is us, using the cache. + * Throws an error if our pubkey is not set */ export function isUsFromCache(pubKey: string | PubKey | undefined): boolean { if (!pubKey) { throw new Error('pubKey is not set'); } const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - if (!ourNumber) { - throw new Error('ourNumber is not set'); - } const pubKeyStr = pubKey instanceof PubKey ? pubKey.key : pubKey; return pubKeyStr === ourNumber; } diff --git a/ts/window.d.ts b/ts/window.d.ts index 6374415654..afc4dafe29 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -12,7 +12,7 @@ import { LibTextsecure } from '../libtextsecure'; import { ConversationType } from '../js/modules/data'; import { RecoveryPhraseUtil } from '../libloki/modules/mnemonic'; import { ConfirmationDialogParams } from '../background'; -import { } from 'styled-components/cssprop'; +import {} from 'styled-components/cssprop'; import { ConversationControllerType } from '../js/ConversationController'; import { any } from 'underscore'; @@ -36,7 +36,6 @@ declare global { LokiFileServerAPI: any; LokiPublicChatAPI: any; LokiSnodeAPI: any; - getMessageController: () => MessageController; Session: any; Signal: SignalInterface; StringView: any; From 5ec9722e002fd89b113a694e9e152a566dd2614c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 8 Feb 2021 17:16:17 +1100 Subject: [PATCH 024/109] autobind typescript class methods with autobind --- AUDRICTOCLEAN.txt | 5 --- libtextsecure/account_manager.js | 10 ------ package.json | 1 + ts/models/conversation.ts | 47 ++--------------------------- ts/models/message.ts | 9 ++++-- ts/receiver/queuedJob.ts | 3 +- ts/state/selectors/conversations.ts | 30 +++++++++++------- yarn.lock | 5 +++ 8 files changed, 35 insertions(+), 75 deletions(-) diff --git a/AUDRICTOCLEAN.txt b/AUDRICTOCLEAN.txt index 97cdd874cb..b8b1465a9d 100644 --- a/AUDRICTOCLEAN.txt +++ b/AUDRICTOCLEAN.txt @@ -25,8 +25,3 @@ ReadSyncs SyncMessage sendSyncMessage needs to be rewritten sendSyncMessageOnly to fix - - - - -LONG_ATTAHCMENNT diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index c6d699f4e3..4c2f5da7e4 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -169,16 +169,6 @@ this.dispatchEvent(new Event('registration')); }, - validatePubKeyHex(pubKey) { - const c = new window.models.Conversation({ - id: pubKey, - type: 'private', - }); - const validationError = c.validateNumber(); - if (validationError) { - throw new Error(validationError); - } - }, }); textsecure.AccountManager = AccountManager; })(); diff --git a/package.json b/package.json index 19a9a04770..8a597300e8 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/react-mic": "^12.4.1", "@types/styled-components": "^5.1.4", "abort-controller": "3.0.0", + "auto-bind": "^4.0.0", "backbone": "1.3.3", "blob-util": "1.3.0", "blueimp-canvas-to-blob": "3.14.0", diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 46bb00d233..2fa60363be 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -21,6 +21,7 @@ import { SignalService } from '../protobuf'; import { MessageCollection, MessageModel } from './message'; import * as Data from '../../js/modules/data'; import { MessageAttributesOptionals } from './messageType'; +import autoBind from 'auto-bind'; export interface OurLokiProfile { displayName: string; @@ -152,6 +153,7 @@ export class ConversationModel extends Backbone.Model { this.messageCollection = new MessageCollection([], { conversation: this, }); + autoBind(this); this.throttledBumpTyping = _.throttle(this.bumpTyping, 300); this.updateLastMessage = _.throttle( @@ -177,8 +179,6 @@ export class ConversationModel extends Backbone.Model { if (this.isPublic()) { this.set('profileSharing', true); } - this.unset('hasFetchedProfile'); - this.unset('tokens'); this.typingRefreshTimer = null; this.typingPauseTimer = null; @@ -549,49 +549,6 @@ export class ConversationModel extends Backbone.Model { return window.Signal.Data.getUnreadCountByConversation(this.id); } - public validate(attributes: any) { - const required = ['id', 'type']; - const missing = _.filter(required, attr => !attributes[attr]); - if (missing.length) { - return `Conversation must have ${missing}`; - } - - if (attributes.type !== 'private' && attributes.type !== 'group') { - return `Invalid conversation type: ${attributes.type}`; - } - - const error = this.validateNumber(); - if (error) { - return error; - } - - return null; - } - - public validateNumber() { - if (!this.id) { - return 'Invalid ID'; - } - if (!this.isPrivate()) { - return null; - } - - // Check if it's hex - const isHex = this.id.replace(/[\s]*/g, '').match(/^[0-9a-fA-F]+$/); - if (!isHex) { - return 'Invalid Hex ID'; - } - - // Check if the pubkey length is 33 and leading with 05 or of length 32 - const len = this.id.length; - if ((len !== 33 * 2 || !/^05/.test(this.id)) && len !== 32 * 2) { - return 'Invalid Pubkey Format'; - } - - this.set({ id: this.id }); - return null; - } - public queueJob(callback: any) { // tslint:disable-next-line: no-promise-as-boolean const previous = this.pending || Promise.resolve(); diff --git a/ts/models/message.ts b/ts/models/message.ts index 8a1bb1e277..6db4f44275 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -20,6 +20,7 @@ import { MessageAttributesOptionals, } from './messageType'; +import autoBind from 'auto-bind'; export class MessageModel extends Backbone.Model { public propsForTimerNotification: any; public propsForGroupNotification: any; @@ -45,6 +46,8 @@ export class MessageModel extends Backbone.Model { this.on('change:expireTimer', this.setToExpire); // this.on('expired', this.onExpired); void this.setToExpire(); + autoBind(this); + this.markRead = this.markRead.bind(this); // Keep props ready const generateProps = (triggerEvent = true) => { if (this.isExpirationTimerUpdate()) { @@ -95,7 +98,7 @@ export class MessageModel extends Backbone.Model { return !!this.get('unread'); } - // Important to allow for this.unset('unread'), save to db, then fetch() + // Important to allow for this.set({ unread}), save to db, then fetch() // to propagate. We don't want the unset key in the db so our unread index // stays small. public merge(model: any) { @@ -103,7 +106,7 @@ export class MessageModel extends Backbone.Model { const { unread } = attributes; if (unread === undefined) { - this.unset('unread'); + this.set({ unread: false }); } this.set(attributes); @@ -1323,7 +1326,7 @@ export class MessageModel extends Backbone.Model { } public async markRead(readAt: number) { - this.unset('unread'); + this.set({ unread: false }); if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { const expirationStartTimestamp = Math.min( diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 2e3e166d98..92ab4d9d63 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -294,7 +294,8 @@ function updateReadStatus( } } if (readSync || message.isExpirationTimerUpdate()) { - message.unset('unread'); + message.set({ unread: false }); + // This is primarily to allow the conversation to mark all older // messages as read, as is done when we receive a read sync for // a message we already know about. diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 20f5d85da3..aa0e5200af 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -1,6 +1,5 @@ import { createSelector } from 'reselect'; -import { LocalizerType } from '../../types/Util'; import { StateType } from '../reducer'; import { ConversationLookupType, @@ -11,6 +10,7 @@ import { import { getIntl, getOurNumber } from './user'; import { BlockedNumberController } from '../../util'; +import { LocalizerType } from '../../types/Util'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -49,13 +49,15 @@ export const getMessagesOfSelectedConversation = createSelector( (state: ConversationsStateType): Array => state.messages ); -function getConversationTitle(conversation: ConversationType): string { +function getConversationTitle( + conversation: ConversationType, + i18n: LocalizerType +): string { if (conversation.name) { return conversation.name; } if (conversation.type === 'group') { - const { i18n } = window; return i18n('unknown'); } return conversation.id; @@ -65,19 +67,25 @@ const collator = new Intl.Collator(); export const _getConversationComparator = (i18n: LocalizerType) => { return (left: ConversationType, right: ConversationType): number => { - const leftTimestamp = left.timestamp; - const rightTimestamp = right.timestamp; - if (leftTimestamp && !rightTimestamp) { + const leftActiveAt = left.activeAt; + const rightActiveAt = right.activeAt; + if (leftActiveAt && !rightActiveAt) { return -1; } - if (rightTimestamp && !leftTimestamp) { + if (rightActiveAt && !leftActiveAt) { return 1; } - if (leftTimestamp && rightTimestamp && leftTimestamp !== rightTimestamp) { - return rightTimestamp - leftTimestamp; + if (leftActiveAt && rightActiveAt && leftActiveAt !== rightActiveAt) { + return rightActiveAt - leftActiveAt; } - const leftTitle = getConversationTitle(left).toLowerCase(); - const rightTitle = getConversationTitle(right).toLowerCase(); + const leftTitle = getConversationTitle( + left, + i18n || window?.i18n + ).toLowerCase(); + const rightTitle = getConversationTitle( + right, + i18n || window?.i18n + ).toLowerCase(); return collator.compare(leftTitle, rightTitle); }; diff --git a/yarn.lock b/yarn.lock index f759664d2f..4e3cb28b36 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1308,6 +1308,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +auto-bind@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" + integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== + autoprefixer@^6.3.1: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" From 6edcb887882b5ec51c235ce7eac6469a9a96d352 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 9 Feb 2021 11:38:11 +1100 Subject: [PATCH 025/109] remove resend as it does not make any sense with medium groups and sendAnyway --- _locales/ar/messages.json | 4 ---- _locales/bg/messages.json | 4 ---- _locales/ca/messages.json | 4 ---- _locales/cs/messages.json | 4 ---- _locales/da/messages.json | 4 ---- _locales/de/messages.json | 4 ---- _locales/el/messages.json | 4 ---- _locales/en/messages.json | 4 ---- _locales/eo/messages.json | 4 ---- _locales/es/messages.json | 4 ---- _locales/es_419/messages.json | 4 ---- _locales/et/messages.json | 4 ---- _locales/fa/messages.json | 4 ---- _locales/fi/messages.json | 4 ---- _locales/fr/messages.json | 4 ---- _locales/he/messages.json | 4 ---- _locales/hi/messages.json | 4 ---- _locales/hr/messages.json | 4 ---- _locales/hu/messages.json | 4 ---- _locales/id/messages.json | 4 ---- _locales/it/messages.json | 4 ---- _locales/ja/messages.json | 4 ---- _locales/km/messages.json | 4 ---- _locales/kn/messages.json | 4 ---- _locales/ko/messages.json | 4 ---- _locales/lt/messages.json | 4 ---- _locales/mk/messages.json | 4 ---- _locales/nb/messages.json | 4 ---- _locales/nl/messages.json | 4 ---- _locales/nn/messages.json | 4 ---- _locales/no/messages.json | 4 ---- _locales/pl/messages.json | 4 ---- _locales/pt_BR/messages.json | 4 ---- _locales/pt_PT/messages.json | 4 ---- _locales/ro/messages.json | 4 ---- _locales/ru/messages.json | 4 ---- _locales/sk/messages.json | 4 ---- _locales/sl/messages.json | 4 ---- _locales/sq/messages.json | 4 ---- _locales/sr/messages.json | 4 ---- _locales/sv/messages.json | 4 ---- _locales/th/messages.json | 4 ---- _locales/tr/messages.json | 4 ---- _locales/uk/messages.json | 6 +----- _locales/vi/messages.json | 4 ---- _locales/zh_CN/messages.json | 4 ---- _locales/zh_TW/messages.json | 4 ---- stylesheets/_modules.scss | 14 -------------- stylesheets/_theme_dark.scss | 5 ----- ts/components/conversation/MessageDetail.tsx | 13 ------------- .../session/conversation/SessionMessagesList.tsx | 6 ------ 51 files changed, 1 insertion(+), 227 deletions(-) diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index 050c860049..8e98f44605 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "أرسل على كل حال", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "رقم الآمان الخاص بك مع $name$ قد تغير وغير متحقق منه. انقر للعرض.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 5236ce1894..00a88f4f8a 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Изпрати все пак", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Вашият номер за сигурност с $name$ е променен и все още не е потвърден. Натиснете за преглед.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/ca/messages.json b/_locales/ca/messages.json index 0cf8e26708..964efb23e3 100644 --- a/_locales/ca/messages.json +++ b/_locales/ca/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Tot i així, envia'l", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "El número de seguretat amb $name$ ha canviat i ja no està verificat. Feu clic per veure'l.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 1f60359edd..791dcce603 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Přesto poslat", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Bezpečnostní číslo $name$ bylo změněno a nebylo ověřeno. Klikněte pro zobrazení.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/da/messages.json b/_locales/da/messages.json index 1f291d88e7..be1bc62f46 100644 --- a/_locales/da/messages.json +++ b/_locales/da/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send alligevel", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Dit sikkerhedsnummer med $name$ har ændret sig og er ikke længere verificeret. Klik for at vise.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/de/messages.json b/_locales/de/messages.json index cc088f205a..1774c694fa 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -324,10 +324,6 @@ } } }, - "sendAnyway": { - "message": "Trotzdem senden", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Deine Sicherheitsnummer mit $name$ hat sich geändert und ist nicht mehr verifiziert. Zum Anzeigen anklicken.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 26b3f003bf..ee5be6d430 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Αποστολή παρ'όλα αυτά", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Ο αριθμός ασφαλείας σας με τον/την $name$ έχει αλλάξει και δεν είναι πλέον επιβεβαιωμένος. Κάντε κλικ για εμφάνιση.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/en/messages.json b/_locales/en/messages.json index eb3da2a750..d52d18610e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -414,10 +414,6 @@ }, "androidKey": "WebRtcCallScreen_you_may_wish_to_verify_this_contact" }, - "sendAnyway": { - "message": "Send Anyway", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Your safety number with $name$ has changed and is no longer verified. Click to show.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json index 01f5ea24a1..a8682b5965 100644 --- a/_locales/eo/messages.json +++ b/_locales/eo/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Tamen sendi", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Via sekuriga numero kun $name$ ŝanĝiĝis kaj ne plu estas konfirmita. Alklaku por montri ĝin.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/es/messages.json b/_locales/es/messages.json index ed3dcafd87..07022ba9c6 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Enviar de todos modos", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Las cifras de seguridad con $name$ han cambiado y ya no están verificadas. Haz clic para mostrar.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/es_419/messages.json b/_locales/es_419/messages.json index 617750ddf3..36ced7177d 100644 --- a/_locales/es_419/messages.json +++ b/_locales/es_419/messages.json @@ -303,10 +303,6 @@ } } }, - "sendAnyway": { - "message": "Enviar de Cualquier Forma", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Tu número de seguridad con $name$ ha cambiado y ya no está verificado. Presiona para mostrar.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/et/messages.json b/_locales/et/messages.json index 67e1cb454a..cfaa009622 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Saada ikkagi", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Sinu turvanumber kasutajaga $name$ on muutunud ja ei ole enam kinnitatud. Klõpsa nägemiseks.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/fa/messages.json b/_locales/fa/messages.json index d97f7dd911..623abcff56 100644 --- a/_locales/fa/messages.json +++ b/_locales/fa/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "به هر حال بفرست", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "کد امنیتی $name$ تغییر کرده و دیگر تأییدشده نیست. برای نمایشش کلیک کنید.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index 7e894b2c73..ad79daccf0 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Lähetä joka tapauksessa", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Turvanumerosi yhteystiedon $name$ kanssa on vaihtunut eikä se ole enää varmennettu. Näytä napsauttamalla.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index b31f5bf75d..1fad04b32f 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Envoyer quand même", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Votre numéro de sécurité avec $name$ a changé et n’est plus vérifié. Cliquez pour l’afficher.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 13c7b4d3f0..51dedea197 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "שלח בכל זאת", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "מספר הבטיחות שלך עם $name$ השתנה ואינו מוֻדא יותר. לחץ כדי לראות.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index 29e3c80f9d..65b20acf10 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "वैसे भी भेजें", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "आपकी सुरक्षा संख्या के साथ$name$बदल गया है और अब सत्यापित नहीं है। दिखने के लिए क्लिक करें।", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index 0f14bf506b..c4e787923f 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Svejedno pošalji", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Sigurnosni se broj s $name$ promijenio i više nije potvrđen. Kliknite za prikaz. ", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 1dbbeac275..8ee8cb30c4 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Küldés ennek ellenére", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Biztonsági számod $name$ nevű partnereddel megváltozott, ezért már nincs hitelesítve. Kattints a megjelenítéshez!", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/id/messages.json b/_locales/id/messages.json index 25c533fad1..b1fae34659 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Kirimkan Saja", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Nomor keamanan Anda dengan $name$ telah berubah dan tidak lagi diverifikasi. Klik untuk menampilkan.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 72b14f3cb1..16aec837d3 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Invia ugualmente", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Il tuo numero di sicurezza con $name$ è cambiato e non è più valido. Clicca per visualizzarlo.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 34005adbff..0aa0ce8be2 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "とにかく送信する", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "$name$との安全番号に変更があり,未検証です。クリックして表示してください。", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/km/messages.json b/_locales/km/messages.json index b662abcc41..7644466f45 100644 --- a/_locales/km/messages.json +++ b/_locales/km/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "ផ្ញើ ទោះយ៉ាងណាក៏ដោយ", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "លេខសុវត្ថិភាពរបស់អ្នកជាមួយ $name$ បានផ្លាស់ប្តូរ និងមិនត្រូវបានផ្ទៀងផ្ទាត់ឡើយ។ ចុច ដើម្បីបង្ហាញ។", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/kn/messages.json b/_locales/kn/messages.json index ba44ef8cd4..6bb700122e 100644 --- a/_locales/kn/messages.json +++ b/_locales/kn/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send Anyway", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Your safety number with $name$ has changed and is no longer verified. Click to show.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index cc2680dca5..dc7bd77c4a 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send Anyway", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Your safety number with $name$ has changed and is no longer verified. Click to show.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/lt/messages.json b/_locales/lt/messages.json index 14fa337b68..6fe8232a84 100644 --- a/_locales/lt/messages.json +++ b/_locales/lt/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Vis tiek siųsti", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Jūsų saugumo numeris su $name$ pasikeitė ir daugiau nebėra patvirtintas. Spustelėkite, norėdami rodyti.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/mk/messages.json b/_locales/mk/messages.json index 1e1e22a667..dc9c8c206f 100644 --- a/_locales/mk/messages.json +++ b/_locales/mk/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send Anyway", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Your safety number with $name$ has changed and is no longer verified. Click to show.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/nb/messages.json b/_locales/nb/messages.json index 7cc8df710a..570635838c 100644 --- a/_locales/nb/messages.json +++ b/_locales/nb/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send allikevel", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Ditt sikkerhetsnummer med $name$ er endret og er ikke lenger bekreftet. Klikk for å vise nummeret.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 0544ff37a6..d540b249d6 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Toch verzenden", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Je veiligheidsnummer met $name$ is veranderd en het is daarom niet langer geverifieerd. Klik om het te tonen.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/nn/messages.json b/_locales/nn/messages.json index 40716a3273..c3956c5ce7 100644 --- a/_locales/nn/messages.json +++ b/_locales/nn/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send likevel", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Tryggingsnummeret ditt med $name$ er endra og er ikkje lenger godkjent. Klikk for å visa nummeret.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/no/messages.json b/_locales/no/messages.json index cbec2e123d..d1226b011a 100644 --- a/_locales/no/messages.json +++ b/_locales/no/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send allikevel", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Ditt sikkerhetsnummer med $name$ er endret og er ikke lenger bekreftet. Klikk for å vise nummeret.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 77d2dcc51e..81da95a5d2 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Wyślij mimo wszystko", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Twój numer bezpieczeństwa z $name$ zmienił się. Naciśnij, aby zobaczyć.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index a95e0db4ab..47619cea5b 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Enviar Assim Mesmo", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "O seu número de segurança com$name$ mudou e não foi verificado. Clique para exibi-lo. ", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index 25cf867f4f..c80c270c5a 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Enviar mesmo assim", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "O número de segurança com $name$ mudou e não está verificado. Clique para mostrar.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index 2acd34d8df..c9fe218cb6 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Trimite oricum", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Numărul tău de siguranță cu $name$ s-a modificat și nu mai este verificat. Apasă pentru a afișa.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index d44d669ffb..a2bd8c18a9 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Отправить в любом случае", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Код безопасности с $name$ изменился и более не является подтвержденным. Нажмите, чтобы показать.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/sk/messages.json b/_locales/sk/messages.json index 72f9f0de1b..50a63acdf0 100644 --- a/_locales/sk/messages.json +++ b/_locales/sk/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Napriek tomu poslať", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Vaše bezpečnosné číslo s $name$ sa zmenilo, a už nie je overené. Kliknite pre zobrazenie. ", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/sl/messages.json b/_locales/sl/messages.json index 3bdd63fc47..cd9711dd19 100644 --- a/_locales/sl/messages.json +++ b/_locales/sl/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Vseeno pošlji", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Vaše varnostno število z uporabnikom $name$ je bilo spremenjeno in ni več označeno kot potrjeno. Kliknite za prikaz.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/sq/messages.json b/_locales/sq/messages.json index a2b32f9df9..cef46e4ae1 100644 --- a/_locales/sq/messages.json +++ b/_locales/sq/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Dërgoje Sido Qoftë", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Numri juaj i sigurisë për me $name$ ka ndryshuar dhe s’është më i verifikuar. Klikoni që të shfaqet.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 3a627d302c..6d0d5b3bea 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Пошаљи свакако", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Ваша шифра за $name$ је промењена и није више проверена. Притисните да би сте приказали.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index cd6d21e01a..fb6148cfef 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Skicka ändå", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Your safety number with $name$ has changed and is no longer verified. Click to show.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/th/messages.json b/_locales/th/messages.json index 19b88bc73f..e29aefe0c2 100644 --- a/_locales/th/messages.json +++ b/_locales/th/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "ยืนยันที่จะส่ง", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "มีการเปลี่ยนแปลงในรหัสความปลอดภัยของคุณกับ $name$ และถือว่ายังไม่ได้รับการยืนยัน คลิกเพื่อแสดง", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 6d3b9d8bf7..520fe15188 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Yine de Gönder", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "$name$ ile olan güvenlik numaranız değişti ve henüz doğrulanmadı. Göstermek için tıklayın.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json index c55c35da68..a043fcb5b1 100644 --- a/_locales/uk/messages.json +++ b/_locales/uk/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Усе одно надіслати", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "$name$ та ви тепер використовуєте новий код безпеки. Натисніть, щоб подивитися.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", @@ -1165,4 +1161,4 @@ } } } -} +} \ No newline at end of file diff --git a/_locales/vi/messages.json b/_locales/vi/messages.json index d5d754c5c3..007b471640 100644 --- a/_locales/vi/messages.json +++ b/_locales/vi/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "Send Anyway", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "Your safety number with $name$ has changed and is no longer verified. Click to show.", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 916c500e07..d913bc1f4e 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "仍要发送", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "您与 $name$ 的安全号码已发生改变,而且不再被确认。点击展示。", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 44656abfe8..e145bf82b5 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -311,10 +311,6 @@ } } }, - "sendAnyway": { - "message": "無論如何都送出", - "description": "Used on a warning dialog to make it clear that it might be risky to send the message." - }, "noLongerVerified": { "message": "你與 $name$ 的安全已經產生變動,並且不再是經驗證過的。點擊來顯示。", "description": "Shown in converation banner when user's safety number has changed, but they were previously verified.", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e8f92107f8..81bd1f3060 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -945,20 +945,6 @@ @include color-svg('../images/unidentified-delivery.svg', $color-gray-60); } -.module-message-detail__contact__error-buttons { - text-align: right; -} - -.module-message-detail__contact__send-anyway { - @include button-reset; - color: $color-white; - background-color: $session-color-danger; - margin-inline-start: 5px; - margin-top: 5px; - padding: 4px; - border-radius: 4px; -} - // Module: Media Gallery .module-media-gallery { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index e0504973e2..a48008594f 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -309,11 +309,6 @@ color: $session-color-danger; } - .module-message-detail__contact__send-anyway { - color: $color-white; - background-color: $session-color-danger; - } - // Module: Media Gallery .module-media-gallery__tab { diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 3f7c77f5ea..e30b984b46 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -17,8 +17,6 @@ interface Contact { isOutgoingKeyError: boolean; errors?: Array; - - onSendAnyway: () => void; } interface Props { @@ -70,16 +68,6 @@ export class MessageDetail extends React.Component { const { i18n } = window; const errors = contact.errors || []; - const errorComponent = contact.isOutgoingKeyError ? ( -
- -
- ) : null; const statusComponent = !contact.isOutgoingKeyError ? (
{
))}
- {errorComponent} {statusComponent} ); diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 5a9bbb75be..c80703b03d 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -66,8 +66,6 @@ export class SessionMessagesList extends React.Component { this.getScrollOffsetBottomPx = this.getScrollOffsetBottomPx.bind(this); this.displayUnreadBannerIndex = this.displayUnreadBannerIndex.bind(this); - this.onSendAnyway = this.onSendAnyway.bind(this); - this.messageContainerRef = this.props.messageContainerRef; this.ignoreScrollEvents = true; } @@ -592,8 +590,4 @@ export class SessionMessagesList extends React.Component { const clientHeight = messageContainer.clientHeight; return scrollHeight - scrollTop - clientHeight; } - - private async onSendAnyway({ contact, message }: any) { - message.resend(contact.id); - } } From ea2c4437a39bee95d6ad1194abbe8ada0b920b9e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 9 Feb 2021 11:40:32 +1100 Subject: [PATCH 026/109] cleanup models with unused events also, sort message from DB and on redux by sent_at or received_at when not a public group --- app/sql.js | 4 +- js/modules/loki_app_dot_net_api.d.ts | 1 + js/read_syncs.js | 5 - ts/models/conversation.ts | 134 ++++++----------- ts/models/message.ts | 136 ++++++------------ ts/models/messageType.ts | 2 + ts/session/conversations/index.ts | 2 +- ts/session/group/index.ts | 4 +- .../outgoing/content/data/group/index.ts | 1 - ts/state/ducks/conversations.ts | 24 +++- ts/state/selectors/conversations.ts | 2 +- ts/test/state/selectors/conversations_test.ts | 15 +- ts/test/test-utils/utils/message.ts | 2 +- 13 files changed, 120 insertions(+), 212 deletions(-) diff --git a/app/sql.js b/app/sql.js index b9a1128ced..3d2b3b6886 100644 --- a/app/sql.js +++ b/app/sql.js @@ -2472,6 +2472,8 @@ async function getUnreadCountByConversation(conversationId) { } // Note: Sorting here is necessary for getting the last message (with limit 1) +// be sure to update the sorting order to sort messages on reduxz too (sortMessages + async function getMessagesByConversation( conversationId, { limit = 100, receivedAt = Number.MAX_VALUE, type = '%' } = {} @@ -2482,7 +2484,7 @@ async function getMessagesByConversation( conversationId = $conversationId AND received_at < $received_at AND type LIKE $type - ORDER BY serverTimestamp DESC, serverId DESC, sent_at DESC + ORDER BY serverTimestamp DESC, serverId DESC, sent_at DESC, received_at DESC LIMIT $limit; `, { diff --git a/js/modules/loki_app_dot_net_api.d.ts b/js/modules/loki_app_dot_net_api.d.ts index 1b5631d92b..fd52aa5035 100644 --- a/js/modules/loki_app_dot_net_api.d.ts +++ b/js/modules/loki_app_dot_net_api.d.ts @@ -24,6 +24,7 @@ export interface LokiAppDotNetServerInterface { } export interface LokiPublicChannelAPI { + banUser(source: string): Promise; getModerators: () => Promise>; serverAPI: any; deleteMessages(arg0: any[]); diff --git a/js/read_syncs.js b/js/read_syncs.js index 9e078f6e1c..c1f191c9e1 100644 --- a/js/read_syncs.js +++ b/js/read_syncs.js @@ -88,11 +88,6 @@ const force = true; await message.setToExpire(force); - - const conversation = message.getConversation(); - if (conversation) { - conversation.trigger('expiration-change', message); - } } this.remove(receipt); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 2fa60363be..718dba6590 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -11,7 +11,7 @@ import { ReadReceiptMessage, TypingMessage, } from '../session/messages/outgoing'; -import { ClosedGroupChatMessage } from '../session/messages/outgoing/content/data/group'; +import { ClosedGroupChatMessage } from '../session/messages/outgoing/content/data/group/ClosedGroupChatMessage'; import { OpenGroup, PubKey } from '../session/types'; import { ToastUtils, UserUtils } from '../session/utils'; import { BlockedNumberController } from '../util'; @@ -20,7 +20,7 @@ import { leaveClosedGroup } from '../session/group'; import { SignalService } from '../protobuf'; import { MessageCollection, MessageModel } from './message'; import * as Data from '../../js/modules/data'; -import { MessageAttributesOptionals } from './messageType'; +import { MessageAttributesOptionals, MessageModelType } from './messageType'; import autoBind from 'auto-bind'; export interface OurLokiProfile { @@ -160,15 +160,7 @@ export class ConversationModel extends Backbone.Model { this.bouncyUpdateLastMessage.bind(this), 1000 ); - // this.listenTo( - // this.messageCollection, - // 'add remove destroy', - // debouncedUpdateLastMessage - // ); // Listening for out-of-band data updates - this.on('delivered', this.updateAndMerge); - this.on('read', this.updateAndMerge); - this.on('expiration-change', this.updateAndMerge); this.on('expired', this.onExpired); this.on('ourAvatarChanged', avatar => @@ -370,21 +362,6 @@ export class ConversationModel extends Backbone.Model { } } - public async updateAndMerge(message: any) { - await this.updateLastMessage(); - - const mergeMessage = () => { - const existing = this.messageCollection.get(message.id); - if (!existing) { - return; - } - - existing.merge(message.attributes); - }; - - mergeMessage(); - } - public async onExpired(message: any) { await this.updateLastMessage(); @@ -439,16 +416,7 @@ export class ConversationModel extends Backbone.Model { await model.setServerTimestamp(serverTimestamp); return undefined; } - public addSingleMessage( - message: MessageAttributesOptionals, - setToExpire = true - ) { - const model = this.messageCollection.add(message, { merge: true }); - if (setToExpire) { - void model.setToExpire(); - } - return model; - } + public format() { return this.cachedProps; } @@ -673,17 +641,23 @@ export class ConversationModel extends Backbone.Model { conversationId: this.id, }); } - public async sendMessageJob(message: any) { + public async sendMessageJob(message: MessageModel) { try { const uploads = await message.uploadData(); const { id } = message; const expireTimer = this.get('expireTimer'); const destination = this.id; + const sentAt = message.get('sent_at'); + + if (!sentAt) { + throw new Error('sendMessageJob() sent_at must be set.'); + } + const chatMessage = new ChatMessage({ body: uploads.body, identifier: id, - timestamp: message.get('sent_at'), + timestamp: sentAt, attachments: uploads.attachments, expireTimer, preview: uploads.preview, @@ -696,7 +670,7 @@ export class ConversationModel extends Backbone.Model { const openGroupParams = { body: uploads.body, - timestamp: message.get('sent_at'), + timestamp: sentAt, group: openGroup, attachments: uploads.attachments, preview: uploads.preview, @@ -716,7 +690,7 @@ export class ConversationModel extends Backbone.Model { const groupInvitation = message.get('groupInvitation'); const groupInvitMessage = new GroupInvitationMessage({ identifier: id, - timestamp: message.get('sent_at'), + timestamp: sentAt, serverName: groupInvitation.name, channelId: groupInvitation.channelId, serverAddress: groupInvitation.address, @@ -767,6 +741,7 @@ export class ConversationModel extends Backbone.Model { this.clearTypingTimers(); const destination = this.id; + const isPrivate = this.isPrivate(); const expireTimer = this.get('expireTimer'); const recipients = this.getRecipients(); @@ -808,28 +783,16 @@ export class ConversationModel extends Backbone.Model { const attributes: MessageAttributesOptionals = { ...messageWithSchema, groupInvitation, - id: window.getGuid(), conversationId: this.id, + destination: isPrivate ? destination : undefined, }; - const model = this.addSingleMessage(attributes); - MessageController.getInstance().register(model.id, model); - - const id = await model.commit(); - model.set({ id }); + const model = await this.addSingleMessage(attributes); - if (this.isPrivate()) { - model.set({ destination }); - } if (this.isPublic()) { await model.setServerTimestamp(new Date().getTime()); } - window.Whisper.events.trigger('messageAdded', { - conversationKey: this.id, - messageModel: model, - }); - this.set({ lastMessage: model.getNotificationText(), lastMessageStatus: 'sending', @@ -912,7 +875,7 @@ export class ConversationModel extends Backbone.Model { public async updateExpirationTimer( providedExpireTimer: any, providedSource?: string, - receivedAt?: number, + receivedAt?: number, // is set if it comes from outside options: any = {} ) { let expireTimer = providedExpireTimer; @@ -936,6 +899,8 @@ export class ConversationModel extends Backbone.Model { source, }); + const isOutgoing = Boolean(receivedAt); + source = source || UserUtils.getOurPubKeyStrFromCache(); // When we add a disappearing messages notification to the conversation, we want it @@ -943,9 +908,8 @@ export class ConversationModel extends Backbone.Model { const timestamp = (receivedAt || Date.now()) - 1; this.set({ expireTimer }); - await this.commit(); - const message = new MessageModel({ + const messageAttributes = { // Even though this isn't reflected to the user, we want to place the last seen // indicator above it. We set it to 'unread' to trigger that placement. unread: true, @@ -961,23 +925,14 @@ export class ConversationModel extends Backbone.Model { fromGroupUpdate: options.fromGroupUpdate, }, expireTimer: 0, - type: 'incoming', - }); - - message.set({ destination: this.id }); - - if (message.isOutgoing()) { - message.set({ recipients: this.getRecipients() }); - } + type: isOutgoing ? 'outgoing' : ('incoming' as MessageModelType), + destination: this.id, + recipients: isOutgoing ? this.getRecipients() : undefined, + }; - const id = await message.commit(); - - message.set({ id }); - window.Whisper.events.trigger('messageAdded', { - conversationKey: this.id, - messageModel: message, - }); + const message = await this.addSingleMessage(messageAttributes); + // tell the UI this conversation was updated await this.commit(); // if change was made remotely, don't send it to the number/group @@ -991,7 +946,7 @@ export class ConversationModel extends Backbone.Model { } const expireUpdate = { - identifier: id, + identifier: message.id, timestamp, expireTimer, profileKey, @@ -1008,13 +963,16 @@ export class ConversationModel extends Backbone.Model { return message.sendSyncMessageOnly(expirationTimerMessage); } - if (this.get('type') === 'private') { + if (this.isPrivate()) { const expirationTimerMessage = new ExpirationTimerUpdateMessage( expireUpdate ); const pubkey = new PubKey(this.get('id')); await getMessageQueue().sendToPubKey(pubkey, expirationTimerMessage); } else { + window.log.warn( + 'TODO: Expiration update for closed groups are to be updated' + ); const expireUpdateForGroup = { ...expireUpdate, groupId: this.get('id'), @@ -1023,24 +981,12 @@ export class ConversationModel extends Backbone.Model { const expirationTimerMessage = new ExpirationTimerUpdateMessage( expireUpdateForGroup ); - // special case when we are the only member of a closed group - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - if ( - this.get('members').length === 1 && - this.get('members')[0] === ourNumber - ) { - return message.sendSyncMessageOnly(expirationTimerMessage); - } await getMessageQueue().sendToGroup(expirationTimerMessage); } return message; } - public isSearchable() { - return !this.get('left'); - } - public async commit() { await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: ConversationModel, @@ -1048,27 +994,33 @@ export class ConversationModel extends Backbone.Model { this.trigger('change', this); } - public async addMessage(messageAttributes: MessageAttributesOptionals) { + public async addSingleMessage( + messageAttributes: MessageAttributesOptionals, + setToExpire = true + ) { const model = new MessageModel(messageAttributes); const messageId = await model.commit(); model.set({ id: messageId }); + + if (setToExpire) { + await model.setToExpire(); + } + MessageController.getInstance().register(messageId, model); + window.Whisper.events.trigger('messageAdded', { conversationKey: this.id, messageModel: model, }); + return model; } public async leaveGroup() { - if (this.get('type') !== ConversationType.GROUP) { - window.log.error('Cannot leave a non-group conversation'); - return; - } - if (this.isMediumGroup()) { await leaveClosedGroup(this.id); } else { + window.log.error('Cannot leave a non-medium group conversation'); throw new Error( 'Legacy group are not supported anymore. You need to create this group again.' ); diff --git a/ts/models/message.ts b/ts/models/message.ts index 6db4f44275..956da58c93 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -11,7 +11,7 @@ import { DataMessage, OpenGroupMessage, } from '../../ts/session/messages/outgoing'; -import { ClosedGroupChatMessage } from '../../ts/session/messages/outgoing/content/data/group'; +import { ClosedGroupChatMessage } from '../../ts/session/messages/outgoing/content/data/group/ClosedGroupChatMessage'; import { EncryptionType, PubKey } from '../../ts/session/types'; import { ToastUtils, UserUtils } from '../../ts/session/utils'; import { @@ -41,9 +41,6 @@ export class MessageModel extends Backbone.Model { ); } - this.on('destroy', this.onDestroy); - this.on('change:expirationStartTimestamp', this.setToExpire); - this.on('change:expireTimer', this.setToExpire); // this.on('expired', this.onExpired); void this.setToExpire(); autoBind(this); @@ -674,7 +671,9 @@ export class MessageModel extends Backbone.Model { ? [this.get('source')] : _.union( this.get('sent_to') || [], - this.get('recipients') || this.getConversation().getRecipients() + this.get('recipients') || + this.getConversation()?.getRecipients() || + [] ); // This will make the error message for outgoing key errors a bit nicer @@ -750,8 +749,20 @@ export class MessageModel extends Backbone.Model { resolve: async () => { const source = this.get('source'); const conversation = this.getConversation(); + if (!conversation) { + window.log.info( + 'cannot ban user, the corresponding conversation was not found.' + ); + return; + } const channelAPI = await conversation.getPublicSendData(); + if (!channelAPI) { + window.log.info( + 'cannot ban user, the corresponding channelAPI was not found.' + ); + return; + } const success = await channelAPI.banUser(source); if (success) { @@ -805,7 +816,8 @@ export class MessageModel extends Backbone.Model { const conversation = this.getConversation(); const openGroup = - conversation && conversation.isPublic() && conversation.toOpenGroup(); + (conversation && conversation.isPublic() && conversation.toOpenGroup()) || + undefined; const { AttachmentUtils } = Utils; const [attachments, preview, quote] = await Promise.all([ @@ -836,9 +848,12 @@ export class MessageModel extends Backbone.Model { await this.commit(); try { const conversation = this.getConversation(); - const intendedRecipients = this.get('recipients') || []; - const successfulRecipients = this.get('sent_to') || []; - const currentRecipients = conversation.getRecipients(); + if (!conversation) { + window.log.info( + 'cannot retry send message, the corresponding conversation was not found.' + ); + return; + } if (conversation.isPublic()) { const openGroup = { @@ -858,19 +873,8 @@ export class MessageModel extends Backbone.Model { return getMessageQueue().sendToGroup(openGroupMessage); } - let recipients = _.intersection(intendedRecipients, currentRecipients); - recipients = recipients.filter( - key => !successfulRecipients.includes(key) - ); - - if (!recipients.length) { - window.log.warn('retrySend: Nobody to send to!'); - - return this.commit(); - } - const { body, attachments, preview, quote } = await this.uploadData(); - const ourNumber = window.storage.get('primaryDevicePubKey'); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const ourConversation = ConversationController.getInstance().get( ourNumber ); @@ -893,86 +897,33 @@ export class MessageModel extends Backbone.Model { const chatMessage = new ChatMessage(chatParams); // Special-case the self-send case - we send only a sync message - if (recipients.length === 1) { - const isOurDevice = UserUtils.isUsFromCache(recipients[0]); - if (isOurDevice) { - return this.sendSyncMessageOnly(chatMessage); - } + if (conversation.isMe()) { + return this.sendSyncMessageOnly(chatMessage); } if (conversation.isPrivate()) { - const [number] = recipients; - const recipientPubKey = new PubKey(number); + return getMessageQueue().sendToPubKey( + PubKey.cast(conversation.id), + chatMessage + ); + } - return getMessageQueue().sendToPubKey(recipientPubKey, chatMessage); + // Here, the convo is neither an open group, a private convo or ourself. It can only be a medium group. + // For a medium group, retry send only means trigger a send again to all recipients + // as they are all polling from the same group swarm pubkey + if (!conversation.isMediumGroup()) { + throw new Error( + 'We should only end up with a medium group here. Anything else is an error' + ); } - // TODO should we handle medium groups message here too? - // Not sure there is the concept of retrySend for those const closedGroupChatMessage = new ClosedGroupChatMessage({ identifier: this.id, chatMessage, groupId: this.get('conversationId'), }); - // Because this is a partial group send, we send the message with the groupId field set, but individually - // to each recipient listed - return Promise.all( - recipients.map(async r => { - const recipientPubKey = new PubKey(r); - return getMessageQueue().sendToPubKey( - recipientPubKey, - closedGroupChatMessage - ); - }) - ); - } catch (e) { - await this.saveErrors(e); - return null; - } - } - - // Called when the user ran into an error with a specific user, wants to send to them - public async resend(number: string) { - const error = this.removeOutgoingErrors(number); - if (!error) { - window.log.warn('resend: requested number was not present in errors'); - return null; - } - try { - const { body, attachments, preview, quote } = await this.uploadData(); - - const chatMessage = new ChatMessage({ - identifier: this.id, - body, - timestamp: this.get('sent_at') || Date.now(), - expireTimer: this.get('expireTimer'), - attachments, - preview, - quote, - }); - - // Special-case the self-send case - we send only a sync message - if (UserUtils.isUsFromCache(number)) { - return this.sendSyncMessageOnly(chatMessage); - } - - const conversation = this.getConversation(); - const recipientPubKey = new PubKey(number); - - if (conversation.isPrivate()) { - return getMessageQueue().sendToPubKey(recipientPubKey, chatMessage); - } - - const closedGroupChatMessage = new ClosedGroupChatMessage({ - chatMessage, - groupId: this.get('conversationId'), - }); - // resend tries to send the message to that specific user only in the context of a closed group - return getMessageQueue().sendToPubKey( - recipientPubKey, - closedGroupChatMessage - ); + return getMessageQueue().sendToGroup(closedGroupChatMessage); } catch (e) { await this.saveErrors(e); return null; @@ -1085,9 +1036,7 @@ export class MessageModel extends Backbone.Model { await this.commit(); - this.getConversation().updateLastMessage(); - - this.trigger('sent', this); + this.getConversation()?.updateLastMessage(); } public async handleMessageSentFailure(sentMessage: any, error: any) { @@ -1113,8 +1062,7 @@ export class MessageModel extends Backbone.Model { }); await this.commit(); - this.getConversation().updateLastMessage(); - this.trigger('done'); + this.getConversation()?.updateLastMessage(); } public getConversation() { diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index ea7f20f9b4..d954a59a57 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -13,6 +13,8 @@ export type MessageDeliveryStatus = | 'error'; export interface MessageAttributes { + // the id of the message + // this can have several uses: id: string; source: string; quote?: any; diff --git a/ts/session/conversations/index.ts b/ts/session/conversations/index.ts index 6694847e4f..6fc35493de 100644 --- a/ts/session/conversations/index.ts +++ b/ts/session/conversations/index.ts @@ -51,7 +51,7 @@ export class ConversationController { ); } // Needed for some model setup which happens during the initial fetch() call below - public getUnsafe(id: string) { + public getUnsafe(id: string): ConversationModel | undefined { return this.conversations.get(id); } diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 4c4a063ec9..df1da67161 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -194,7 +194,7 @@ export async function addUpdateMessage( const unread = type === 'incoming'; - const message = await convo.addMessage({ + const message = await convo.addSingleMessage({ conversationId: convo.get('id'), type, sent_at: now, @@ -340,7 +340,7 @@ export async function leaveClosedGroup(groupId: string) { convo.set({ groupAdmins: admins }); await convo.commit(); - const dbMessage = await convo.addMessage({ + const dbMessage = await convo.addSingleMessage({ group_update: { left: 'You' }, conversationId: groupId, type: 'outgoing', diff --git a/ts/session/messages/outgoing/content/data/group/index.ts b/ts/session/messages/outgoing/content/data/group/index.ts index c16a052c16..636bc00019 100644 --- a/ts/session/messages/outgoing/content/data/group/index.ts +++ b/ts/session/messages/outgoing/content/data/group/index.ts @@ -1,4 +1,3 @@ -export * from './ClosedGroupChatMessage'; export * from './ClosedGroupEncryptionPairMessage'; export * from './ClosedGroupNewMessage'; export * from './ClosedGroupAddedMembersMessage'; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index f8c2440db7..5d78bd04f7 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -56,7 +56,6 @@ export interface ConversationType { index?: number; activeAt?: number; - timestamp: number; lastMessage?: { status: 'error' | 'sending' | 'sent' | 'delivered' | 'read'; text: string; @@ -443,15 +442,26 @@ function sortMessages( isPublic: boolean ): Array { // we order by serverTimestamp for public convos + // be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation if (isPublic) { return messages.sort( (a: any, b: any) => b.attributes.serverTimestamp - a.attributes.serverTimestamp ); } - return messages.sort( - (a: any, b: any) => b.attributes.timestamp - a.attributes.timestamp + if (messages.some(n => !n.attributes.sent_at && !n.attributes.received_at)) { + throw new Error('Found some messages without any timestamp set'); + } + + // for non public convos, we order by sent_at or received_at timestamp. + // we assume that a message has either a sent_at or a received_at field set. + const messagesSorted = messages.sort( + (a: any, b: any) => + (b.attributes.sent_at || b.attributes.received_at) - + (a.attributes.sent_at || a.attributes.received_at) ); + + return messagesSorted; } function handleMessageAdded( @@ -488,12 +498,13 @@ function handleMessageChanged( state: ConversationsStateType, action: MessageChangedActionType ) { + const { payload } = action; const messageInStoreIndex = state?.messages?.findIndex( - m => m.id === action.payload.id + m => m.id === payload.id ); if (messageInStoreIndex >= 0) { const changedMessage = _.pick( - action.payload as any, + payload as any, toPickFromMessageModel ) as MessageTypeInConvo; // we cannot edit the array directly, so slice the first part, insert our edited message, and slice the second part @@ -503,7 +514,10 @@ function handleMessageChanged( ...state.messages.slice(messageInStoreIndex + 1), ]; + const convo = state.conversationLookup[payload.get('conversationId')]; + const isPublic = convo?.isPublic || false; // reorder the messages depending on the timestamp (we might have an updated serverTimestamp now) + const sortedMessage = sortMessages(editedMessages, isPublic); const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeries( editedMessages ); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index aa0e5200af..9929f57e78 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -142,7 +142,7 @@ export const _getLeftPaneLists = ( } // Show loading icon while fetching messages - if (conversation.isPublic && !conversation.timestamp) { + if (conversation.isPublic && !conversation.activeAt) { conversation.lastMessage = { status: 'sending', text: '', diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts index 35fefeb07b..c814d829b8 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test/state/selectors/conversations_test.ts @@ -13,9 +13,8 @@ describe('state/selectors/conversations', () => { const data: ConversationLookupType = { id1: { id: 'id1', - activeAt: Date.now(), + activeAt: 0, name: 'No timestamp', - timestamp: 0, phoneNumber: 'notused', type: 'direct', @@ -30,9 +29,8 @@ describe('state/selectors/conversations', () => { }, id2: { id: 'id2', - activeAt: Date.now(), + activeAt: 20, name: 'B', - timestamp: 20, phoneNumber: 'notused', type: 'direct', @@ -47,9 +45,8 @@ describe('state/selectors/conversations', () => { }, id3: { id: 'id3', - activeAt: Date.now(), + activeAt: 20, name: 'C', - timestamp: 20, phoneNumber: 'notused', type: 'direct', @@ -64,9 +61,8 @@ describe('state/selectors/conversations', () => { }, id4: { id: 'id4', - activeAt: Date.now(), + activeAt: 20, name: 'Á', - timestamp: 20, phoneNumber: 'notused', type: 'direct', isMe: false, @@ -80,9 +76,8 @@ describe('state/selectors/conversations', () => { }, id5: { id: 'id5', - activeAt: Date.now(), + activeAt: 30, name: 'First!', - timestamp: 30, phoneNumber: 'notused', type: 'direct', isMe: false, diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index 84be09db10..cecc2f3088 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -5,7 +5,7 @@ import { import { v4 as uuid } from 'uuid'; import { OpenGroup } from '../../../session/types'; import { generateFakePubKey, generateFakePubKeys } from './pubkey'; -import { ClosedGroupChatMessage } from '../../../session/messages/outgoing/content/data/group'; +import { ClosedGroupChatMessage } from '../../../session/messages/outgoing/content/data/group/ClosedGroupChatMessage'; import { ConversationAttributes } from '../../../models/conversation'; export function generateChatMessage(identifier?: string): ChatMessage { From f41bf3151507b4a7f57fb2ac83f85107606be80d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 9 Feb 2021 11:49:55 +1100 Subject: [PATCH 027/109] fix tests --- js/modules/attachment_downloads.js | 6 +++--- js/modules/backup.js | 2 -- js/modules/data.js | 2 +- js/modules/types/message.js | 1 - js/views/update_group_dialog_view.js | 2 +- libtextsecure/account_manager.js | 1 - test/backup_test.js | 2 +- test/models/messages_test.js | 4 ++-- ts/components/session/ActionsPanel.tsx | 13 +++++++++++-- .../session/usingClosedConversationDetails.tsx | 2 +- ts/state/ducks/conversations.ts | 9 --------- 11 files changed, 20 insertions(+), 24 deletions(-) diff --git a/js/modules/attachment_downloads.js b/js/modules/attachment_downloads.js index a0ece355a3..2c2a99085e 100644 --- a/js/modules/attachment_downloads.js +++ b/js/modules/attachment_downloads.js @@ -1,4 +1,4 @@ -/* global Signal, setTimeout, clearTimeout, getMessageController, NewReceiver */ +/* global Signal, setTimeout, clearTimeout, getMessageController, NewReceiver, models */ const { isNumber, omit } = require('lodash'); const getGuid = require('uuid/v4'); @@ -143,7 +143,7 @@ async function _runJob(job) { } const found = await getMessageById(messageId, { - Message: window.models.Message.MessageModel, + Message: models.Message.MessageModel, }); if (!found) { logger.error('_runJob: Source message not found, deleting job'); @@ -227,7 +227,7 @@ async function _runJob(job) { async function _finishJob(message, id) { if (message) { await saveMessage(message.attributes, { - Message: window.models.Message.MessageModel, + Message: models.Message.MessageModel, }); const conversation = message.getConversation(); if (conversation) { diff --git a/js/modules/backup.js b/js/modules/backup.js index 833e80f913..6c8344469a 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -1,7 +1,5 @@ /* global Signal: false */ -/* global Whisper: false */ /* global _: false */ -/* global textsecure: false */ /* global i18n: false */ /* eslint-env browser */ diff --git a/js/modules/data.js b/js/modules/data.js index 2d6ce7cd56..2ebb9a8def 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -879,7 +879,7 @@ async function saveSeenMessageHash(data) { await channels.saveSeenMessageHash(_cleanData(data)); } -async function saveMessage(data, { forceSave, Message } = {}) { +async function saveMessage(data, { forceSave } = {}) { const id = await channels.saveMessage(_cleanData(data), { forceSave }); window.Whisper.ExpiringMessagesListener.update(); return id; diff --git a/js/modules/types/message.js b/js/modules/types/message.js index 926dab87df..fdb2209085 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -6,7 +6,6 @@ const SchemaVersion = require('./schema_version'); const { initializeAttachmentMetadata, } = require('../../../ts/types/message/initializeAttachmentMetadata'); -const MessageTS = require('../../../ts/types/Message'); const Contact = require('./contact'); const GROUP = 'group'; diff --git a/js/views/update_group_dialog_view.js b/js/views/update_group_dialog_view.js index cf6bb56f04..813c1c8132 100644 --- a/js/views/update_group_dialog_view.js +++ b/js/views/update_group_dialog_view.js @@ -1,4 +1,4 @@ -/* global Whisper, i18n, textsecure, _ */ +/* global Whisper, i18n, _ */ // eslint-disable-next-line func-names (function() { diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 4c2f5da7e4..49739aa2ad 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -9,7 +9,6 @@ dcodeIO, StringView, Event, - Whisper */ /* eslint-disable more/no-then */ diff --git a/test/backup_test.js b/test/backup_test.js index 97d8eeff5b..2625491fe4 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -1,4 +1,4 @@ -/* global Signal, Whisper, assert, textsecure, _, libsignal */ +/* global Signal, assert, textsecure, _, libsignal */ /* eslint-disable no-console */ diff --git a/test/models/messages_test.js b/test/models/messages_test.js index 8fa8a86522..3add5067dd 100644 --- a/test/models/messages_test.js +++ b/test/models/messages_test.js @@ -1,5 +1,3 @@ -/* global Whisper */ - 'use strict'; const attributes = { @@ -16,6 +14,8 @@ describe('MessageCollection', () => { before(async () => { await clearDatabase(); window.getConversationController().reset(); + window.textsecure.storage.user.getNumber = () => + '051111111111111111111111111111111111111111111111111111111111111111'; await window.getConversationController().load(); }); after(() => { diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index eec18a63ff..93a5f88295 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -148,7 +148,16 @@ class ActionsPanelPrivate extends React.Component { } public render(): JSX.Element { - const { selectedSection, unreadMessageCount } = this.props; + const { + selectedSection, + unreadMessageCount, + ourPrimaryConversation, + } = this.props; + + if (!ourPrimaryConversation) { + window.log.warn('ActionsPanel: ourPrimaryConversation is not set'); + return <>; + } const isProfilePageSelected = selectedSection === SectionType.Profile; const isMessagePageSelected = selectedSection === SectionType.Message; @@ -160,7 +169,7 @@ class ActionsPanelPrivate extends React.Component {
diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx index dc0d060987..0043a9fa2d 100644 --- a/ts/components/session/usingClosedConversationDetails.tsx +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -55,7 +55,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { (conversationType === 'group' || type === 'group' || isGroup) ) { const groupId = id || phoneNumber; - const ourPrimary = await UserUtils.getOurPubKeyFromCache(); + const ourPrimary = UserUtils.getOurPubKeyFromCache(); let members = await GroupUtils.getGroupMembers(PubKey.cast(groupId)); const ourself = members.find(m => m.key !== ourPrimary.key); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 5d78bd04f7..fe9fd0e8a7 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -614,15 +614,6 @@ export function reducer( return state; } - if (selectedConversation === id) { - // Inbox -> Archived: no conversation is selected - // Note: With today's stacked converastions architecture, this can result in weird - // behavior - no selected conversation in the left pane, but a conversation show - // in the right pane. - // if (!existing.isArchived && data.isArchived) { - // selectedConversation = undefined; - // } - } return { ...state, selectedConversation, From ed84760f0e4f2cffe17cf58f27ced38cea0c78d4 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 9 Feb 2021 17:00:54 +1100 Subject: [PATCH 028/109] add a tslint rule to forbid use of async without await --- ts/components/session/RegistrationTabs.tsx | 12 +++---- ts/components/session/SessionSeedModal.tsx | 4 +-- .../conversation/SessionCompositionBox.tsx | 4 +-- .../conversation/SessionConversation.tsx | 2 +- .../conversation/SessionMessagesList.tsx | 4 +-- .../session/conversation/SessionRecording.tsx | 10 +++--- .../conversation/SessionRightPanel.tsx | 22 ++++++------ ts/models/conversation.ts | 7 ++-- ts/receiver/queuedJob.ts | 2 +- ts/receiver/receiver.ts | 27 +++++++------- ts/session/group/index.ts | 8 ++--- ts/session/snode_api/swarmPolling.ts | 2 +- ts/test/session/integration/common.ts | 12 +++---- .../unit/crypto/MessageEncrypter_test.ts | 6 ++-- .../unit/receiving/ClosedGroupUpdates_test.ts | 19 +++------- .../session/unit/sending/MessageQueue_test.ts | 26 +++++++------- .../unit/sending/PendingMessageCache_test.ts | 3 +- ts/test/session/unit/utils/JobQueue_test.ts | 7 ++-- ts/test/session/unit/utils/Messages_test.ts | 6 ++-- ts/test/session/unit/utils/Promise_test.ts | 26 +++++++------- ts/test/session/unit/utils/String_test.ts | 36 +++++++++---------- ts/test/session/unit/utils/SyncUtils_test.ts | 13 +++---- ts/test/tslint.json | 5 --- ts/test/util/blockedNumberController_test.ts | 16 +++++---- tslint.json | 23 +----------- 25 files changed, 133 insertions(+), 169 deletions(-) diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index e4d3ce1b2b..0bf51680b5 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -131,8 +131,8 @@ export class RegistrationTabs extends React.Component { } public componentDidMount() { - this.generateMnemonicAndKeyPair().ignore(); - this.resetRegistration().ignore(); + void this.generateMnemonicAndKeyPair(); + void this.resetRegistration(); } public render() { @@ -345,7 +345,7 @@ export class RegistrationTabs extends React.Component { { if (signUpMode === SignUpMode.Default) { - this.onSignUpGenerateSessionIDClick().ignore(); + this.onSignUpGenerateSessionIDClick(); } else { this.onSignUpGetStartedClick(); } @@ -357,7 +357,7 @@ export class RegistrationTabs extends React.Component { ); } - private async onSignUpGenerateSessionIDClick() { + private onSignUpGenerateSessionIDClick() { this.setState( { signUpMode: SignUpMode.SessionIDShown, @@ -375,7 +375,7 @@ export class RegistrationTabs extends React.Component { } private onCompleteSignUpClick() { - this.register('english').ignore(); + void this.register('english'); } private renderSignIn() { @@ -541,7 +541,7 @@ export class RegistrationTabs extends React.Component { private handleContinueYourSessionClick() { if (this.state.signInMode === SignInMode.UsingRecoveryPhrase) { - this.register('english').ignore(); + void this.register('english'); } } diff --git a/ts/components/session/SessionSeedModal.tsx b/ts/components/session/SessionSeedModal.tsx index 0852a9411a..539894e68f 100644 --- a/ts/components/session/SessionSeedModal.tsx +++ b/ts/components/session/SessionSeedModal.tsx @@ -50,8 +50,8 @@ class SessionSeedModalInner extends React.Component { public render() { const i18n = window.i18n; - this.checkHasPassword(); - this.getRecoveryPhrase().ignore(); + void this.checkHasPassword(); + void this.getRecoveryPhrase(); const { onClose } = this.props; const { hasPassword, passwordValid } = this.state; diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index fb3b723c54..95f3906010 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -520,7 +520,7 @@ export class SessionCompositionBox extends React.Component { } if (firstLink !== this.state.stagedLinkPreview?.url) { // trigger fetching of link preview data and image - void this.fetchLinkPreview(firstLink); + this.fetchLinkPreview(firstLink); } // if the fetch did not start yet, just don't show anything @@ -553,7 +553,7 @@ export class SessionCompositionBox extends React.Component { return <>; } - private async fetchLinkPreview(firstLink: string) { + private fetchLinkPreview(firstLink: string) { // mark the link preview as loading, no data are set yet this.setState({ stagedLinkPreview: { diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 2cb68faa07..eba3c7e61f 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -824,7 +824,7 @@ export class SessionConversation extends React.Component { } } - private async showMessageDetails(messageProps: any) { + private showMessageDetails(messageProps: any) { messageProps.onDeleteMessage = async (id: string) => { await this.deleteMessagesById([id], false); this.setState({ messageDetailShowProps: undefined }); diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 3e84edc94e..a4a9a0c26e 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -41,7 +41,7 @@ interface Props { count: number; }) => void; replyToMessage: (messageId: number) => Promise; - showMessageDetails: (messageProps: any) => Promise; + showMessageDetails: (messageProps: any) => void; onClickAttachment: (attachment: any, message: any) => void; onDownloadAttachment: ({ attachment }: { attachment: any }) => void; onDeleteSelectedMessages: () => Promise; @@ -326,7 +326,7 @@ export class SessionMessagesList extends React.Component { messageProps.onReply = this.props.replyToMessage; messageProps.onShowDetail = async () => { const messageDetailsProps = await message.getPropsForMessageDetail(); - void this.props.showMessageDetails(messageDetailsProps); + this.props.showMessageDetails(messageDetailsProps); }; messageProps.onClickAttachment = (attachment: AttachmentType) => { diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx index 053e808d41..0657408654 100644 --- a/ts/components/session/conversation/SessionRecording.tsx +++ b/ts/components/session/conversation/SessionRecording.tsx @@ -122,9 +122,9 @@ class SessionRecordingInner extends React.Component { }; } - public async componentWillMount() { + public componentWillMount() { // This turns on the microphone on the system. Later we need to turn it off. - await this.initiateRecordingStream(); + this.initiateRecordingStream(); } public componentDidMount() { @@ -309,7 +309,7 @@ class SessionRecordingInner extends React.Component { } } - private async stopRecording() { + private stopRecording() { this.setState({ isRecording: false, isPaused: true, @@ -436,7 +436,7 @@ class SessionRecordingInner extends React.Component { this.props.sendVoiceMessage(audioBlob); } - private async initiateRecordingStream() { + private initiateRecordingStream() { navigator.getUserMedia( { audio: true }, this.onRecordingStream, @@ -462,7 +462,7 @@ class SessionRecordingInner extends React.Component { streamParams.stream.getTracks().forEach((track: any) => track.stop); // Stop recording - await this.stopRecording(); + this.stopRecording(); } private async onRecordingStream(stream: any) { diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 47164cd0b1..07c97d0cdd 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -69,23 +69,23 @@ class SessionRightPanel extends React.Component { } public componentWillMount() { - this.getMediaGalleryProps() - .then(({ documents, media, onItemClick }) => { + void this.getMediaGalleryProps().then( + ({ documents, media, onItemClick }) => { this.setState({ documents, media, onItemClick, }); - }) - .ignore(); + } + ); } public componentDidUpdate() { const mediaScanInterval = 1000; setTimeout(() => { - this.getMediaGalleryProps() - .then(({ documents, media, onItemClick }) => { + void this.getMediaGalleryProps().then( + ({ documents, media, onItemClick }) => { const { documents: oldDocs, media: oldMedias } = this.state; if ( oldDocs.length !== documents.length || @@ -97,8 +97,8 @@ class SessionRightPanel extends React.Component { onItemClick, }); } - }) - .ignore(); + } + ); }, mediaScanInterval); } @@ -193,7 +193,7 @@ class SessionRightPanel extends React.Component { } ); - const saveAttachment = async ({ attachment, message }: any = {}) => { + const saveAttachment = ({ attachment, message }: any = {}) => { const timestamp = message.received_at; save({ attachment, @@ -203,10 +203,10 @@ class SessionRightPanel extends React.Component { }); }; - const onItemClick = async ({ message, attachment, type }: any) => { + const onItemClick = ({ message, attachment, type }: any) => { switch (type) { case 'documents': { - saveAttachment({ message, attachment }).ignore(); + saveAttachment({ message, attachment }); break; } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 6605209e3f..a2619d3f51 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -422,7 +422,7 @@ export class ConversationModel extends Backbone.Model { if (!registeredMessage || !registeredMessage.message) { return null; } - const model = registeredMessage.message as MessageModel; + const model = registeredMessage.message; await model.setIsPublic(true); await model.setServerId(serverId); await model.setServerTimestamp(serverTimestamp); @@ -1298,7 +1298,10 @@ export class ConversationModel extends Backbone.Model { } } public async setSubscriberCount(count: number) { - this.set({ subscriberCount: count }); + if (this.get('subscriberCount') !== count) { + this.set({ subscriberCount: count }); + await this.commit(); + } // Not sure if we care about updating the database } public async setGroupNameAndAvatar(name: any, avatarPath: any) { diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index a30d2f2640..88152bf765 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -115,7 +115,7 @@ async function copyFromQuotedMessage( window.log.info( `Looking for the message id : ${id}, attempt: ${attemptCount + 1}` ); - copyFromQuotedMessage(msg, quote, attemptCount + 1).ignore(); + void copyFromQuotedMessage(msg, quote, attemptCount + 1); }, attemptCount * attemptCount * 500); } else { window.log.warn( diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index e4fc398037..3524477b7e 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -52,7 +52,7 @@ class EnvelopeQueue { // Last pending promise private pending: Promise = Promise.resolve(); - public async add(task: any): Promise { + public add(task: any): void { this.count += 1; const promise = this.pending.then(task, task); this.pending = promise; @@ -85,16 +85,16 @@ function queueEnvelope(envelope: EnvelopePlus) { `queueEnvelope ${id}` ); - const promise = envelopeQueue.add(taskWithTimeout); - - promise.catch((error: any) => { + try { + envelopeQueue.add(taskWithTimeout); + } catch (error) { window.log.error( 'queueEnvelope error handling envelope', id, ':', error && error.stack ? error.stack : error ); - }); + } } async function handleRequestDetail( @@ -154,10 +154,7 @@ async function handleRequestDetail( } } -export async function handleRequest( - body: any, - options: ReqOptions -): Promise { +export function handleRequest(body: any, options: ReqOptions): void { // tslint:disable-next-line no-promise-as-boolean const lastPromise = _.last(incomingMessagePromises) || Promise.resolve(); @@ -210,7 +207,7 @@ async function queueCached(item: any) { if (decrypted) { const payloadPlaintext = StringUtils.encode(decrypted, 'base64'); - await queueDecryptedEnvelope(envelope, payloadPlaintext); + queueDecryptedEnvelope(envelope, payloadPlaintext); } else { queueEnvelope(envelope); } @@ -236,7 +233,7 @@ async function queueCached(item: any) { } } -async function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer) { +function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer) { const id = getEnvelopeId(envelope); window.log.info('queueing decrypted envelope', id); @@ -245,14 +242,14 @@ async function queueDecryptedEnvelope(envelope: any, plaintext: ArrayBuffer) { task, `queueEncryptedEnvelope ${id}` ); - const promise = envelopeQueue.add(taskWithTimeout); - - return promise.catch(error => { + try { + envelopeQueue.add(taskWithTimeout); + } catch (error) { window.log.error( `queueDecryptedEnvelope error handling envelope ${id}:`, error && error.stack ? error.stack : error ); - }); + } } async function handleDecryptedEnvelope( diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 80bc582820..cb9a6e1e90 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -310,9 +310,7 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { } export async function leaveClosedGroup(groupId: string) { - const convo = ConversationController.getInstance().get( - groupId - ) as ConversationModel; + const convo = ConversationController.getInstance().get(groupId); if (!convo) { window.log.error('Cannot leave non-existing group'); @@ -334,7 +332,9 @@ export async function leaveClosedGroup(groupId: string) { } else { // otherwise, just the exclude ourself from the members and trigger an update with this convo.set({ left: true }); - members = (convo.get('members') || []).filter(m => m !== ourNumber.key); + members = (convo.get('members') || []).filter( + (m: string) => m !== ourNumber.key + ); admins = convo.get('groupAdmins') || []; } convo.set({ members }); diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index e5d32edf97..5ae433956d 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -52,7 +52,7 @@ export class SwarmPolling { this.lastHashes = {}; } - public async start(): Promise { + public start(): void { this.loadGroupIds(); void this.pollForAllKeys(); } diff --git a/ts/test/session/integration/common.ts b/ts/test/session/integration/common.ts index 0772ddfe85..0bb5e1d230 100644 --- a/ts/test/session/integration/common.ts +++ b/ts/test/session/integration/common.ts @@ -1,6 +1,6 @@ // tslint:disable: no-implicit-dependencies -import { Application, SpectronClient } from 'spectron'; +import { Application } from 'spectron'; import path from 'path'; import url from 'url'; import http from 'http'; @@ -73,7 +73,7 @@ export class Common { } public static async closeToast(app: Application) { - app.client.element(CommonPage.toastCloseButton).click(); + await app.client.element(CommonPage.toastCloseButton).click(); } // a wrapper to work around electron/spectron bug @@ -219,7 +219,7 @@ export class Common { env?: string; }) { const app = await Common.startAndAssureCleanedApp(env); - await Common.startStubSnodeServer(); + Common.startStubSnodeServer(); if (recoveryPhrase && displayName) { await Common.restoreFromRecoveryPhrase(app, recoveryPhrase, displayName); @@ -562,7 +562,7 @@ export class Common { return `Test message from integration tests ${Date.now()}`; } - public static async startStubSnodeServer() { + public static startStubSnodeServer() { if (!Common.stubSnode) { Common.messages = {}; Common.stubSnode = http.createServer((request: any, response: any) => { @@ -684,7 +684,7 @@ export class Common { public static async stopStubSnodeServer() { if (Common.stubSnode) { - Common.stubSnode.close(); + await Common.stubSnode.close(); Common.stubSnode = null; } } @@ -695,7 +695,7 @@ export class Common { * @param str the string to search (not regex) * Note: getRenderProcessLogs() clears the app logs each calls. */ - public static async logsContains( + public static logsContains( renderLogs: Array<{ message: string }>, str: string, count?: number diff --git a/ts/test/session/unit/crypto/MessageEncrypter_test.ts b/ts/test/session/unit/crypto/MessageEncrypter_test.ts index c05dfb4b0b..a3fcbc011c 100644 --- a/ts/test/session/unit/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/unit/crypto/MessageEncrypter_test.ts @@ -165,9 +165,9 @@ describe('MessageEncrypter', () => { .to.deep.equal(SignalService.Envelope.Type.UNIDENTIFIED_SENDER); }); - it('should throw an error for anything else than Fallback or ClosedGroup', async () => { + it('should throw an error for anything else than Fallback or ClosedGroup', () => { const data = crypto.randomBytes(10); - await expect( + expect( MessageEncrypter.encrypt( TestUtils.generateFakePubKey(), data, @@ -182,7 +182,7 @@ describe('MessageEncrypter', () => { describe('Session Protocol', () => { let sandboxSessionProtocol: sinon.SinonSandbox; - beforeEach(async () => { + beforeEach(() => { sandboxSessionProtocol = sinon.createSandbox(); sandboxSessionProtocol diff --git a/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts b/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts index 560b21c941..ca8224b266 100644 --- a/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts +++ b/ts/test/session/unit/receiving/ClosedGroupUpdates_test.ts @@ -1,22 +1,11 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression + import chai from 'chai'; -import * as sinon from 'sinon'; import _ from 'lodash'; import { describe } from 'mocha'; -import { GroupUtils, PromiseUtils, UserUtils } from '../../../../session/utils'; -import { TestUtils } from '../../../../test/test-utils'; -import { - generateEnvelopePlusClosedGroup, - generateGroupUpdateNameChange, -} from '../../../test-utils/utils/envelope'; -import { handleClosedGroupControlMessage } from '../../../../receiver/closedGroups'; -import { ConversationController } from '../../../../session/conversations'; - -// tslint:disable-next-line: no-require-imports no-var-requires no-implicit-dependencies -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); - -const { expect } = chai; +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); // tslint:disable-next-line: max-func-body-length describe('ClosedGroupUpdates', () => { diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index a7b5e4a85e..6dd33aa27f 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -1,3 +1,5 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression + import chai from 'chai'; import * as sinon from 'sinon'; import _ from 'lodash'; @@ -15,9 +17,8 @@ import { MessageSender } from '../../../../session/sending'; import { PendingMessageCacheStub } from '../../../test-utils/stubs'; import { ClosedGroupMessage } from '../../../../session/messages/outgoing/content/data/group/ClosedGroupMessage'; -// tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); const { expect } = chai; @@ -35,7 +36,7 @@ describe('MessageQueue', () => { // Message Sender Stubs let sendStub: sinon.SinonStub<[RawMessage, (number | undefined)?]>; - beforeEach(async () => { + beforeEach(() => { // Utils Stubs sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber); @@ -65,7 +66,8 @@ describe('MessageQueue', () => { messageQueueStub.events.once('sendSuccess', done); }); await messageQueueStub.processPending(device); - await expect(successPromise).to.be.fulfilled; + // tslint:disable-next-line: no-unused-expression + expect(successPromise).to.eventually.be.fulfilled; }); it('should remove message from cache', async () => { @@ -88,7 +90,7 @@ describe('MessageQueue', () => { const messages = await pendingMessageCache.getForDevice(device); return messages.length === 0; }); - await expect(promise).to.be.fulfilled; + return promise.should.be.fulfilled; } }).timeout(15000); @@ -105,7 +107,6 @@ describe('MessageQueue', () => { }); await messageQueueStub.processPending(device); - await expect(eventPromise).to.be.fulfilled; const rawMessage = await eventPromise; expect(rawMessage.identifier).to.equal(message.identifier); @@ -130,7 +131,6 @@ describe('MessageQueue', () => { }); await messageQueueStub.processPending(device); - await expect(eventPromise).to.be.fulfilled; const [rawMessage, error] = await eventPromise; expect(rawMessage.identifier).to.equal(message.identifier); @@ -166,7 +166,7 @@ describe('MessageQueue', () => { }); describe('sendToGroup', () => { - it('should throw an error if invalid non-group message was passed', async () => { + it('should throw an error if invalid non-group message was passed', () => { // const chatMessage = TestUtils.generateChatMessage(); // await expect( // messageQueueStub.sendToGroup(chatMessage) @@ -174,7 +174,7 @@ describe('MessageQueue', () => { // Cannot happen with typescript as this function only accept group message now }); - describe('closed groups', async () => { + describe('closed groups', () => { it('can send to closed group', async () => { const members = TestUtils.generateFakePubKeys(4).map( p => new PubKey(p.key) @@ -194,7 +194,7 @@ describe('MessageQueue', () => { ); }); - describe('open groups', async () => { + describe('open groups', () => { let sendToOpenGroupStub: sinon.SinonStub< [OpenGroupMessage], Promise<{ serverId: number; serverTimestamp: number }> @@ -223,7 +223,7 @@ describe('MessageQueue', () => { }, 2000); await messageQueueStub.sendToGroup(message); - await expect(eventPromise).to.be.fulfilled; + return eventPromise.should.be.fulfilled; }); it('should emit a fail event if something went wrong', async () => { @@ -234,7 +234,7 @@ describe('MessageQueue', () => { }, 2000); await messageQueueStub.sendToGroup(message); - await expect(eventPromise).to.be.fulfilled; + return eventPromise.should.be.fulfilled; }); }); }); diff --git a/ts/test/session/unit/sending/PendingMessageCache_test.ts b/ts/test/session/unit/sending/PendingMessageCache_test.ts index 7e493b33da..d3cc81f172 100644 --- a/ts/test/session/unit/sending/PendingMessageCache_test.ts +++ b/ts/test/session/unit/sending/PendingMessageCache_test.ts @@ -1,3 +1,4 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression import { expect } from 'chai'; import * as sinon from 'sinon'; import * as _ from 'lodash'; @@ -17,7 +18,7 @@ describe('PendingMessageCache', () => { let data: StorageItem; let pendingMessageCacheStub: PendingMessageCache; - beforeEach(async () => { + beforeEach(() => { // Stub out methods which touch the database const storageID = 'pendingMessages'; data = { diff --git a/ts/test/session/unit/utils/JobQueue_test.ts b/ts/test/session/unit/utils/JobQueue_test.ts index 1abb38b878..ecdc6e7fed 100644 --- a/ts/test/session/unit/utils/JobQueue_test.ts +++ b/ts/test/session/unit/utils/JobQueue_test.ts @@ -1,11 +1,12 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression + import chai from 'chai'; import { v4 as uuid } from 'uuid'; import { JobQueue } from '../../../../session/utils/JobQueue'; import { TestUtils } from '../../../test-utils'; -// tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); const { assert } = chai; diff --git a/ts/test/session/unit/utils/Messages_test.ts b/ts/test/session/unit/utils/Messages_test.ts index 57aaa699ea..d34e217bb7 100644 --- a/ts/test/session/unit/utils/Messages_test.ts +++ b/ts/test/session/unit/utils/Messages_test.ts @@ -17,9 +17,9 @@ import { import { MockConversation } from '../../../test-utils/utils'; import { ConfigurationMessage } from '../../../../session/messages/outgoing/content/ConfigurationMessage'; import { ConversationModel } from '../../../../models/conversation'; -// tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); + +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); const { expect } = chai; diff --git a/ts/test/session/unit/utils/Promise_test.ts b/ts/test/session/unit/utils/Promise_test.ts index 28ec57532c..1bc04261cb 100644 --- a/ts/test/session/unit/utils/Promise_test.ts +++ b/ts/test/session/unit/utils/Promise_test.ts @@ -1,11 +1,13 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression + import chai from 'chai'; import * as sinon from 'sinon'; import { PromiseUtils } from '../../../../session/utils'; // tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); const { expect } = chai; @@ -49,20 +51,20 @@ describe('Promise Utils', () => { }; const promise = PromiseUtils.poll(task, {}); + await promise; - await expect(promise).to.be.fulfilled; expect(pollSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(1); }); - it('can timeout a task', async () => { + it('can timeout a task', () => { // completionSpy will be called on done const completionSpy = sandbox.spy(); const task = (_done: any) => undefined; const promise = PromiseUtils.poll(task, { timeoutMs: 1 }); - await expect(promise).to.be.rejectedWith('Periodic check timeout'); + promise.should.eventually.be.rejectedWith('Periodic check timeout'); expect(pollSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(0); }); @@ -83,8 +85,8 @@ describe('Promise Utils', () => { }; const promise = PromiseUtils.poll(task, { timeoutMs: timeout, interval }); + await promise; - await expect(promise).to.be.fulfilled; expect(pollSpy.callCount).to.equal(1); expect(recurrenceSpy.callCount).to.equal(expectedRecurrences); }); @@ -103,19 +105,19 @@ describe('Promise Utils', () => { const promise = PromiseUtils.waitForTask(task); - await expect(promise).to.be.fulfilled; + await promise; expect(waitForTaskSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(1); }); - it('can timeout a task', async () => { + it('can timeout a task', () => { // completionSpy will be called on done const completionSpy = sandbox.spy(); const task = async (_done: any) => undefined; const promise = PromiseUtils.waitForTask(task, 1); - await expect(promise).to.be.rejectedWith('Task timed out.'); + promise.should.eventually.be.rejectedWith('Task timed out.'); expect(waitForTaskSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(0); }); @@ -125,16 +127,16 @@ describe('Promise Utils', () => { it('can wait for check', async () => { const check = () => true; const promise = PromiseUtils.waitUntil(check); + await promise; - await expect(promise).to.be.fulfilled; expect(waitUntilSpy.callCount).to.equal(1); }); - it('can timeout a check', async () => { + it('can timeout a check', () => { const check = () => false; const promise = PromiseUtils.waitUntil(check, 1); - await expect(promise).to.be.rejectedWith('Periodic check timeout'); + promise.should.eventually.be.rejectedWith('Periodic check timeout'); expect(waitUntilSpy.callCount).to.equal(1); }); }); diff --git a/ts/test/session/unit/utils/String_test.ts b/ts/test/session/unit/utils/String_test.ts index 5e42bf7315..503a9023bd 100644 --- a/ts/test/session/unit/utils/String_test.ts +++ b/ts/test/session/unit/utils/String_test.ts @@ -1,3 +1,4 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression import chai from 'chai'; import ByteBuffer from 'bytebuffer'; @@ -5,15 +6,14 @@ import ByteBuffer from 'bytebuffer'; import { Encoding } from '../../../../session/utils/String'; import { StringUtils } from '../../../../session/utils'; -// tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); const { expect } = chai; describe('String Utils', () => { describe('encode', () => { - it('can encode to base64', async () => { + it('can encode to base64', () => { const testString = 'AAAAAAAAAA'; const encoded = StringUtils.encode(testString, 'base64'); @@ -24,7 +24,7 @@ describe('String Utils', () => { expect(encoded.byteLength).to.be.greaterThan(0); }); - it('can encode to hex', async () => { + it('can encode to hex', () => { const testString = 'AAAAAAAAAA'; const encoded = StringUtils.encode(testString, 'hex'); @@ -35,14 +35,14 @@ describe('String Utils', () => { expect(encoded.byteLength).to.be.greaterThan(0); }); - it('wont encode invalid hex', async () => { + it('wont encode invalid hex', () => { const testString = 'ZZZZZZZZZZ'; const encoded = StringUtils.encode(testString, 'hex'); expect(encoded.byteLength).to.equal(0); }); - it('can encode to binary', async () => { + it('can encode to binary', () => { const testString = 'AAAAAAAAAA'; const encoded = StringUtils.encode(testString, 'binary'); @@ -53,7 +53,7 @@ describe('String Utils', () => { expect(encoded.byteLength).to.be.greaterThan(0); }); - it('can encode to utf8', async () => { + it('can encode to utf8', () => { const testString = 'AAAAAAAAAA'; const encoded = StringUtils.encode(testString, 'binary'); @@ -64,7 +64,7 @@ describe('String Utils', () => { expect(encoded.byteLength).to.be.greaterThan(0); }); - it('can encode empty string', async () => { + it('can encode empty string', () => { const testString = ''; expect(testString).to.have.length(0); @@ -81,7 +81,7 @@ describe('String Utils', () => { }); }); - it('can encode huge string', async () => { + it('can encode huge string', () => { const stringSize = Math.pow(2, 16); const testString = Array(stringSize) .fill('0') @@ -100,7 +100,7 @@ describe('String Utils', () => { }); }); - it("won't encode illegal string length in hex", async () => { + it("won't encode illegal string length in hex", () => { const testString = 'A'; const encode = () => StringUtils.encode(testString, 'hex'); @@ -109,7 +109,7 @@ describe('String Utils', () => { expect(encode).to.throw('Illegal str: Length not a multiple of 2'); }); - it('can encode obscure string', async () => { + it('can encode obscure string', () => { const testString = '↓←¶ᶑᵶ⅑⏕→⅓‎ᵹ⅙ᵰᶎ⅔⅗↔‌ᶈ⅞⸜ᶊᵴᶉ↉¥ᶖᶋᶃᶓ⏦ᵾᶂᶆ↕⸝ᶔᶐ⏔£⏙⅐⅒ᶌ⁁ᶘᶄᶒᶸ⅘‏⅚⅛ᶙᶇᶕᶀ↑ᵿ⏠ᶍᵯ⏖⏗⅜ᶚᶏ⁊‍ᶁᶗᵽᵼ⅝⏘⅖⅕⏡'; @@ -128,7 +128,7 @@ describe('String Utils', () => { }); describe('decode', () => { - it('can decode empty buffer', async () => { + it('can decode empty buffer', () => { const buffer = new ByteBuffer(0); const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array; @@ -143,7 +143,7 @@ describe('String Utils', () => { }); }); - it('can decode huge buffer', async () => { + it('can decode huge buffer', () => { const bytes = Math.pow(2, 16); const bufferString = Array(bytes) .fill('A') @@ -162,7 +162,7 @@ describe('String Utils', () => { }); }); - it('can decode from ByteBuffer', async () => { + it('can decode from ByteBuffer', () => { const buffer = ByteBuffer.fromUTF8('AAAAAAAAAA'); const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array; @@ -177,7 +177,7 @@ describe('String Utils', () => { }); }); - it('can decode from Buffer', async () => { + it('can decode from Buffer', () => { const arrayBuffer = new ArrayBuffer(10); const buffer = Buffer.from(arrayBuffer); buffer.writeUInt8(0, 0); @@ -194,7 +194,7 @@ describe('String Utils', () => { }); }); - it('can decode from ArrayBuffer', async () => { + it('can decode from ArrayBuffer', () => { const buffer = new ArrayBuffer(10); const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array; @@ -209,7 +209,7 @@ describe('String Utils', () => { }); }); - it('can decode from Uint8Array', async () => { + it('can decode from Uint8Array', () => { const buffer = new Uint8Array(10); const encodings = ['base64', 'hex', 'binary', 'utf8'] as Array; diff --git a/ts/test/session/unit/utils/SyncUtils_test.ts b/ts/test/session/unit/utils/SyncUtils_test.ts index d3c9517754..a5594b807c 100644 --- a/ts/test/session/unit/utils/SyncUtils_test.ts +++ b/ts/test/session/unit/utils/SyncUtils_test.ts @@ -1,15 +1,10 @@ +// tslint:disable: no-implicit-dependencies import chai from 'chai'; import * as sinon from 'sinon'; -import { ConversationController } from '../../../../session/conversations'; -import * as MessageUtils from '../../../../session/utils/Messages'; -import { syncConfigurationIfNeeded } from '../../../../session/utils/syncUtils'; -import { TestUtils } from '../../../test-utils'; import { restoreStubs } from '../../../test-utils/utils'; -// tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); -const { expect } = chai; +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); describe('SyncUtils', () => { const sandbox = sinon.createSandbox(); @@ -20,7 +15,7 @@ describe('SyncUtils', () => { }); describe('syncConfigurationIfNeeded', () => { - it('sync if last sync undefined', async () => { + it('sync if last sync undefined', () => { // TestUtils.stubData('getItemById').resolves(undefined); // sandbox.stub(ConversationController.getInstance(), 'getConversations').returns([]); // const getCurrentConfigurationMessageSpy = sandbox.spy(MessageUtils, 'getCurrentConfigurationMessage'); diff --git a/ts/test/tslint.json b/ts/test/tslint.json index d18bab5613..ebe01b7f13 100644 --- a/ts/test/tslint.json +++ b/ts/test/tslint.json @@ -2,15 +2,10 @@ "defaultSeverity": "error", "extends": ["../../tslint.json"], "rules": { - // To allow the use of devDependencies here "no-implicit-dependencies": false, - - // All tests use arrow functions, and they can be long "max-func-body-length": false, - "no-unused-expression": false, "no-unused-variable": false, - "await-promise": [true, "PromiseLike"], "no-floating-promises": [true, "PromiseLike"] } diff --git a/ts/test/util/blockedNumberController_test.ts b/ts/test/util/blockedNumberController_test.ts index 83a7302bab..f4d3dee3fe 100644 --- a/ts/test/util/blockedNumberController_test.ts +++ b/ts/test/util/blockedNumberController_test.ts @@ -1,3 +1,5 @@ +// tslint:disable: no-implicit-dependencies max-func-body-length no-unused-expression + import { expect } from 'chai'; import * as sinon from 'sinon'; import { BlockedNumberController } from '../../util/blockedNumberController'; @@ -35,7 +37,7 @@ describe('BlockedNumberController', () => { TestUtils.restoreStubs(); }); - describe('load', async () => { + describe('load', () => { it('should load data from the database', async () => { const normal = TestUtils.generateFakePubKey(); const group = TestUtils.generateFakePubKey(); @@ -62,7 +64,7 @@ describe('BlockedNumberController', () => { }); }); - describe('block', async () => { + describe('block', () => { it('should block the user', async () => { const other = TestUtils.generateFakePubKey(); @@ -76,7 +78,7 @@ describe('BlockedNumberController', () => { }); }); - describe('unblock', async () => { + describe('unblock', () => { it('should unblock the user', async () => { const primary = TestUtils.generateFakePubKey(); memoryDB.blocked = [primary.key]; @@ -103,7 +105,7 @@ describe('BlockedNumberController', () => { }); }); - describe('blockGroup', async () => { + describe('blockGroup', () => { it('should block a group', async () => { const group = TestUtils.generateFakePubKey(); @@ -118,7 +120,7 @@ describe('BlockedNumberController', () => { }); }); - describe('unblockGroup', async () => { + describe('unblockGroup', () => { it('should unblock a group', async () => { const group = TestUtils.generateFakePubKey(); const another = TestUtils.generateFakePubKey(); @@ -134,7 +136,7 @@ describe('BlockedNumberController', () => { }); }); - describe('isBlocked', async () => { + describe('isBlocked', () => { it('should return true if number is blocked', async () => { const pubKey = TestUtils.generateFakePubKey(); const groupPubKey = TestUtils.generateFakePubKey(); @@ -198,7 +200,7 @@ describe('BlockedNumberController', () => { }); }); - describe('isGroupBlocked', async () => { + describe('isGroupBlocked', () => { it('should return true if group is blocked', async () => { const pubKey = TestUtils.generateFakePubKey(); const groupPubKey = TestUtils.generateFakePubKey(); diff --git a/tslint.json b/tslint.json index 70bd8cdd77..9b974cd238 100644 --- a/tslint.json +++ b/tslint.json @@ -7,40 +7,31 @@ "align": false, "newline-per-chained-call": false, "array-type": [true, "generic"], - // Preferred by Prettier: "arrow-parens": [true, "ban-single-arg-parens"], - "import-spacing": false, "indent": [true, "spaces", 2], "interface-name": [true, "never-prefix"], - // Allows us to write inline `style`s. Revisit when we have a more sophisticated // CSS-in-JS solution: "jsx-no-multiline-js": false, - // We'll make tradeoffs where appropriate "jsx-no-lambda": false, "react-this-binding-issue": false, - "linebreak-style": [true, "LF"], - // Prettier handles this for us "max-line-length": false, - "mocha-avoid-only": true, // Disabled until we can allow dynamically generated tests: // https://github.com/Microsoft/tslint-microsoft-contrib/issues/85#issuecomment-371749352 "mocha-no-side-effect-code": false, "mocha-unneeded-done": true, - // We always want 'as Type' "no-angle-bracket-type-assertion": true, - "no-consecutive-blank-lines": [true, 2], "object-literal-key-quotes": [true, "as-needed"], "object-literal-sort-keys": false, - + "no-async-without-await": true, // Ignore import sources order until we can specify that we want ordering // based on import name vs module name: "ordered-imports": [ @@ -50,7 +41,6 @@ "named-imports-order": "case-insensitive" } ], - "quotemark": [ true, "single", @@ -58,10 +48,8 @@ "avoid-template", "avoid-escape" ], - // Preferred by Prettier: "semicolon": [true, "always", "ignore-bound-class-methods"], - // Preferred by Prettier: "trailing-comma": [ true, @@ -76,11 +64,8 @@ "esSpecCompliant": true } ], - // Disabling a large set of Microsoft-recommended rules - // Modifying: - // React components and namespaces are Pascal case "variable-name": [ true, @@ -88,7 +73,6 @@ "allow-leading-underscore", "allow-pascal-case" ], - "function-name": [ true, { @@ -99,12 +83,9 @@ "static-method-regex": "^[a-zA-Z][\\w\\d]+$" } ], - // Adding select dev dependencies here for now, may turn on all in the future "no-implicit-dependencies": [true, ["dashdash", "electron"]], - // Maybe will turn on: - // We're not trying to be comprehensive with JSDoc right now. We have the style guide. "completed-docs": false, // Today we have files with a single named export which isn't the filename. Eventually. @@ -121,9 +102,7 @@ "no-unsafe-any": false, // Not everything needs to be typed right now "typedef": false, - // Probably won't turn on: - "possible-timing-attack": false, // We use null "no-null-keyword": false, From 8eb1507fcfa221ab4406ed2649480de25ea9c9a7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 10 Feb 2021 09:30:56 +1100 Subject: [PATCH 029/109] fix tests with should() --- ts/test/session/unit/sending/MessageQueue_test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index 6dd33aa27f..3de4c73a98 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -19,6 +19,7 @@ import { ClosedGroupMessage } from '../../../../session/messages/outgoing/conten import chaiAsPromised from 'chai-as-promised'; chai.use(chaiAsPromised as any); +chai.should(); const { expect } = chai; From 850233bc9e2188e9a846d3d21e874b4d63164b13 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 10 Feb 2021 10:31:48 +1100 Subject: [PATCH 030/109] review PR --- js/modules/backup.js | 2 +- js/modules/data.d.ts | 6 +++--- .../conversation/SessionCompositionBox.tsx | 2 +- ts/receiver/queuedJob.ts | 2 +- .../unit/crypto/MessageEncrypter_test.ts | 12 +++++------- ts/test/session/unit/utils/Promise_test.ts | 17 +++++++++++------ 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/js/modules/backup.js b/js/modules/backup.js index 6c8344469a..d3f5bae5b6 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -196,7 +196,7 @@ async function importConversationsFromJSON(conversations, options) { ); // eslint-disable-next-line no-await-in-loop await window.Signal.Data.saveConversation(migrated, { - Conversation: window.Wh, + Conversation: window.models.Conversation.ConversationModel, }); } diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts index 71aea19c9b..d81863f44a 100644 --- a/js/modules/data.d.ts +++ b/js/modules/data.d.ts @@ -1,4 +1,5 @@ import { KeyPair } from '../../libtextsecure/libsignal-protocol'; +import { MessageCollection } from '../../ts/models/message'; import { HexKeyPair } from '../../ts/receiver/closedGroups'; import { PubKey } from '../../ts/session/types'; import { ConversationType } from '../../ts/state/ducks/conversations'; @@ -242,8 +243,7 @@ export function getUnreadByConversation( { MessageCollection }?: any ): Promise; export function getUnreadCountByConversation( - conversationId: string, - { MessageCollection }?: any + conversationId: string ): Promise; export function removeAllMessagesInConversation( conversationId: string, @@ -261,7 +261,7 @@ export function getMessageBySender( export function getMessagesBySender( { source, sourceDevice }: { source: any; sourceDevice: any }, { Message }: { Message: any } -): Promise; +): Promise; export function getMessageIdsFromServerIds( serverIds: any, conversationId: any diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 95f3906010..08c38720b8 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -98,7 +98,7 @@ const sendMessageStyle = { input: { overflow: 'auto', maxHeight: 70, - wordBreak: 'break-all', + wordBreak: 'break-word', padding: '0px', margin: '0px', }, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 88152bf765..adc8c8614a 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -54,7 +54,7 @@ async function handleGroups( // Check if anyone got kicked: const removedMembers = _.difference(oldMembers, attributes.members); - const ourDeviceWasRemoved = removedMembers.some(async member => + const ourDeviceWasRemoved = removedMembers.some(member => UserUtils.isUsFromCache(member) ); diff --git a/ts/test/session/unit/crypto/MessageEncrypter_test.ts b/ts/test/session/unit/crypto/MessageEncrypter_test.ts index a3fcbc011c..4e38afef5a 100644 --- a/ts/test/session/unit/crypto/MessageEncrypter_test.ts +++ b/ts/test/session/unit/crypto/MessageEncrypter_test.ts @@ -167,13 +167,11 @@ describe('MessageEncrypter', () => { it('should throw an error for anything else than Fallback or ClosedGroup', () => { const data = crypto.randomBytes(10); - expect( - MessageEncrypter.encrypt( - TestUtils.generateFakePubKey(), - data, - EncryptionType.Signal - ) - ).to.be.rejectedWith(Error); + return MessageEncrypter.encrypt( + TestUtils.generateFakePubKey(), + data, + EncryptionType.Signal + ).should.eventually.be.rejectedWith(Error); }); }); }); diff --git a/ts/test/session/unit/utils/Promise_test.ts b/ts/test/session/unit/utils/Promise_test.ts index 1bc04261cb..abf73a2c90 100644 --- a/ts/test/session/unit/utils/Promise_test.ts +++ b/ts/test/session/unit/utils/Promise_test.ts @@ -8,6 +8,7 @@ import { PromiseUtils } from '../../../../session/utils'; // tslint:disable-next-line: no-require-imports no-var-requires import chaiAsPromised from 'chai-as-promised'; chai.use(chaiAsPromised as any); +chai.should(); const { expect } = chai; @@ -51,10 +52,10 @@ describe('Promise Utils', () => { }; const promise = PromiseUtils.poll(task, {}); - await promise; expect(pollSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(1); + return promise; }); it('can timeout a task', () => { @@ -64,9 +65,11 @@ describe('Promise Utils', () => { const promise = PromiseUtils.poll(task, { timeoutMs: 1 }); - promise.should.eventually.be.rejectedWith('Periodic check timeout'); expect(pollSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(0); + return promise.should.eventually.be.rejectedWith( + 'Periodic check timeout' + ); }); it('will recur according to interval option', async () => { @@ -105,9 +108,9 @@ describe('Promise Utils', () => { const promise = PromiseUtils.waitForTask(task); - await promise; expect(waitForTaskSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(1); + return promise; }); it('can timeout a task', () => { @@ -117,9 +120,9 @@ describe('Promise Utils', () => { const promise = PromiseUtils.waitForTask(task, 1); - promise.should.eventually.be.rejectedWith('Task timed out.'); expect(waitForTaskSpy.callCount).to.equal(1); expect(completionSpy.callCount).to.equal(0); + return promise.should.eventually.be.rejectedWith('Task timed out.'); }); }); @@ -127,17 +130,19 @@ describe('Promise Utils', () => { it('can wait for check', async () => { const check = () => true; const promise = PromiseUtils.waitUntil(check); - await promise; expect(waitUntilSpy.callCount).to.equal(1); + return promise; }); it('can timeout a check', () => { const check = () => false; const promise = PromiseUtils.waitUntil(check, 1); - promise.should.eventually.be.rejectedWith('Periodic check timeout'); expect(waitUntilSpy.callCount).to.equal(1); + return promise.should.eventually.be.rejectedWith( + 'Periodic check timeout' + ); }); }); }); From ebf9714e499001ddad5129f9e00e72c4ed0193cc Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 11 Feb 2021 15:00:52 +1100 Subject: [PATCH 031/109] remove MessageQueueInterface --- ts/models/conversation.ts | 53 ++++++++++--------- ts/session/instance.ts | 4 +- ts/session/sending/MessageQueue.ts | 38 ++++++++++--- ts/session/sending/MessageQueueInterface.ts | 53 ------------------- ts/session/sending/index.ts | 1 - ts/session/utils/Messages.ts | 4 +- .../receiving/ConfigurationMessage_test.ts | 11 ++-- .../receiving/KeyPairRequestManager_test.ts | 7 +-- 8 files changed, 73 insertions(+), 98 deletions(-) delete mode 100644 ts/session/sending/MessageQueueInterface.ts diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index a2619d3f51..7deeb37173 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1650,35 +1650,36 @@ export class ConversationModel extends Backbone.Model { public async notify(message: any) { if (!message.isIncoming()) { - return Promise.resolve(); + return; } const conversationId = this.id; - return ConversationController.getInstance() - .getOrCreateAndWait(message.get('source'), 'private') - .then(sender => - sender.getNotificationIcon().then((iconUrl: any) => { - const messageJSON = message.toJSON(); - const messageSentAt = messageJSON.sent_at; - const messageId = message.id; - const isExpiringMessage = this.isExpiringMessage(messageJSON); - - // window.log.info('Add notification', { - // conversationId: this.idForLogging(), - // isExpiringMessage, - // messageSentAt, - // }); - window.Whisper.Notifications.add({ - conversationId, - iconUrl, - isExpiringMessage, - message: message.getNotificationText(), - messageId, - messageSentAt, - title: sender.getTitle(), - }); - }) - ); + const convo = await ConversationController.getInstance().getOrCreateAndWait( + message.get('source'), + 'private' + ); + + const iconUrl = await convo.getNotificationIcon(); + + const messageJSON = message.toJSON(); + const messageSentAt = messageJSON.sent_at; + const messageId = message.id; + const isExpiringMessage = this.isExpiringMessage(messageJSON); + + // window.log.info('Add notification', { + // conversationId: this.idForLogging(), + // isExpiringMessage, + // messageSentAt, + // }); + window.Whisper.Notifications.add({ + conversationId, + iconUrl, + isExpiringMessage, + message: message.getNotificationText(), + messageId, + messageSentAt, + title: convo.getTitle(), + }); } public async notifyTyping({ isTyping, sender }: any) { // We don't do anything with typing messages from our other devices diff --git a/ts/session/instance.ts b/ts/session/instance.ts index 15f35652be..cf7dc55a3d 100644 --- a/ts/session/instance.ts +++ b/ts/session/instance.ts @@ -1,8 +1,8 @@ -import { MessageQueue, MessageQueueInterface } from './sending/'; +import { MessageQueue } from './sending/'; let messageQueue: MessageQueue; -function getMessageQueue(): MessageQueueInterface { +function getMessageQueue(): MessageQueue { if (!messageQueue) { messageQueue = new MessageQueue(); } diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index fb2d8ed4ee..d4475426e8 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,11 +1,7 @@ import { EventEmitter } from 'events'; -import { - GroupMessageType, - MessageQueueInterface, - MessageQueueInterfaceEvents, -} from './MessageQueueInterface'; import { ChatMessage, + ClosedGroupChatMessage, ClosedGroupNewMessage, ContentMessage, DataMessage, @@ -18,8 +14,38 @@ import { PubKey, RawMessage } from '../types'; import { MessageSender } from '.'; import { ClosedGroupMessage } from '../messages/outgoing/content/data/group/ClosedGroupMessage'; import { ConfigurationMessage } from '../messages/outgoing/content/ConfigurationMessage'; +import { ClosedGroupNameChangeMessage } from '../messages/outgoing/content/data/group/ClosedGroupNameChangeMessage'; +import { + ClosedGroupAddedMembersMessage, + ClosedGroupEncryptionPairMessage, + ClosedGroupEncryptionPairRequestMessage, + ClosedGroupRemovedMembersMessage, + ClosedGroupUpdateMessage, +} from '../messages/outgoing/content/data/group'; +import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage'; + +export type GroupMessageType = + | OpenGroupMessage + | ClosedGroupChatMessage + | ClosedGroupAddedMembersMessage + | ClosedGroupRemovedMembersMessage + | ClosedGroupNameChangeMessage + | ClosedGroupMemberLeftMessage + | ClosedGroupUpdateMessage + | ExpirationTimerUpdateMessage + | ClosedGroupEncryptionPairMessage + | ClosedGroupEncryptionPairRequestMessage; + +// ClosedGroupEncryptionPairReplyMessage must be sent to a user pubkey. Not a group. +export interface MessageQueueInterfaceEvents { + sendSuccess: ( + message: RawMessage | OpenGroupMessage, + wrappedEnvelope?: Uint8Array + ) => void; + sendFail: (message: RawMessage | OpenGroupMessage, error: Error) => void; +} -export class MessageQueue implements MessageQueueInterface { +export class MessageQueue { public readonly events: TypedEventEmitter; private readonly jobQueues: Map = new Map(); private readonly pendingMessageCache: PendingMessageCache; diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts deleted file mode 100644 index 535fc0737c..0000000000 --- a/ts/session/sending/MessageQueueInterface.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - ContentMessage, - ExpirationTimerUpdateMessage, - OpenGroupMessage, -} from '../messages/outgoing'; -import { RawMessage } from '../types/RawMessage'; -import { TypedEventEmitter } from '../utils'; -import { PubKey } from '../types'; -import { ClosedGroupChatMessage } from '../messages/outgoing/content/data/group/ClosedGroupChatMessage'; -import { - ClosedGroupAddedMembersMessage, - ClosedGroupEncryptionPairMessage, - ClosedGroupNameChangeMessage, - ClosedGroupRemovedMembersMessage, - ClosedGroupUpdateMessage, -} from '../messages/outgoing/content/data/group'; -import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage'; -import { ClosedGroupEncryptionPairRequestMessage } from '../messages/outgoing/content/data/group/ClosedGroupEncryptionPairRequestMessage'; - -export type GroupMessageType = - | OpenGroupMessage - | ClosedGroupChatMessage - | ClosedGroupAddedMembersMessage - | ClosedGroupRemovedMembersMessage - | ClosedGroupNameChangeMessage - | ClosedGroupMemberLeftMessage - | ClosedGroupUpdateMessage - | ClosedGroupEncryptionPairMessage - | ClosedGroupEncryptionPairRequestMessage; - -// ClosedGroupEncryptionPairReplyMessage must be sent to a user pubkey. Not a group. -export interface MessageQueueInterfaceEvents { - sendSuccess: ( - message: RawMessage | OpenGroupMessage, - wrappedEnvelope?: Uint8Array - ) => void; - sendFail: (message: RawMessage | OpenGroupMessage, error: Error) => void; -} - -export interface MessageQueueInterface { - events: TypedEventEmitter; - sendToPubKey(user: PubKey, message: ContentMessage): Promise; - send(device: PubKey, message: ContentMessage): Promise; - sendToGroup( - message: GroupMessageType, - sentCb?: (message?: RawMessage) => Promise - ): Promise; - sendSyncMessage( - message: any, - sentCb?: (message?: RawMessage) => Promise - ): Promise; - processPending(device: PubKey): Promise; -} diff --git a/ts/session/sending/index.ts b/ts/session/sending/index.ts index a6de06ca76..251c5d04bb 100644 --- a/ts/session/sending/index.ts +++ b/ts/session/sending/index.ts @@ -4,4 +4,3 @@ export { MessageSender }; export * from './PendingMessageCache'; export * from './MessageQueue'; -export * from './MessageQueueInterface'; diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index f783764f8a..61da963b32 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -72,8 +72,8 @@ export const getCurrentConfigurationMessage = async ( const openGroupsIds = convos .filter(c => !!c.get('active_at') && c.isPublic() && !c.get('left')) .map(c => c.id.substring((c.id as string).lastIndexOf('@') + 1)) as Array< - string - >; + string + >; const closedGroupModels = convos.filter( c => !!c.get('active_at') && diff --git a/ts/test/session/unit/receiving/ConfigurationMessage_test.ts b/ts/test/session/unit/receiving/ConfigurationMessage_test.ts index 0589ff1fc3..6651697a3f 100644 --- a/ts/test/session/unit/receiving/ConfigurationMessage_test.ts +++ b/ts/test/session/unit/receiving/ConfigurationMessage_test.ts @@ -1,3 +1,5 @@ +// tslint:disable: no-implicit-dependencies + import { SignalService } from '../../../../protobuf'; import { handleConfigurationMessage } from '../../../../receiver/contentMessage'; import chai from 'chai'; @@ -11,9 +13,8 @@ import * as cache from '../../../../receiver/cache'; import * as data from '../../../../../js/modules/data'; import { EnvelopePlus } from '../../../../receiver/types'; -// tslint:disable-next-line: no-require-imports no-var-requires -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); chai.should(); const { expect } = chai; @@ -44,7 +45,7 @@ describe('ConfigurationMessage_receiving', () => { }); it('should not be processed if we do not have a pubkey', async () => { - sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(undefined); + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').resolves(undefined); envelope = TestUtils.generateEnvelopePlus(sender); const proto = config.contentProto(); @@ -62,7 +63,7 @@ describe('ConfigurationMessage_receiving', () => { const ourNumber = TestUtils.generateFakePubKey().key; beforeEach(() => { - sandbox.stub(UserUtils, 'getCurrentDevicePubKey').resolves(ourNumber); + sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').resolves(ourNumber); }); it('should not be processed if the message is not coming from our number', async () => { diff --git a/ts/test/session/unit/receiving/KeyPairRequestManager_test.ts b/ts/test/session/unit/receiving/KeyPairRequestManager_test.ts index bab9f4dc8c..4f5d38bb3f 100644 --- a/ts/test/session/unit/receiving/KeyPairRequestManager_test.ts +++ b/ts/test/session/unit/receiving/KeyPairRequestManager_test.ts @@ -1,13 +1,14 @@ +// tslint:disable: no-implicit-dependencies + import chai from 'chai'; -// tslint:disable: no-require-imports no-var-requires no-implicit-dependencies import _ from 'lodash'; import { describe } from 'mocha'; import { KeyPairRequestManager } from '../../../../receiver/keyPairRequestManager'; import { TestUtils } from '../../../test-utils'; -const chaiAsPromised = require('chai-as-promised'); -chai.use(chaiAsPromised); +import chaiAsPromised from 'chai-as-promised'; +chai.use(chaiAsPromised as any); chai.should(); const { expect } = chai; From 7b81c4213ac78efd899041a97cf8920819526f7d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 12 Feb 2021 14:08:11 +1100 Subject: [PATCH 032/109] Merge clearnet --- images/group_default.png | Bin 0 -> 555 bytes js/modules/loki_app_dot_net_api.js | 6 +- preload.js | 10 +- ts/receiver/closedGroups.ts | 146 ++++++++++++++++++++--------- ts/receiver/contentMessage.ts | 19 ++-- ts/session/group/index.ts | 9 ++ ts/window.d.ts | 1 + 7 files changed, 133 insertions(+), 58 deletions(-) create mode 100644 images/group_default.png diff --git a/images/group_default.png b/images/group_default.png new file mode 100644 index 0000000000000000000000000000000000000000..6b503e9ff80f31824ea4cdcb67e8f24494638ed0 GIT binary patch literal 555 zcmeAS@N?(olHy`uVBq!ia0vp^DIm4nJ za0`PlBg3pY5H=O_6N*jg7ShvJ}R>q7#R0>x;TbZ+xGdHYDo|V!@`5|}R-29a*6(*> zKUr7p%{Itu*Ksj)6mVn_biqe(#!p)`cdC&XtIsZz6~+ZmB>OgM9O$v}F*GcyhJU_t|H-u zwg1I)e9w*uK6!dy@QAiQugmgy$1k=<)iY$bX>RiCOMNh1-tOgvMXvRWo+^s3t4!*? znT&Gjp5p5^X&&>my%=w8uY6K% z!f2keU#K+#cfS%&Roqd wt!XJazjZUWlE)R-`^rrmN*xM#iM@XqU)1F+`?}690T{Imp00i_>zopr02zYUJ^%m! literal 0 HcmV?d00001 diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 79a853da40..1fe042ab19 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -287,7 +287,7 @@ const serverRequest = async (endpoint, options = {}) => { txtResponse = await result.text(); // cloudflare timeouts (504s) will be html... - response = options.textResponse ? txtResponse : JSON.parse(txtResponse); + response = options.noJson ? txtResponse : JSON.parse(txtResponse); // result.status will always be 200 // emulate the correct http code if available @@ -303,7 +303,7 @@ const serverRequest = async (endpoint, options = {}) => { e.message, `json: ${txtResponse}`, 'attempting connection to', - url + url.toString() ); } else { log.error( @@ -311,7 +311,7 @@ const serverRequest = async (endpoint, options = {}) => { e.code, e.message, 'attempting connection to', - url + url.toString() ); } diff --git a/preload.js b/preload.js index 57bb354021..358648184d 100644 --- a/preload.js +++ b/preload.js @@ -61,6 +61,7 @@ window.lokiFeatureFlags = { useFileOnionRequests: true, useFileOnionRequestsV2: true, // more compact encoding of files in response onionRequestHops: 3, + useRequestEncryptionKeyPair: false, }; if ( @@ -85,7 +86,7 @@ window.isBeforeVersion = (toCheck, baseVersion) => { }; // eslint-disable-next-line func-names -window.CONSTANTS = new (function() { +window.CONSTANTS = new (function () { this.MAX_GROUP_NAME_LENGTH = 64; this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); this.MAX_LINKED_DEVICES = 1; @@ -376,7 +377,7 @@ window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args); // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(() => { - window.nodeSetImmediate(() => {}); + window.nodeSetImmediate(() => { }); }, 1000); const { autoOrientImage } = require('./js/modules/auto_orient_image'); @@ -455,9 +456,9 @@ if (process.env.USE_STUBBED_NETWORK) { } // eslint-disable-next-line no-extend-native,func-names -Promise.prototype.ignore = function() { +Promise.prototype.ignore = function () { // eslint-disable-next-line more/no-then - this.then(() => {}); + this.then(() => { }); }; if ( @@ -484,6 +485,7 @@ if (config.environment.includes('test-integration')) { useOnionRequests: false, useFileOnionRequests: false, useOnionRequestsV2: false, + useRequestEncryptionKeyPair: false, }; /* eslint-disable global-require, import/no-extraneous-dependencies */ window.sinon = require('sinon'); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index c94034ffbf..472292b420 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -33,6 +33,11 @@ import { MessageController } from '../session/messages'; import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/content/data/group'; import { queueAllCachedFromSource } from './receiver'; +export const distributingClosedGroupEncryptionKeyPairs = new Map< + string, + ECKeyPair +>(); + export async function handleClosedGroupControlMessage( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage @@ -456,6 +461,9 @@ async function handleClosedGroupEncryptionKeyPair( ); if (isKeyPairAlreadyHere) { + const existingKeyPairs = await getAllEncryptionKeyPairsForGroup( + groupPublicKey + ); window.log.info('Dropping already saved keypair for group', groupPublicKey); await removeFromCache(envelope); return; @@ -532,11 +540,18 @@ async function performIfValid( } else if (groupUpdate.type === Type.MEMBER_LEFT) { await handleClosedGroupMemberLeft(envelope, groupUpdate, convo); } else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) { - await handleClosedGroupEncryptionKeyPairRequest( - envelope, - groupUpdate, - convo - ); + if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + await handleClosedGroupEncryptionKeyPairRequest( + envelope, + groupUpdate, + convo + ); + } else { + window.log.warn( + 'Received ENCRYPTION_KEY_PAIR_REQUEST message but it is not enabled for now.' + ); + await removeFromCache(envelope); + } // if you add a case here, remember to add it where performIfValid is called too. } @@ -590,6 +605,15 @@ async function handleClosedGroupMembersAdded( return; } + if (await areWeAdmin(convo)) { + await sendLatestKeyPairToUsers( + envelope, + convo, + convo.id, + membersNotAlreadyPresent + ); + } + const members = [...oldMembers, ...membersNotAlreadyPresent]; // Only add update message if we have something to show @@ -604,6 +628,16 @@ async function handleClosedGroupMembersAdded( await removeFromCache(envelope); } +async function areWeAdmin(groupConvo: ConversationModel) { + if (!groupConvo) { + throw new Error('areWeAdmin needs a convo'); + } + + const groupAdmins = groupConvo.get('groupAdmins'); + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + return groupAdmins?.includes(ourNumber) || false; +} + async function handleClosedGroupMembersRemoved( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, @@ -650,8 +684,7 @@ async function handleClosedGroupMembersRemoved( window.SwarmPolling.removePubkey(groupPubKey); } // Generate and distribute a new encryption key pair if needed - const isCurrentUserAdmin = firstAdmin === ourPubKey.key; - if (isCurrentUserAdmin) { + if (await areWeAdmin(convo)) { try { await ClosedGroup.generateAndSendNewEncryptionKeyPair( groupPubKey, @@ -731,58 +764,83 @@ async function handleClosedGroupMemberLeft( await removeFromCache(envelope); } -async function handleClosedGroupEncryptionKeyPairRequest( +async function sendLatestKeyPairToUsers( envelope: EnvelopePlus, - groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, - convo: ConversationModel + groupConvo: ConversationModel, + groupPubKey: string, + targetUsers: Array ) { - const sender = envelope.senderIdentity; - const groupPublicKey = envelope.source; - // Guard against self-sends - if (UserUtils.isUsFromCache(sender)) { - window.log.info( - 'Dropping self send message of type ENCRYPTION_KEYPAIR_REQUEST' - ); - await removeFromCache(envelope); - return; - } + // use the inMemory keypair if found + const inMemoryKeyPair = distributingClosedGroupEncryptionKeyPairs.get( + groupPubKey + ); + // Get the latest encryption key pair const latestKeyPair = await getLatestClosedGroupEncryptionKeyPair( - groupPublicKey + groupPubKey ); - if (!latestKeyPair) { + if (!inMemoryKeyPair && !latestKeyPair) { window.log.info( 'We do not have the keypair ourself, so dropping this message.' ); - await removeFromCache(envelope); return; } - window.log.info( - `Responding to closed group encryption key pair request from: ${sender}` - ); - await ConversationController.getInstance().getOrCreateAndWait( - sender, - 'private' - ); + const expireTimer = groupConvo.get('expireTimer') || 0; - const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( - [sender], - ECKeyPair.fromHexKeyPair(latestKeyPair) - ); - const expireTimer = convo.get('expireTimer') || 0; + await Promise.all( + targetUsers.map(async member => { + window.log.info( + `Sending latest closed group encryption key pair to: ${member}` + ); + await ConversationController.getInstance().getOrCreateAndWait( + member, + 'private' + ); - const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ - groupId: groupPublicKey, - timestamp: Date.now(), - encryptedKeyPairs: wrappers, - expireTimer, - }); + const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( + [member], + inMemoryKeyPair || ECKeyPair.fromHexKeyPair(latestKeyPair) + ); - // the encryption keypair is sent using established channels - await getMessageQueue().sendToPubKey(PubKey.cast(sender), keypairsMessage); + const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ + groupId: groupPubKey, + timestamp: Date.now(), + encryptedKeyPairs: wrappers, + expireTimer, + }); + + // the encryption keypair is sent using established channels + await getMessageQueue().sendToPubKey( + PubKey.cast(member), + keypairsMessage + ); + }) + ); +} - await removeFromCache(envelope); +async function handleClosedGroupEncryptionKeyPairRequest( + envelope: EnvelopePlus, + groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + groupConvo: ConversationModel +) { + if (!window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + throw new Error('useRequestEncryptionKeyPair is disabled'); + } + const sender = envelope.senderIdentity; + const groupPublicKey = envelope.source; + // Guard against self-sends + if (UserUtils.isUsFromCache(sender)) { + window.log.info( + 'Dropping self send message of type ENCRYPTION_KEYPAIR_REQUEST' + ); + await removeFromCache(envelope); + return; + } + await sendLatestKeyPairToUsers(envelope, groupConvo, groupPublicKey, [ + sender, + ]); + return removeFromCache(envelope); } export async function createClosedGroup( diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index ecf7c634d4..765da65470 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -124,18 +124,23 @@ async function decryptForClosedGroup( 'decryptWithSessionProtocol for medium group message throw:', e ); - const keypairRequestManager = KeyPairRequestManager.getInstance(); const groupPubKey = PubKey.cast(envelope.source); - if (keypairRequestManager.canTriggerRequestWith(groupPubKey)) { - keypairRequestManager.markRequestSendFor(groupPubKey, Date.now()); - await requestEncryptionKeyPair(groupPubKey); + + // To enable back if we decide to enable encryption key pair request work again + if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + const keypairRequestManager = KeyPairRequestManager.getInstance(); + if (keypairRequestManager.canTriggerRequestWith(groupPubKey)) { + keypairRequestManager.markRequestSendFor(groupPubKey, Date.now()); + await requestEncryptionKeyPair(groupPubKey); + } } + // IMPORTANT do not remove the message from the cache just yet. + // We will try to decrypt it once we get the encryption keypair. + // for that to work, we need to throw an error just like here. throw new Error( `Waiting for an encryption keypair to be received for group ${groupPubKey.key}` ); - // do not remove it from the cache yet. We will try to decrypt it once we get the encryption keypair - // TODO drop it if after some time we still don't get to decrypt it - // await removeFromCache(envelope); + return null; } } diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 83bc9ab8c6..560d90e4bd 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -32,6 +32,7 @@ import { ConversationModel } from '../../models/conversation'; import { MessageModel } from '../../models/message'; import { MessageModelType } from '../../models/messageType'; import { MessageController } from '../messages'; +import { distributingClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; export interface GroupInfo { id: string; @@ -557,11 +558,15 @@ export async function generateAndSendNewEncryptionKeyPair( expireTimer, }); + distributingClosedGroupEncryptionKeyPairs.set(toHex(groupId), newKeyPair); + const messageSentCallback = async () => { window.log.info( `KeyPairMessage for ClosedGroup ${groupPublicKey} is sent. Saving the new encryptionKeyPair.` ); + distributingClosedGroupEncryptionKeyPairs.delete(toHex(groupId)); + await addClosedGroupEncryptionKeyPair( toHex(groupId), newKeyPair.toHexKeyPair() @@ -611,6 +616,10 @@ export async function buildEncryptionKeyPairWrappers( export async function requestEncryptionKeyPair( groupPublicKey: string | PubKey ) { + if (!window.lokiFeatureFlags.useRequestEncryptionKeyPair) { + throw new Error('useRequestEncryptionKeyPair is disabled'); + } + const groupConvo = ConversationController.getInstance().get( PubKey.cast(groupPublicKey).key ); diff --git a/ts/window.d.ts b/ts/window.d.ts index afc4dafe29..bf91cdefe8 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -64,6 +64,7 @@ declare global { useFileOnionRequests: boolean; useFileOnionRequestsV2: boolean; onionRequestHops: number; + useRequestEncryptionKeyPair: boolean; }; lokiFileServerAPI: LokiFileServerInstance; lokiMessageAPI: LokiMessageInterface; From 8ea9f02cecf247f21d27204b162e85b25e253417 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 15 Feb 2021 15:16:38 +1100 Subject: [PATCH 033/109] Move data.js to data.ts --- AUDRICTOCLEAN.txt | 6 + app/sql.js | 358 +---- js/background.js | 144 +- js/database.js | 4 - js/delivery_receipts.js | 6 +- js/modules/attachment_downloads.js | 2 +- js/modules/backup.js | 12 +- js/modules/data.d.ts | 425 ------ js/modules/data.js | 1282 ----------------- js/modules/idle_detector.js | 60 - js/modules/indexeddb.js | 168 --- js/modules/loki_app_dot_net_api.js | 8 +- js/modules/messages_data_migrator.js | 405 ------ js/modules/migrate_to_sql.js | 409 ------ js/modules/migrations/18/index.js | 17 - .../migrations/get_placeholder_migrations.js | 35 - js/modules/migrations/migrations.js | 245 ---- js/modules/migrations/run_migrations.js | 79 - js/modules/signal.js | 26 +- js/read_receipts.js | 5 +- js/signal_protocol_store.js | 341 ----- libloki/crypto.js | 4 +- libtextsecure/account_manager.js | 21 +- libtextsecure/storage/unprocessed.js | 23 +- main.js | 14 - preload.js | 20 +- test/backup_test.js | 7 +- ts/components/session/ActionsPanel.tsx | 2 +- ts/components/session/RegistrationTabs.tsx | 2 +- ts/components/session/SessionInboxView.tsx | 6 +- .../session/SessionPasswordModal.tsx | 4 +- ts/components/session/SessionSeedModal.tsx | 2 +- .../conversation/SessionConversation.tsx | 12 +- .../conversation/SessionMessagesList.tsx | 8 +- .../conversation/SessionRightPanel.tsx | 2 +- .../session/settings/SessionSettings.tsx | 2 +- ts/data/data.ts | 1072 ++++++++++++++ ts/models/conversation.ts | 22 +- ts/models/message.ts | 17 +- ts/receiver/attachments.ts | 6 +- ts/receiver/closedGroups.ts | 9 +- ts/receiver/contentMessage.ts | 2 +- ts/receiver/dataMessage.ts | 13 +- ts/receiver/queuedJob.ts | 10 +- ts/session/conversations/index.ts | 14 +- ts/session/crypto/MessageEncrypter.ts | 2 +- ts/session/group/index.ts | 2 +- ts/session/messages/MessageController.ts | 17 +- ts/session/onions/index.ts | 2 +- ts/session/sending/PendingMessageCache.ts | 2 +- ts/session/snode_api/snodePool.ts | 2 +- ts/session/snode_api/swarmPolling.ts | 2 +- ts/session/types/OpenGroup.ts | 59 +- ts/session/utils/Messages.ts | 2 +- ts/session/utils/User.ts | 2 +- ts/session/utils/syncUtils.ts | 2 +- ts/shims/Signal.ts | 2 +- ts/state/ducks/conversations.ts | 17 +- ts/state/ducks/search.ts | 2 +- .../receiving/ConfigurationMessage_test.ts | 2 +- ts/test/test-utils/utils/stubbing.ts | 4 +- ts/util/blockedNumberController.ts | 2 +- ts/window.d.ts | 3 +- 63 files changed, 1259 insertions(+), 4198 deletions(-) delete mode 100644 js/modules/data.d.ts delete mode 100644 js/modules/data.js delete mode 100644 js/modules/idle_detector.js delete mode 100644 js/modules/indexeddb.js delete mode 100644 js/modules/messages_data_migrator.js delete mode 100644 js/modules/migrate_to_sql.js delete mode 100644 js/modules/migrations/18/index.js delete mode 100644 js/modules/migrations/get_placeholder_migrations.js delete mode 100644 js/modules/migrations/migrations.js delete mode 100644 js/modules/migrations/run_migrations.js create mode 100644 ts/data/data.ts diff --git a/AUDRICTOCLEAN.txt b/AUDRICTOCLEAN.txt index b8b1465a9d..f1bf48c7d0 100644 --- a/AUDRICTOCLEAN.txt +++ b/AUDRICTOCLEAN.txt @@ -25,3 +25,9 @@ ReadSyncs SyncMessage sendSyncMessage needs to be rewritten sendSyncMessageOnly to fix + + +indexedDB +initializeAttachmentMetadata=> +schemaVersion for messages to put as what needs to be set +run_migration \ No newline at end of file diff --git a/app/sql.js b/app/sql.js index 50b968c73b..444bde45da 100644 --- a/app/sql.js +++ b/app/sql.js @@ -26,51 +26,21 @@ module.exports = { initialize, close, removeDB, - removeIndexedDBFiles, setSQLPassword, getPasswordHash, savePasswordHash, removePasswordHash, - createOrUpdateIdentityKey, getIdentityKeyById, - bulkAddIdentityKeys, - removeIdentityKeyById, removeAllIdentityKeys, getAllIdentityKeys, - createOrUpdatePreKey, - getPreKeyById, - getPreKeyByRecipient, - bulkAddPreKeys, - removePreKeyById, - removeAllPreKeys, - getAllPreKeys, - - createOrUpdateSignedPreKey, - getSignedPreKeyById, - getAllSignedPreKeys, - bulkAddSignedPreKeys, - removeSignedPreKeyById, removeAllSignedPreKeys, - - createOrUpdateContactPreKey, - getContactPreKeyById, - getContactPreKeyByIdentityKey, - getContactPreKeys, - getAllContactPreKeys, - bulkAddContactPreKeys, - removeContactPreKeyByIdentityKey, removeAllContactPreKeys, - - createOrUpdateContactSignedPreKey, - getContactSignedPreKeyById, - getContactSignedPreKeyByIdentityKey, - getContactSignedPreKeys, - bulkAddContactSignedPreKeys, - removeContactSignedPreKeyByIdentityKey, removeAllContactSignedPreKeys, + removeAllPreKeys, + removeAllSessions, createOrUpdateItem, getItemById, @@ -79,15 +49,6 @@ module.exports = { removeItemById, removeAllItems, - createOrUpdateSession, - getSessionById, - getSessionsByNumber, - bulkAddSessions, - removeSessionById, - removeSessionsByNumber, - removeAllSessions, - getAllSessions, - getSwarmNodesForPubkey, updateSwarmNodesForPubkey, getGuardNodes, @@ -108,7 +69,6 @@ module.exports = { getAllConversationIds, getAllGroupsInvolvingId, removeAllConversations, - removeAllPrivateConversations, searchConversations, searchMessages, @@ -126,7 +86,6 @@ module.exports = { getUnreadByConversation, getUnreadCountByConversation, getMessageBySender, - getMessagesBySender, getMessageIdsFromServerIds, getMessageById, getAllMessages, @@ -158,16 +117,12 @@ module.exports = { removeAllAttachmentDownloadJobs, removeAll, - removeAllConfiguration, - getMessagesNeedingUpgrade, getMessagesWithVisualMediaAttachments, getMessagesWithFileAttachments, removeKnownAttachments, - removeAllClosedGroupRatchets, - getAllEncryptionKeyPairsForGroup, getLatestClosedGroupEncryptionKeyPair, addClosedGroupEncryptionKeyPair, @@ -906,12 +861,6 @@ async function updateToLokiSchemaVersion3(currentVersion, instance) { const SENDER_KEYS_TABLE = 'senderKeys'; -async function removeAllClosedGroupRatchets(groupId) { - await db.run(`DELETE FROM ${SENDER_KEYS_TABLE} WHERE groupId = $groupId;`, { - $groupId: groupId, - }); -} - async function updateToLokiSchemaVersion4(currentVersion, instance) { if (currentVersion >= 4) { return; @@ -1181,11 +1130,8 @@ async function createLokiSchemaTable(instance) { let db; let filePath; -let indexedDBPath; function _initializePaths(configDir) { - indexedDBPath = path.join(configDir, 'IndexedDB'); - const dbDir = path.join(configDir, 'sql'); mkdirp.sync(dbDir); @@ -1297,18 +1243,6 @@ async function removeDB(configDir = null) { rimraf.sync(filePath); } -async function removeIndexedDBFiles() { - if (!indexedDBPath) { - throw new Error( - 'removeIndexedDBFiles: Need to initialize and set indexedDBPath first!' - ); - } - - const pattern = path.join(indexedDBPath, '*.leveldb'); - rimraf.sync(pattern); - indexedDBPath = null; -} - // Password hash const PASS_HASH_ID = 'passHash'; async function getPasswordHash() { @@ -1328,18 +1262,9 @@ async function removePasswordHash() { } const IDENTITY_KEYS_TABLE = 'identityKeys'; -async function createOrUpdateIdentityKey(data) { - return createOrUpdate(IDENTITY_KEYS_TABLE, data); -} async function getIdentityKeyById(id, instance) { return getById(IDENTITY_KEYS_TABLE, id, instance); } -async function bulkAddIdentityKeys(array) { - return bulkAdd(IDENTITY_KEYS_TABLE, array); -} -async function removeIdentityKeyById(id) { - return removeById(IDENTITY_KEYS_TABLE, id); -} async function removeAllIdentityKeys() { return removeAllFromTable(IDENTITY_KEYS_TABLE); } @@ -1348,203 +1273,24 @@ async function getAllIdentityKeys() { } const PRE_KEYS_TABLE = 'preKeys'; -async function createOrUpdatePreKey(data) { - const { id, recipient } = data; - if (!id) { - throw new Error('createOrUpdate: Provided data did not have a truthy id'); - } - - await db.run( - `INSERT OR REPLACE INTO ${PRE_KEYS_TABLE} ( - id, - recipient, - json - ) values ( - $id, - $recipient, - $json - )`, - { - $id: id, - $recipient: recipient || '', - $json: objectToJSON(data), - } - ); -} -async function getPreKeyById(id) { - return getById(PRE_KEYS_TABLE, id); -} -async function getPreKeyByRecipient(recipient) { - const row = await db.get( - `SELECT * FROM ${PRE_KEYS_TABLE} WHERE recipient = $recipient;`, - { - $recipient: recipient, - } - ); - - if (!row) { - return null; - } - return jsonToObject(row.json); -} -async function bulkAddPreKeys(array) { - return bulkAdd(PRE_KEYS_TABLE, array); -} -async function removePreKeyById(id) { - return removeById(PRE_KEYS_TABLE, id); -} async function removeAllPreKeys() { return removeAllFromTable(PRE_KEYS_TABLE); } -async function getAllPreKeys() { - return getAllFromTable(PRE_KEYS_TABLE); -} const CONTACT_PRE_KEYS_TABLE = 'contactPreKeys'; -async function createOrUpdateContactPreKey(data) { - const { keyId, identityKeyString } = data; - - await db.run( - `INSERT OR REPLACE INTO ${CONTACT_PRE_KEYS_TABLE} ( - keyId, - identityKeyString, - json - ) values ( - $keyId, - $identityKeyString, - $json - )`, - { - $keyId: keyId, - $identityKeyString: identityKeyString || '', - $json: objectToJSON(data), - } - ); -} -async function getContactPreKeyById(id) { - return getById(CONTACT_PRE_KEYS_TABLE, id); -} -async function getContactPreKeyByIdentityKey(key) { - const row = await db.get( - `SELECT * FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString ORDER BY keyId DESC LIMIT 1;`, - { - $identityKeyString: key, - } - ); - - if (!row) { - return null; - } - - return jsonToObject(row.json); -} -async function getContactPreKeys(keyId, identityKeyString) { - const query = `SELECT * FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString AND keyId = $keyId;`; - const rows = await db.all(query, { - $keyId: keyId, - $identityKeyString: identityKeyString, - }); - return map(rows, row => jsonToObject(row.json)); -} -async function bulkAddContactPreKeys(array) { - return bulkAdd(CONTACT_PRE_KEYS_TABLE, array); -} -async function removeContactPreKeyByIdentityKey(key) { - await db.run( - `DELETE FROM ${CONTACT_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`, - { - $identityKeyString: key, - } - ); -} async function removeAllContactPreKeys() { return removeAllFromTable(CONTACT_PRE_KEYS_TABLE); } const CONTACT_SIGNED_PRE_KEYS_TABLE = 'contactSignedPreKeys'; -async function createOrUpdateContactSignedPreKey(data) { - const { keyId, identityKeyString } = data; - await db.run( - `INSERT OR REPLACE INTO ${CONTACT_SIGNED_PRE_KEYS_TABLE} ( - keyId, - identityKeyString, - json - ) values ( - $keyId, - $identityKeyString, - $json - )`, - { - $keyId: keyId, - $identityKeyString: identityKeyString || '', - $json: objectToJSON(data), - } - ); -} -async function getContactSignedPreKeyById(id) { - return getById(CONTACT_SIGNED_PRE_KEYS_TABLE, id); -} -async function getContactSignedPreKeyByIdentityKey(key) { - const row = await db.get( - `SELECT * FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString ORDER BY keyId DESC;`, - { - $identityKeyString: key, - } - ); - - if (!row) { - return null; - } - - return jsonToObject(row.json); -} -async function getContactSignedPreKeys(keyId, identityKeyString) { - const query = `SELECT * FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString AND keyId = $keyId;`; - const rows = await db.all(query, { - $keyId: keyId, - $identityKeyString: identityKeyString, - }); - return map(rows, row => jsonToObject(row.json)); -} -async function bulkAddContactSignedPreKeys(array) { - return bulkAdd(CONTACT_SIGNED_PRE_KEYS_TABLE, array); -} -async function removeContactSignedPreKeyByIdentityKey(key) { - await db.run( - `DELETE FROM ${CONTACT_SIGNED_PRE_KEYS_TABLE} WHERE identityKeyString = $identityKeyString;`, - { - $identityKeyString: key, - } - ); -} async function removeAllContactSignedPreKeys() { return removeAllFromTable(CONTACT_SIGNED_PRE_KEYS_TABLE); } const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; -async function createOrUpdateSignedPreKey(data) { - return createOrUpdate(SIGNED_PRE_KEYS_TABLE, data); -} -async function getSignedPreKeyById(id) { - return getById(SIGNED_PRE_KEYS_TABLE, id); -} -async function getAllSignedPreKeys() { - const rows = await db.all('SELECT json FROM signedPreKeys ORDER BY id ASC;'); - return map(rows, row => jsonToObject(row.json)); -} -async function getAllContactPreKeys() { - const rows = await db.all('SELECT json FROM contactPreKeys ORDER BY id ASC;'); - return map(rows, row => jsonToObject(row.json)); -} -async function bulkAddSignedPreKeys(array) { - return bulkAdd(SIGNED_PRE_KEYS_TABLE, array); -} -async function removeSignedPreKeyById(id) { - return removeById(SIGNED_PRE_KEYS_TABLE, id); -} async function removeAllSignedPreKeys() { return removeAllFromTable(SIGNED_PRE_KEYS_TABLE); } @@ -1607,62 +1353,9 @@ async function removeAllItems() { } const SESSIONS_TABLE = 'sessions'; -async function createOrUpdateSession(data) { - const { id, number } = data; - if (!id) { - throw new Error( - 'createOrUpdateSession: Provided data did not have a truthy id' - ); - } - if (!number) { - throw new Error( - 'createOrUpdateSession: Provided data did not have a truthy number' - ); - } - - await db.run( - `INSERT OR REPLACE INTO sessions ( - id, - number, - json - ) values ( - $id, - $number, - $json - )`, - { - $id: id, - $number: number, - $json: objectToJSON(data), - } - ); -} -async function getSessionById(id) { - return getById(SESSIONS_TABLE, id); -} -async function getSessionsByNumber(number) { - const rows = await db.all('SELECT * FROM sessions WHERE number = $number;', { - $number: number, - }); - return map(rows, row => jsonToObject(row.json)); -} -async function bulkAddSessions(array) { - return bulkAdd(SESSIONS_TABLE, array); -} -async function removeSessionById(id) { - return removeById(SESSIONS_TABLE, id); -} -async function removeSessionsByNumber(number) { - await db.run('DELETE FROM sessions WHERE number = $number;', { - $number: number, - }); -} async function removeAllSessions() { return removeAllFromTable(SESSIONS_TABLE); } -async function getAllSessions() { - return getAllFromTable(SESSIONS_TABLE); -} async function createOrUpdate(table, data) { const { id } = data; @@ -2412,20 +2105,6 @@ async function getMessageBySender({ source, sourceDevice, sent_at }) { return map(rows, row => jsonToObject(row.json)); } -async function getMessagesBySender({ source, sourceDevice }) { - const rows = await db.all( - `SELECT json FROM ${MESSAGES_TABLE} WHERE - source = $source AND - sourceDevice = $sourceDevice`, - { - $source: source, - $sourceDevice: sourceDevice, - } - ); - - return map(rows, row => jsonToObject(row.json)); -} - async function getAllUnsentMessages() { const rows = await db.all(` SELECT json FROM ${MESSAGES_TABLE} WHERE @@ -2836,43 +2515,10 @@ function getRemoveConfigurationPromises() { ]; } -// Anything that isn't user-visible data -async function removeAllConfiguration() { - let promise; - - db.serialize(() => { - promise = Promise.all([ - db.run('BEGIN TRANSACTION;'), - ...getRemoveConfigurationPromises(), - db.run('COMMIT TRANSACTION;'), - ]); - }); - - await promise; -} - async function removeAllConversations() { await removeAllFromTable(CONVERSATIONS_TABLE); } -async function removeAllPrivateConversations() { - await db.run(`DELETE FROM ${CONVERSATIONS_TABLE} WHERE type = 'private'`); -} - -async function getMessagesNeedingUpgrade(limit, { maxVersion }) { - const rows = await db.all( - `SELECT json FROM ${MESSAGES_TABLE} - WHERE schemaVersion IS NULL OR schemaVersion < $maxVersion - LIMIT $limit;`, - { - $maxVersion: maxVersion, - $limit: limit, - } - ); - - return map(rows, row => jsonToObject(row.json)); -} - async function getMessagesWithVisualMediaAttachments( conversationId, { limit } diff --git a/js/background.js b/js/background.js index 3008d254f1..1248aeadf0 100644 --- a/js/background.js +++ b/js/background.js @@ -67,19 +67,6 @@ // of preload.js processing window.setImmediate = window.nodeSetImmediate; - const { IdleDetector, MessageDataMigrator } = Signal.Workflow; - const { - mandatoryMessageUpgrade, - migrateAllToSQLCipher, - removeDatabase, - runMigrations, - doesDatabaseExist, - } = Signal.IndexedDB; - const { Message } = window.Signal.Types; - const { - upgradeMessageSchema, - writeNewAttachmentData, - } = window.Signal.Migrations; const { Views } = window.Signal; // Implicitly used in `indexeddb-backbonejs-adapter`: @@ -100,7 +87,6 @@ }, 2000); } - let idleDetector; let initialLoadComplete = false; let newVersion = false; @@ -133,13 +119,6 @@ const cancelInitializationMessage = Views.Initialization.setMessage(); - const isIndexedDBPresent = await doesDatabaseExist(); - if (isIndexedDBPresent) { - window.installStorage(window.legacyStorage); - window.log.info('Start IndexedDB migrations'); - await runMigrations(); - } - window.log.info('Storage fetch'); storage.fetch(); @@ -148,12 +127,7 @@ if (specialConvInited) { return; } - const publicConversations = await window.Signal.Data.getAllPublicConversations( - { - ConversationCollection: - window.models.Conversation.ConversationCollection, - } - ); + const publicConversations = await window.Signal.Data.getAllPublicConversations(); publicConversations.forEach(conversation => { // weird but create the object and does everything we need conversation.getPublicSendData(); @@ -262,9 +236,6 @@ shutdown: async () => { // Stop background processing window.Signal.AttachmentDownloads.stop(); - if (idleDetector) { - idleDetector.stop(); - } // Stop processing incoming messages if (messageReceiver) { @@ -292,58 +263,10 @@ await window.Signal.Logs.deleteAll(); } - if (isIndexedDBPresent) { - await mandatoryMessageUpgrade({ upgradeMessageSchema }); - await migrateAllToSQLCipher({ writeNewAttachmentData, Views }); - await removeDatabase(); - try { - await window.Signal.Data.removeIndexedDBFiles(); - } catch (error) { - window.log.error( - 'Failed to remove IndexedDB files:', - error && error.stack ? error.stack : error - ); - } - - window.installStorage(window.newStorage); - await window.storage.fetch(); - await storage.put('indexeddb-delete-needed', true); - } - Views.Initialization.setMessage(window.i18n('optimizingApplication')); Views.Initialization.setMessage(window.i18n('loading')); - idleDetector = new IdleDetector(); - let isMigrationWithIndexComplete = false; - window.log.info( - `Starting background data migration. Target version: ${Message.CURRENT_SCHEMA_VERSION}` - ); - idleDetector.on('idle', async () => { - const NUM_MESSAGES_PER_BATCH = 1; - - if (!isMigrationWithIndexComplete) { - const batchWithIndex = await MessageDataMigrator.processNext({ - BackboneMessage: window.models.Message.MessageModel, - BackboneMessageCollection: window.models.Message.MessageCollection, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - getMessagesNeedingUpgrade: - window.Signal.Data.getMessagesNeedingUpgrade, - saveMessage: window.Signal.Data.saveMessage, - }); - window.log.info('Upgrade message schema (with index):', batchWithIndex); - isMigrationWithIndexComplete = batchWithIndex.done; - } - - if (isMigrationWithIndexComplete) { - window.log.info( - 'Background migration complete. Stopping idle detector.' - ); - idleDetector.stop(); - } - }); - const themeSetting = window.Events.getThemeSetting(); const newThemeSetting = mapOldThemeToNew(themeSetting); window.Events.setThemeSetting(newThemeSetting); @@ -351,7 +274,6 @@ try { await Promise.all([ window.getConversationController().load(), - textsecure.storage.protocol.hydrateCaches(), BlockedNumberController.load(), ]); } catch (error) { @@ -706,66 +628,6 @@ window.setMediaPermissions(!value); }; - // Attempts a connection to an open group server - window.attemptConnection = async (serverURL, channelId) => { - let completeServerURL = serverURL.toLowerCase(); - const valid = window.libsession.Types.OpenGroup.validate( - completeServerURL - ); - if (!valid) { - return new Promise((_resolve, reject) => { - reject(window.i18n('connectToServerFail')); - }); - } - - // Add http or https prefix to server - completeServerURL = window.libsession.Types.OpenGroup.prefixify( - completeServerURL - ); - - const rawServerURL = serverURL - .replace(/^https?:\/\//i, '') - .replace(/[/\\]+$/i, ''); - - const conversationId = `publicChat:${channelId}@${rawServerURL}`; - - // Quickly peak to make sure we don't already have it - const conversationExists = window - .getConversationController() - .get(conversationId); - if (conversationExists) { - // We are already a member of this public chat - return new Promise((_resolve, reject) => { - reject(window.i18n('publicChatExists')); - }); - } - - // Get server - const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer( - completeServerURL - ); - // SSL certificate failure or offline - if (!serverAPI) { - // Url incorrect or server not compatible - return new Promise((_resolve, reject) => { - reject(window.i18n('connectToServerFail')); - }); - } - - // Create conversation - const conversation = await window - .getConversationController() - .getOrCreateAndWait(conversationId, 'group'); - - // Convert conversation to a public one - await conversation.setPublicSource(completeServerURL, channelId); - - // and finally activate it - conversation.getPublicSendData(); // may want "await" if you want to use the API - - return conversation; - }; - Whisper.events.on('updateGroupName', async groupConvo => { if (appView) { appView.showUpdateGroupNameDialog(groupConvo); @@ -1046,10 +908,6 @@ }); window.textsecure.messaging = true; - - storage.onready(async () => { - idleDetector.start(); - }); } function onEmpty() { diff --git a/js/database.js b/js/database.js index 8a53d11e8f..5fc9b18967 100644 --- a/js/database.js +++ b/js/database.js @@ -7,8 +7,6 @@ (function() { 'use strict'; - const { getPlaceholderMigrations } = window.Signal.Migrations; - window.Whisper = window.Whisper || {}; window.Whisper.Database = window.Whisper.Database || {}; window.Whisper.Database.id = window.Whisper.Database.id || 'loki-messenger'; @@ -125,6 +123,4 @@ request.onsuccess = resolve; }); - - Whisper.Database.migrations = getPlaceholderMigrations(); })(); diff --git a/js/delivery_receipts.js b/js/delivery_receipts.js index fdc419a11f..b3522b015e 100644 --- a/js/delivery_receipts.js +++ b/js/delivery_receipts.js @@ -43,11 +43,7 @@ } const groups = await window.Signal.Data.getAllGroupsInvolvingId( - originalSource, - { - ConversationCollection: - window.models.Conversation.ConversationCollection, - } + originalSource ); const ids = groups.pluck('id'); diff --git a/js/modules/attachment_downloads.js b/js/modules/attachment_downloads.js index 2c2a99085e..9ae463c324 100644 --- a/js/modules/attachment_downloads.js +++ b/js/modules/attachment_downloads.js @@ -10,7 +10,7 @@ const { saveAttachmentDownloadJob, saveMessage, setAttachmentDownloadJobPending, -} = require('./data'); +} = require('../../ts/data/data'); const { stringFromBytes } = require('./crypto'); module.exports = { diff --git a/js/modules/backup.js b/js/modules/backup.js index d3f5bae5b6..cf971f3729 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -141,9 +141,7 @@ async function exportConversationList(fileWriter) { stream.write('{'); stream.write('"conversations": '); - const conversations = await window.Signal.Data.getAllConversations({ - ConversationCollection: window.models.Conversation.ConversationCollection, - }); + const conversations = await window.Signal.Data.getAllConversations(); window.log.info(`Exporting ${conversations.length} conversations`); writeArray(stream, getPlainJS(conversations)); @@ -257,11 +255,7 @@ async function importFromJsonString(jsonString, targetPath, options) { await importConversationsFromJSON(conversations, options); const SAVE_FUNCTIONS = { - identityKeys: window.Signal.Data.createOrUpdateIdentityKey, items: window.Signal.Data.createOrUpdateItem, - preKeys: window.Signal.Data.createOrUpdatePreKey, - sessions: window.Signal.Data.createOrUpdateSession, - signedPreKeys: window.Signal.Data.createOrUpdateSignedPreKey, }; await Promise.all( @@ -839,9 +833,7 @@ async function exportConversations(options) { throw new Error('Need an attachments directory!'); } - const collection = await window.Signal.Data.getAllConversations({ - ConversationCollection: window.models.Conversation.ConversationCollection, - }); + const collection = await window.Signal.Data.getAllConversations(); const conversations = collection.models; for (let i = 0, max = conversations.length; i < max; i += 1) { diff --git a/js/modules/data.d.ts b/js/modules/data.d.ts deleted file mode 100644 index e87d438ce2..0000000000 --- a/js/modules/data.d.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { KeyPair } from '../../libtextsecure/libsignal-protocol'; -import { MessageCollection } from '../../ts/models/message'; -import { HexKeyPair } from '../../ts/receiver/closedGroups'; -import { ECKeyPair } from '../../ts/receiver/keypairs'; -import { PubKey } from '../../ts/session/types'; -import { ConversationType } from '../../ts/state/ducks/conversations'; -import { Message } from '../../ts/types/Message'; - -export type IdentityKey = { - id: string; - publicKey: ArrayBuffer; - firstUse: boolean; - nonblockingApproval: boolean; - secretKey?: string; // found in medium groups -}; - -export type PreKey = { - id: number; - publicKey: ArrayBuffer; - privateKey: ArrayBuffer; - recipient: string; -}; - -export type SignedPreKey = { - id: number; - publicKey: ArrayBuffer; - privateKey: ArrayBuffer; - created_at: number; - confirmed: boolean; - signature: ArrayBuffer; -}; - -export type ContactPreKey = { - id: number; - identityKeyString: string; - publicKey: ArrayBuffer; - keyId: number; -}; - -export type ContactSignedPreKey = { - id: number; - identityKeyString: string; - publicKey: ArrayBuffer; - keyId: number; - signature: ArrayBuffer; - created_at: number; - confirmed: boolean; -}; - -export type GuardNode = { - ed25519PubKey: string; -}; - -export type SwarmNode = { - address: string; - ip: string; - port: string; - pubkey_ed25519: string; - pubkey_x25519: string; -}; - -export type StorageItem = { - id: string; - value: any; -}; - -export type SessionDataInfo = { - id: string; - number: string; - deviceId: number; - record: string; -}; - -export type ServerToken = { - serverUrl: string; - token: string; -}; - -// Basic -export function searchMessages(query: string): Promise>; -export function searchConversations(query: string): Promise>; -export function shutdown(): Promise; -export function close(): Promise; -export function removeDB(): Promise; -export function removeIndexedDBFiles(): Promise; -export function getPasswordHash(): Promise; - -// Identity Keys -// TODO: identity key has different shape depending on how it is called, -// so we need to come up with a way to make TS work with all of them -export function createOrUpdateIdentityKey(data: any): Promise; -export function getIdentityKeyById(id: string): Promise; -export function bulkAddIdentityKeys(array: Array): Promise; -export function removeIdentityKeyById(id: string): Promise; -export function removeAllIdentityKeys(): Promise; - -// Pre Keys -export function createOrUpdatePreKey(data: PreKey): Promise; -export function getPreKeyById(id: number): Promise; -export function getPreKeyByRecipient(recipient: string): Promise; -export function bulkAddPreKeys(data: Array): Promise; -export function removePreKeyById(id: number): Promise; -export function getAllPreKeys(): Promise>; - -// Signed Pre Keys -export function createOrUpdateSignedPreKey(data: SignedPreKey): Promise; -export function getSignedPreKeyById(id: number): Promise; -export function getAllSignedPreKeys(): Promise; -export function bulkAddSignedPreKeys(array: Array): Promise; -export function removeSignedPreKeyById(id: number): Promise; -export function removeAllSignedPreKeys(): Promise; - -// Contact Pre Key -export function createOrUpdateContactPreKey(data: ContactPreKey): Promise; -export function getContactPreKeyById(id: number): Promise; -export function getContactPreKeyByIdentityKey( - key: string -): Promise; -export function getContactPreKeys( - keyId: number, - identityKeyString: string -): Promise>; -export function getAllContactPreKeys(): Promise>; -export function bulkAddContactPreKeys( - array: Array -): Promise; -export function removeContactPreKeyByIdentityKey(id: number): Promise; -export function removeAllContactPreKeys(): Promise; - -// Contact Signed Pre Key -export function createOrUpdateContactSignedPreKey( - data: ContactSignedPreKey -): Promise; -export function getContactSignedPreKeyById( - id: number -): Promise; -export function getContactSignedPreKeyByIdentityKey( - key: string -): Promise; -export function getContactSignedPreKeys( - keyId: number, - identityKeyString: string -): Promise>; -export function bulkAddContactSignedPreKeys( - array: Array -): Promise; -export function removeContactSignedPreKeyByIdentityKey( - id: string -): Promise; -export function removeAllContactSignedPreKeys(): Promise; - -// Guard Nodes -export function getGuardNodes(): Promise>; -export function updateGuardNodes(nodes: Array): Promise; - -// Storage Items -export function createOrUpdateItem(data: StorageItem): Promise; -export function getItemById(id: string): Promise; -export function getAlItems(): Promise>; -export function bulkAddItems(array: Array): Promise; -export function removeItemById(id: string): Promise; -export function removeAllItems(): Promise; - -// Sessions -export function createOrUpdateSession(data: SessionDataInfo): Promise; -export function getAllSessions(): Promise>; -export function getSessionById(id: string): Promise; -export function getSessionsByNumber(number: string): Promise; -export function bulkAddSessions(array: Array): Promise; -export function removeSessionById(id: string): Promise; -export function removeSessionsByNumber(number: string): Promise; -export function removeAllSessions(): Promise; - -// Conversations -export function getConversationCount(): Promise; -export function saveConversation(data: ConversationType): Promise; -export function saveConversations(data: Array): Promise; -export function updateConversation( - id: string, - data: ConversationType, - { Conversation } -): Promise; -export function removeConversation(id: string, { Conversation }): Promise; - -export function getAllConversations({ - ConversationCollection, -}: { - ConversationCollection: any; -}): Promise; - -export function getAllConversationIds(): Promise>; -export function getPublicConversationsByServer( - server: string, - { ConversationCollection }: { ConversationCollection: any } -): Promise; -export function getPubkeysInPublicConversation( - id: string -): Promise>; -export function savePublicServerToken(data: ServerToken): Promise; -export function getPublicServerTokenByServerUrl( - serverUrl: string -): Promise; -export function getAllGroupsInvolvingId( - id: string, - { ConversationCollection }: { ConversationCollection: any } -): Promise; - -// Returns conversation row -// TODO: Make strict return types for search -export function searchConversations(query: string): Promise; -export function searchMessages(query: string): Promise; -export function searchMessagesInConversation( - query: string, - conversationId: string, - { limit }?: { limit: any } -): Promise; -export function saveMessage( - data: Mesasge, - { forceSave, Message }?: { forceSave?: any; Message?: any } -): Promise; -export function cleanSeenMessages(): Promise; -export function cleanLastHashes(): Promise; -export function saveSeenMessageHash(data: { - expiresAt: number; - hash: string; -}): Promise; - -export function getSwarmNodesForPubkey(pubkey: string): Promise>; -export function updateSwarmNodesForPubkey( - pubkey: string, - snodeEdKeys: Array -): Promise; -// TODO: Strictly type the following -export function updateLastHash(data: any): Promise; -export function saveSeenMessageHashes(data: any): Promise; -export function saveLegacyMessage(data: any): Promise; -export function saveMessages( - arrayOfMessages: any, - { forceSave }?: any -): Promise; -export function removeMessage(id: string, { Message }?: any): Promise; -export function getUnreadByConversation( - conversationId: string, - { MessageCollection }?: any -): Promise; -export function getUnreadCountByConversation( - conversationId: string -): Promise; -export function removeAllMessagesInConversation( - conversationId: string, - { MessageCollection }?: any -): Promise; - -export function getMessageBySender( - { - source, - sourceDevice, - sent_at, - }: { source: any; sourceDevice: any; sent_at: any }, - { Message }: { Message: any } -): Promise; -export function getMessagesBySender( - { source, sourceDevice }: { source: any; sourceDevice: any }, - { Message }: { Message: any } -): Promise; -export function getMessageIdsFromServerIds( - serverIds: any, - conversationId: any -): Promise; -export function getMessageById( - id: string, - { Message }: { Message: any } -): Promise; -export function getAllMessages({ - MessageCollection, -}: { - MessageCollection: any; -}): Promise; -export function getAllUnsentMessages({ - MessageCollection, -}: { - MessageCollection: any; -}): Promise; -export function getAllMessageIds(): Promise; -export function getMessagesBySentAt( - sentAt: any, - { MessageCollection }: { MessageCollection: any } -): Promise; -export function getExpiredMessages({ - MessageCollection, -}: { - MessageCollection: any; -}): Promise; -export function getOutgoingWithoutExpiresAt({ - MessageCollection, -}: any): Promise; -export function getNextExpiringMessage({ - MessageCollection, -}: { - MessageCollection: any; -}): Promise; -export function getNextExpiringMessage({ - MessageCollection, -}: { - MessageCollection: any; -}): Promise; -export function getMessagesByConversation( - conversationId: any, - { - limit, - receivedAt, - MessageCollection, - type, - }: { - limit?: number; - receivedAt?: number; - MessageCollection: any; - type?: string; - } -): Promise; - -export function getSeenMessagesByHashList(hashes: any): Promise; -export function getLastHashBySnode(convoId: any, snode: any): Promise; - -// Unprocessed -export function getUnprocessedCount(): Promise; -export function getAllUnprocessed(): Promise; -export function getUnprocessedById(id: any): Promise; -export function saveUnprocessed( - data: any, - { - forceSave, - }?: { - forceSave: any; - } -): Promise; -export function saveUnprocesseds( - arrayOfUnprocessed: any, - { - forceSave, - }?: { - forceSave: any; - } -): Promise; -export function updateUnprocessedAttempts( - id: any, - attempts: any -): Promise; -export function updateUnprocessedWithData(id: any, data: any): Promise; -export function removeUnprocessed(id: any): Promise; -export function removeAllUnprocessed(): Promise; - -// Attachment Downloads -export function getNextAttachmentDownloadJobs(limit: any): Promise; -export function saveAttachmentDownloadJob(job: any): Promise; -export function setAttachmentDownloadJobPending( - id: any, - pending: any -): Promise; -export function resetAttachmentDownloadPending(): Promise; -export function removeAttachmentDownloadJob(id: any): Promise; -export function removeAllAttachmentDownloadJobs(): Promise; - -// Other -export function removeAll(): Promise; -export function removeAllConfiguration(): Promise; -export function removeAllConversations(): Promise; -export function removeAllPrivateConversations(): Promise; -export function removeOtherData(): Promise; -export function cleanupOrphanedAttachments(): Promise; - -// Getters -export function getMessagesNeedingUpgrade( - limit: any, - { - maxVersion, - }: { - maxVersion?: number; - } -): Promise; -export function getLegacyMessagesNeedingUpgrade( - limit: any, - { - maxVersion, - }: { - maxVersion?: number; - } -): Promise; -export function getMessagesWithVisualMediaAttachments( - conversationId: any, - { - limit, - }: { - limit: any; - } -): Promise; -export function getMessagesWithFileAttachments( - conversationId: any, - { - limit, - }: { - limit: any; - } -): Promise; - -// Sender Keys -export function removeAllClosedGroupRatchets(groupId: string): Promise; - -export function getAllEncryptionKeyPairsForGroup( - groupPublicKey: string | PubKey -): Promise | undefined>; -export function isKeyPairAlreadySaved( - groupPublicKey: string, - keypair: HexKeyPair -): Promise; -export function getLatestClosedGroupEncryptionKeyPair( - groupPublicKey: string -): Promise; -export function addClosedGroupEncryptionKeyPair( - groupPublicKey: string, - keypair: HexKeyPair -): Promise; -export function removeAllClosedGroupEncryptionKeyPairs( - groupPublicKey: string -): Promise; diff --git a/js/modules/data.js b/js/modules/data.js deleted file mode 100644 index ee3d0e15ee..0000000000 --- a/js/modules/data.js +++ /dev/null @@ -1,1282 +0,0 @@ -/* global window, setTimeout, clearTimeout, IDBKeyRange */ -const electron = require('electron'); - -const { ipcRenderer } = electron; - -// TODO: this results in poor readability, would be -// much better to explicitly call with `_`. -const { - cloneDeep, - forEach, - get, - isFunction, - isObject, - map, - set, - omit, -} = require('lodash'); - -const _ = require('lodash'); - -const { base64ToArrayBuffer, arrayBufferToBase64 } = require('./crypto'); -const MessageType = require('./types/message'); - -const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes - -const SQL_CHANNEL_KEY = 'sql-channel'; -const ERASE_SQL_KEY = 'erase-sql-key'; -const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; -const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments'; - -const _jobs = Object.create(null); -const _DEBUG = false; -let _jobCounter = 0; -let _shuttingDown = false; -let _shutdownCallback = null; -let _shutdownPromise = null; - -const channels = {}; - -module.exports = { - init, - _jobs, - _cleanData, - - shutdown, - close, - removeDB, - removeIndexedDBFiles, - getPasswordHash, - - createOrUpdateIdentityKey, - getIdentityKeyById, - bulkAddIdentityKeys, - removeIdentityKeyById, - removeAllIdentityKeys, - getAllIdentityKeys, - - createOrUpdatePreKey, - getPreKeyById, - getPreKeyByRecipient, - bulkAddPreKeys, - removePreKeyById, - removeAllPreKeys, - getAllPreKeys, - - createOrUpdateSignedPreKey, - getSignedPreKeyById, - getAllSignedPreKeys, - bulkAddSignedPreKeys, - removeSignedPreKeyById, - removeAllSignedPreKeys, - - createOrUpdateContactPreKey, - getContactPreKeyById, - getContactPreKeyByIdentityKey, - getContactPreKeys, - getAllContactPreKeys, - bulkAddContactPreKeys, - removeContactPreKeyByIdentityKey, - removeAllContactPreKeys, - - createOrUpdateContactSignedPreKey, - getContactSignedPreKeyById, - getContactSignedPreKeyByIdentityKey, - getContactSignedPreKeys, - bulkAddContactSignedPreKeys, - removeContactSignedPreKeyByIdentityKey, - removeAllContactSignedPreKeys, - - getGuardNodes, - updateGuardNodes, - - createOrUpdateItem, - getItemById, - getAllItems, - bulkAddItems, - removeItemById, - removeAllItems, - - createOrUpdateSession, - getSessionById, - getSessionsByNumber, - bulkAddSessions, - removeSessionById, - removeSessionsByNumber, - removeAllSessions, - getAllSessions, - - getSwarmNodesForPubkey, - updateSwarmNodesForPubkey, - - getConversationCount, - saveConversation, - saveConversations, - getConversationById, - updateConversation, - removeConversation, - _removeConversations, - - getAllConversations, - getAllConversationIds, - getAllPublicConversations, - getPublicConversationsByServer, - getPubkeysInPublicConversation, - savePublicServerToken, - getPublicServerTokenByServerUrl, - getAllGroupsInvolvingId, - - searchConversations, - searchMessages, - searchMessagesInConversation, - - saveMessage, - cleanSeenMessages, - cleanLastHashes, - saveSeenMessageHash, - updateLastHash, - saveSeenMessageHashes, - saveLegacyMessage, - saveMessages, - removeMessage, - _removeMessages, - getUnreadByConversation, - getUnreadCountByConversation, - - removeAllMessagesInConversation, - - getMessageBySender, - getMessagesBySender, - getMessageIdsFromServerIds, - getMessageById, - getAllMessages, - getAllUnsentMessages, - getAllMessageIds, - getMessagesBySentAt, - getExpiredMessages, - getOutgoingWithoutExpiresAt, - getNextExpiringMessage, - getMessagesByConversation, - getSeenMessagesByHashList, - getLastHashBySnode, - - getUnprocessedCount, - getAllUnprocessed, - getUnprocessedById, - saveUnprocessed, - saveUnprocesseds, - updateUnprocessedAttempts, - updateUnprocessedWithData, - removeUnprocessed, - removeAllUnprocessed, - - getNextAttachmentDownloadJobs, - saveAttachmentDownloadJob, - resetAttachmentDownloadPending, - setAttachmentDownloadJobPending, - removeAttachmentDownloadJob, - removeAllAttachmentDownloadJobs, - - removeAll, - removeAllConfiguration, - removeAllConversations, - removeAllPrivateConversations, - - removeOtherData, - cleanupOrphanedAttachments, - - // Returning plain JSON - getMessagesNeedingUpgrade, - getLegacyMessagesNeedingUpgrade, - getMessagesWithVisualMediaAttachments, - getMessagesWithFileAttachments, - - removeAllClosedGroupRatchets, - - getAllEncryptionKeyPairsForGroup, - getLatestClosedGroupEncryptionKeyPair, - addClosedGroupEncryptionKeyPair, - isKeyPairAlreadySaved, - removeAllClosedGroupEncryptionKeyPairs, -}; - -function init() { - // We listen to a lot of events on ipcRenderer, often on the same channel. This prevents - // any warnings that might be sent to the console in that case. - ipcRenderer.setMaxListeners(0); - - forEach(module.exports, fn => { - if (isFunction(fn) && fn.name !== 'init') { - makeChannel(fn.name); - } - }); - - ipcRenderer.on( - `${SQL_CHANNEL_KEY}-done`, - (event, jobId, errorForDisplay, result) => { - const job = _getJob(jobId); - if (!job) { - throw new Error( - `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` - ); - } - - const { resolve, reject, fnName } = job; - - if (errorForDisplay) { - return reject( - new Error( - `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` - ) - ); - } - - return resolve(result); - } - ); -} - -// When IPC arguments are prepared for the cross-process send, they are JSON.stringified. -// We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). -function _cleanData(data) { - const keys = Object.keys(data); - for (let index = 0, max = keys.length; index < max; index += 1) { - const key = keys[index]; - const value = data[key]; - - if (value === null || value === undefined) { - // eslint-disable-next-line no-continue - continue; - } - - if (isFunction(value.toNumber)) { - // eslint-disable-next-line no-param-reassign - data[key] = value.toNumber(); - } else if (Array.isArray(value)) { - // eslint-disable-next-line no-param-reassign - data[key] = value.map(item => _cleanData(item)); - } else if (isObject(value)) { - // eslint-disable-next-line no-param-reassign - data[key] = _cleanData(value); - } else if ( - typeof value !== 'string' && - typeof value !== 'number' && - typeof value !== 'boolean' - ) { - window.log.info(`_cleanData: key ${key} had type ${typeof value}`); - } - } - return data; -} - -async function _shutdown() { - if (_shutdownPromise) { - return _shutdownPromise; - } - - _shuttingDown = true; - - const jobKeys = Object.keys(_jobs); - window.log.info( - `data.shutdown: starting process. ${jobKeys.length} jobs outstanding` - ); - - // No outstanding jobs, return immediately - if (jobKeys.length === 0) { - return null; - } - - // Outstanding jobs; we need to wait until the last one is done - _shutdownPromise = new Promise((resolve, reject) => { - _shutdownCallback = error => { - window.log.info('data.shutdown: process complete'); - if (error) { - return reject(error); - } - - return resolve(); - }; - }); - - return _shutdownPromise; -} - -function _makeJob(fnName) { - if (_shuttingDown && fnName !== 'close') { - throw new Error( - `Rejecting SQL channel job (${fnName}); application is shutting down` - ); - } - - _jobCounter += 1; - const id = _jobCounter; - - if (_DEBUG) { - window.log.debug(`SQL channel job ${id} (${fnName}) started`); - } - _jobs[id] = { - fnName, - start: Date.now(), - }; - - return id; -} - -function _updateJob(id, data) { - const { resolve, reject } = data; - const { fnName, start } = _jobs[id]; - - _jobs[id] = { - ..._jobs[id], - ...data, - resolve: value => { - _removeJob(id); - // const end = Date.now(); - // const delta = end - start; - // if (delta > 10) { - // window.log.debug( - // `SQL channel job ${id} (${fnName}) succeeded in ${end - start}ms` - // ); - // } - return resolve(value); - }, - reject: error => { - _removeJob(id); - const end = Date.now(); - window.log.warn( - `SQL channel job ${id} (${fnName}) failed in ${end - start}ms` - ); - return reject(error); - }, - }; -} - -function _removeJob(id) { - if (_DEBUG) { - _jobs[id].complete = true; - return; - } - - if (_jobs[id].timer) { - clearTimeout(_jobs[id].timer); - _jobs[id].timer = null; - } - - delete _jobs[id]; - - if (_shutdownCallback) { - const keys = Object.keys(_jobs); - if (keys.length === 0) { - _shutdownCallback(); - } - } -} - -function _getJob(id) { - return _jobs[id]; -} - -function makeChannel(fnName) { - channels[fnName] = (...args) => { - const jobId = _makeJob(fnName); - - return new Promise((resolve, reject) => { - ipcRenderer.send(SQL_CHANNEL_KEY, jobId, fnName, ...args); - - _updateJob(jobId, { - resolve, - reject, - args: _DEBUG ? args : null, - }); - - _jobs[jobId].timer = setTimeout( - () => - reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)), - DATABASE_UPDATE_TIMEOUT - ); - }); - }; -} - -function keysToArrayBuffer(keys, data) { - 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, base64ToArrayBuffer(value)); - } - } - - return updated; -} - -function keysFromArrayBuffer(keys, data) { - 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, arrayBufferToBase64(value)); - } - } - - return updated; -} - -// Top-level calls - -async function shutdown() { - // Stop accepting new SQL jobs, flush outstanding queue - await _shutdown(); - - // Close database - await close(); -} - -// Note: will need to restart the app after calling this, to set up afresh -async function close() { - await channels.close(); -} - -// Note: will need to restart the app after calling this, to set up afresh -async function removeDB() { - await channels.removeDB(); -} - -async function removeIndexedDBFiles() { - await channels.removeIndexedDBFiles(); -} - -// Password hash - -async function getPasswordHash() { - return channels.getPasswordHash(); -} - -// Identity Keys - -const IDENTITY_KEY_KEYS = ['publicKey']; -async function createOrUpdateIdentityKey(data) { - const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data); - await channels.createOrUpdateIdentityKey(updated); -} -async function getIdentityKeyById(id) { - const data = await channels.getIdentityKeyById(id); - return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); -} -async function bulkAddIdentityKeys(array) { - const updated = map(array, data => - keysFromArrayBuffer(IDENTITY_KEY_KEYS, data) - ); - await channels.bulkAddIdentityKeys(updated); -} -async function removeIdentityKeyById(id) { - await channels.removeIdentityKeyById(id); -} -async function removeAllIdentityKeys() { - await channels.removeAllIdentityKeys(); -} -async function getAllIdentityKeys() { - const keys = await channels.getAllIdentityKeys(); - return keys.map(key => keysToArrayBuffer(IDENTITY_KEY_KEYS, key)); -} - -// Pre Keys - -async function createOrUpdatePreKey(data) { - const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); - await channels.createOrUpdatePreKey(updated); -} -async function getPreKeyById(id) { - const data = await channels.getPreKeyById(id); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function getPreKeyByRecipient(recipient) { - const data = await channels.getPreKeyByRecipient(recipient); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function bulkAddPreKeys(array) { - const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); - await channels.bulkAddPreKeys(updated); -} -async function removePreKeyById(id) { - await channels.removePreKeyById(id); -} -async function removeAllPreKeys() { - await channels.removeAllPreKeys(); -} -async function getAllPreKeys() { - const keys = await channels.getAllPreKeys(); - return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); -} - -// Signed Pre Keys - -const PRE_KEY_KEYS = ['privateKey', 'publicKey', 'signature']; -async function createOrUpdateSignedPreKey(data) { - const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); - await channels.createOrUpdateSignedPreKey(updated); -} -async function getSignedPreKeyById(id) { - const data = await channels.getSignedPreKeyById(id); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function getAllSignedPreKeys() { - const keys = await channels.getAllSignedPreKeys(); - return keys.map(key => keysToArrayBuffer(PRE_KEY_KEYS, key)); -} -async function bulkAddSignedPreKeys(array) { - const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); - await channels.bulkAddSignedPreKeys(updated); -} -async function removeSignedPreKeyById(id) { - await channels.removeSignedPreKeyById(id); -} -async function removeAllSignedPreKeys() { - await channels.removeAllSignedPreKeys(); -} - -// Contact Pre Key -async function createOrUpdateContactPreKey(data) { - const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); - await channels.createOrUpdateContactPreKey(updated); -} -async function getContactPreKeyById(id) { - const data = await channels.getContactPreKeyById(id); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function getContactPreKeyByIdentityKey(key) { - const data = await channels.getContactPreKeyByIdentityKey(key); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function getContactPreKeys(keyId, identityKeyString) { - const keys = await channels.getContactPreKeys(keyId, identityKeyString); - return keys.map(k => keysToArrayBuffer(PRE_KEY_KEYS, k)); -} -async function getAllContactPreKeys() { - const keys = await channels.getAllContactPreKeys(); - return keys; -} -async function bulkAddContactPreKeys(array) { - const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); - await channels.bulkAddContactPreKeys(updated); -} -async function removeContactPreKeyByIdentityKey(id) { - await channels.removeContactPreKeyByIdentityKey(id); -} -async function removeAllContactPreKeys() { - await channels.removeAllContactPreKeys(); -} - -// Contact Signed Pre Key -async function createOrUpdateContactSignedPreKey(data) { - const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data); - await channels.createOrUpdateContactSignedPreKey(updated); -} -async function getContactSignedPreKeyById(id) { - const data = await channels.getContactSignedPreKeyById(id); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function getContactSignedPreKeyByIdentityKey(key) { - const data = await channels.getContactSignedPreKeyByIdentityKey(key); - return keysToArrayBuffer(PRE_KEY_KEYS, data); -} -async function getContactSignedPreKeys(keyId, identityKeyString) { - const keys = await channels.getContactSignedPreKeys(keyId, identityKeyString); - return keys.map(k => keysToArrayBuffer(PRE_KEY_KEYS, k)); -} -async function bulkAddContactSignedPreKeys(array) { - const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data)); - await channels.bulkAddContactSignedPreKeys(updated); -} -async function removeContactSignedPreKeyByIdentityKey(id) { - await channels.removeContactSignedPreKeyByIdentityKey(id); -} -async function removeAllContactSignedPreKeys() { - await channels.removeAllContactSignedPreKeys(); -} - -function getGuardNodes() { - return channels.getGuardNodes(); -} - -function updateGuardNodes(nodes) { - return channels.updateGuardNodes(nodes); -} - -// Items - -const ITEM_KEYS = { - identityKey: ['value.pubKey', 'value.privKey'], - senderCertificate: [ - 'value.certificate', - 'value.signature', - 'value.serialized', - ], - signaling_key: ['value'], - profileKey: ['value'], -}; -async function createOrUpdateItem(data) { - const { id } = data; - if (!id) { - throw new Error( - 'createOrUpdateItem: Provided data did not have a truthy id' - ); - } - - const keys = ITEM_KEYS[id]; - const updated = Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; - - await channels.createOrUpdateItem(updated); -} -async function getItemById(id) { - const keys = ITEM_KEYS[id]; - const data = await channels.getItemById(id); - - return Array.isArray(keys) ? keysToArrayBuffer(keys, data) : data; -} -async function getAllItems() { - const items = await channels.getAllItems(); - return map(items, item => { - const { id } = item; - const keys = ITEM_KEYS[id]; - return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item; - }); -} -async function bulkAddItems(array) { - const updated = map(array, data => { - const { id } = data; - const keys = ITEM_KEYS[id]; - return Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; - }); - await channels.bulkAddItems(updated); -} -async function removeItemById(id) { - await channels.removeItemById(id); -} -async function removeAllItems() { - await channels.removeAllItems(); -} - -// Sender Keys -async function removeAllClosedGroupRatchets(groupId) { - await channels.removeAllClosedGroupRatchets(groupId); -} - -// Sessions - -async function createOrUpdateSession(data) { - await channels.createOrUpdateSession(data); -} -async function getSessionById(id) { - const session = await channels.getSessionById(id); - return session; -} -async function getSessionsByNumber(number) { - const sessions = await channels.getSessionsByNumber(number); - return sessions; -} -async function bulkAddSessions(array) { - await channels.bulkAddSessions(array); -} -async function removeSessionById(id) { - await channels.removeSessionById(id); -} -async function removeSessionsByNumber(number) { - await channels.removeSessionsByNumber(number); -} -async function removeAllSessions(id) { - await channels.removeAllSessions(id); -} -async function getAllSessions(id) { - const sessions = await channels.getAllSessions(id); - return sessions; -} - -// Swarm nodes - -async function getSwarmNodesForPubkey(pubkey) { - return channels.getSwarmNodesForPubkey(pubkey); -} - -async function updateSwarmNodesForPubkey(pubkey, snodeEdKeys) { - await channels.updateSwarmNodesForPubkey(pubkey, snodeEdKeys); -} - -// Closed group - -/** - * The returned array is ordered based on the timestamp, the latest is at the end. - * @param {*} groupPublicKey - */ -async function getAllEncryptionKeyPairsForGroup(groupPublicKey) { - return channels.getAllEncryptionKeyPairsForGroup(groupPublicKey); -} - -async function getLatestClosedGroupEncryptionKeyPair(groupPublicKey) { - return channels.getLatestClosedGroupEncryptionKeyPair(groupPublicKey); -} - -async function addClosedGroupEncryptionKeyPair(groupPublicKey, keypair) { - return channels.addClosedGroupEncryptionKeyPair(groupPublicKey, keypair); -} - -async function isKeyPairAlreadySaved(groupPublicKey, keypair) { - return channels.isKeyPairAlreadySaved(groupPublicKey, keypair); -} - -async function removeAllClosedGroupEncryptionKeyPairs(groupPublicKey) { - return channels.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); -} - -// Conversation -async function getConversationCount() { - return channels.getConversationCount(); -} - -async function saveConversation(data) { - const cleaned = omit(data, 'isOnline'); - await channels.saveConversation(cleaned); -} - -async function saveConversations(data) { - const cleaned = data.map(d => omit(d, 'isOnline')); - await channels.saveConversations(cleaned); -} - -async function getConversationById(id, { Conversation }) { - const data = await channels.getConversationById(id); - return new Conversation(data); -} - -async function updateConversation(id, data, { Conversation }) { - const existing = await getConversationById(id, { Conversation }); - if (!existing) { - throw new Error(`Conversation ${id} does not exist!`); - } - - const merged = _.merge({}, existing.attributes, data); - - // Merging is a really bad idea and not what we want here, e.g. - // it will take a union of old and new members and that's not - // what we want for member deletion, so: - merged.members = data.members; - - // Don't save the online status of the object - const cleaned = omit(merged, 'isOnline'); - await channels.updateConversation(cleaned); -} - -async function removeConversation(id, { Conversation }) { - const existing = await getConversationById(id, { Conversation }); - - // 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(); - } -} - -// Note: this method will not clean up external files, just delete from SQL -async function _removeConversations(ids) { - await channels.removeConversation(ids); -} - -async function getAllConversations({ ConversationCollection }) { - const conversations = await channels.getAllConversations(); - - const collection = new ConversationCollection(); - collection.add(conversations); - return collection; -} - -async function getAllConversationIds() { - const ids = await channels.getAllConversationIds(); - return ids; -} - -async function getAllPublicConversations({ ConversationCollection }) { - const conversations = await channels.getAllPublicConversations(); - - const collection = new ConversationCollection(); - collection.add(conversations); - return collection; -} - -async function getPubkeysInPublicConversation(id) { - return channels.getPubkeysInPublicConversation(id); -} - -async function savePublicServerToken(data) { - await channels.savePublicServerToken(data); -} - -async function getPublicServerTokenByServerUrl(serverUrl) { - const token = await channels.getPublicServerTokenByServerUrl(serverUrl); - return token; -} - -async function getPublicConversationsByServer( - server, - { ConversationCollection } -) { - const conversations = await channels.getPublicConversationsByServer(server); - - const collection = new ConversationCollection(); - collection.add(conversations); - return collection; -} - -async function getAllGroupsInvolvingId(id, { ConversationCollection }) { - const conversations = await channels.getAllGroupsInvolvingId(id); - - const collection = new ConversationCollection(); - collection.add(conversations); - return collection; -} - -async function searchConversations(query) { - const conversations = await channels.searchConversations(query); - return conversations; -} - -async function searchMessages(query, { limit } = {}) { - const messages = await channels.searchMessages(query, { limit }); - return messages; -} - -async function searchMessagesInConversation( - query, - conversationId, - { limit } = {} -) { - const messages = await channels.searchMessagesInConversation( - query, - conversationId, - { limit } - ); - return messages; -} - -// Message - -async function cleanSeenMessages() { - await channels.cleanSeenMessages(); -} - -async function cleanLastHashes() { - await channels.cleanLastHashes(); -} - -async function saveSeenMessageHashes(data) { - await channels.saveSeenMessageHashes(_cleanData(data)); -} - -async function updateLastHash(data) { - await channels.updateLastHash(_cleanData(data)); -} - -async function saveSeenMessageHash(data) { - await channels.saveSeenMessageHash(_cleanData(data)); -} - -async function saveMessage(data, { forceSave } = {}) { - const id = await channels.saveMessage(_cleanData(data), { forceSave }); - window.Whisper.ExpiringMessagesListener.update(); - return id; -} - -async function saveLegacyMessage(data) { - const db = await window.Whisper.Database.open(); - try { - await new Promise((resolve, reject) => { - const transaction = db.transaction('messages', 'readwrite'); - - transaction.onerror = () => { - window.Whisper.Database.handleDOMException( - 'saveLegacyMessage transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = resolve; - - const store = transaction.objectStore('messages'); - - if (!data.id) { - // eslint-disable-next-line no-param-reassign - data.id = window.getGuid(); - } - - const request = store.put(data, data.id); - request.onsuccess = resolve; - request.onerror = () => { - window.Whisper.Database.handleDOMException( - 'saveLegacyMessage request error', - request.error, - reject - ); - }; - }); - } finally { - db.close(); - } -} - -async function saveMessages(arrayOfMessages, { forceSave } = {}) { - await channels.saveMessages(_cleanData(arrayOfMessages), { forceSave }); -} - -async function removeMessage(id, { Message }) { - const message = await getMessageById(id, { Message }); - - // 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 -async function _removeMessages(ids) { - await channels.removeMessage(ids); -} - -async function getMessageIdsFromServerIds(serverIds, conversationId) { - return channels.getMessageIdsFromServerIds(serverIds, conversationId); -} - -async function getMessageById(id, { Message }) { - const message = await channels.getMessageById(id); - if (!message) { - return null; - } - - return new Message(message); -} - -// For testing only -async function getAllMessages({ MessageCollection }) { - const messages = await channels.getAllMessages(); - return new MessageCollection(messages); -} - -async function getAllUnsentMessages({ MessageCollection }) { - const messages = await channels.getAllUnsentMessages(); - return new MessageCollection(messages); -} - -async function getAllMessageIds() { - const ids = await channels.getAllMessageIds(); - return ids; -} - -async function getMessageBySender( - // eslint-disable-next-line camelcase - { source, sourceDevice, sent_at }, - { Message } -) { - const messages = await channels.getMessageBySender({ - source, - sourceDevice, - sent_at, - }); - if (!messages || !messages.length) { - return null; - } - - return new Message(messages[0]); -} - -async function getMessagesBySender( - // eslint-disable-next-line camelcase - { source, sourceDevice }, - { Message } -) { - const messages = await channels.getMessagesBySender({ - source, - sourceDevice, - }); - if (!messages || !messages.length) { - return null; - } - - return messages.map(m => new Message(m)); -} - -async function getUnreadByConversation(conversationId, { MessageCollection }) { - const messages = await channels.getUnreadByConversation(conversationId); - return new MessageCollection(messages); -} - -async function getUnreadCountByConversation(conversationId) { - return channels.getUnreadCountByConversation(conversationId); -} - -async function getMessagesByConversation( - conversationId, - { limit = 100, receivedAt = Number.MAX_VALUE, MessageCollection, type = '%' } -) { - const messages = await channels.getMessagesByConversation(conversationId, { - limit, - receivedAt, - type, - }); - - return new MessageCollection(messages); -} - -async function getLastHashBySnode(convoId, snode) { - return channels.getLastHashBySnode(convoId, snode); -} - -async function getSeenMessagesByHashList(hashes) { - return channels.getSeenMessagesByHashList(hashes); -} - -async function removeAllMessagesInConversation( - conversationId, - { MessageCollection } -) { - let messages; - do { - // Yes, we really want the await in the loop. We're deleting 100 at a - // time so we don't use too much memory. - // eslint-disable-next-line no-await-in-loop - messages = await getMessagesByConversation(conversationId, { - limit: 100, - MessageCollection, - }); - - if (!messages.length) { - return; - } - - const ids = messages.map(message => message.id); - - // 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. - // eslint-disable-next-line no-await-in-loop - await Promise.all(messages.map(message => message.cleanup())); - - // eslint-disable-next-line no-await-in-loop - await channels.removeMessage(ids); - } while (messages.length > 0); -} - -async function getMessagesBySentAt(sentAt, { MessageCollection }) { - const messages = await channels.getMessagesBySentAt(sentAt); - return new MessageCollection(messages); -} - -async function getExpiredMessages({ MessageCollection }) { - const messages = await channels.getExpiredMessages(); - return new MessageCollection(messages); -} - -async function getOutgoingWithoutExpiresAt({ MessageCollection }) { - const messages = await channels.getOutgoingWithoutExpiresAt(); - return new MessageCollection(messages); -} - -async function getNextExpiringMessage({ MessageCollection }) { - const messages = await channels.getNextExpiringMessage(); - return new MessageCollection(messages); -} - -// Unprocessed - -async function getUnprocessedCount() { - return channels.getUnprocessedCount(); -} - -async function getAllUnprocessed() { - return channels.getAllUnprocessed(); -} - -async function getUnprocessedById(id) { - return channels.getUnprocessedById(id); -} - -async function saveUnprocessed(data, { forceSave } = {}) { - const id = await channels.saveUnprocessed(_cleanData(data), { forceSave }); - return id; -} - -async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) { - await channels.saveUnprocesseds(_cleanData(arrayOfUnprocessed), { - forceSave, - }); -} - -async function updateUnprocessedAttempts(id, attempts) { - await channels.updateUnprocessedAttempts(id, attempts); -} -async function updateUnprocessedWithData(id, data) { - await channels.updateUnprocessedWithData(id, data); -} - -async function removeUnprocessed(id) { - await channels.removeUnprocessed(id); -} - -async function removeAllUnprocessed() { - await channels.removeAllUnprocessed(); -} - -// Attachment downloads - -async function getNextAttachmentDownloadJobs(limit) { - return channels.getNextAttachmentDownloadJobs(limit); -} -async function saveAttachmentDownloadJob(job) { - await channels.saveAttachmentDownloadJob(job); -} -async function setAttachmentDownloadJobPending(id, pending) { - await channels.setAttachmentDownloadJobPending(id, pending); -} -async function resetAttachmentDownloadPending() { - await channels.resetAttachmentDownloadPending(); -} -async function removeAttachmentDownloadJob(id) { - await channels.removeAttachmentDownloadJob(id); -} -async function removeAllAttachmentDownloadJobs() { - await channels.removeAllAttachmentDownloadJobs(); -} - -// Other - -async function removeAll() { - await channels.removeAll(); -} - -async function removeAllConfiguration() { - await channels.removeAllConfiguration(); -} - -async function removeAllConversations() { - await channels.removeAllConversations(); -} - -async function removeAllPrivateConversations() { - await channels.removeAllPrivateConversations(); -} - -async function cleanupOrphanedAttachments() { - await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY); -} - -// Note: will need to restart the app after calling this, to set up afresh -async function removeOtherData() { - await Promise.all([ - callChannel(ERASE_SQL_KEY), - callChannel(ERASE_ATTACHMENTS_KEY), - ]); -} - -async function callChannel(name) { - return new Promise((resolve, reject) => { - ipcRenderer.send(name); - ipcRenderer.once(`${name}-done`, (event, error) => { - if (error) { - return reject(error); - } - - return resolve(); - }); - - setTimeout( - () => reject(new Error(`callChannel call to ${name} timed out`)), - DATABASE_UPDATE_TIMEOUT - ); - }); -} - -// Functions below here return plain JSON instead of Backbone Models - -async function getLegacyMessagesNeedingUpgrade( - limit, - { maxVersion = MessageType.CURRENT_SCHEMA_VERSION } -) { - const db = await window.Whisper.Database.open(); - try { - return new Promise((resolve, reject) => { - const transaction = db.transaction('messages', 'readonly'); - const messages = []; - - transaction.onerror = () => { - window.Whisper.Database.handleDOMException( - 'getLegacyMessagesNeedingUpgrade transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = () => { - resolve(messages); - }; - - const store = transaction.objectStore('messages'); - const index = store.index('schemaVersion'); - const range = IDBKeyRange.upperBound(maxVersion, true); - - const request = index.openCursor(range); - let count = 0; - - request.onsuccess = event => { - const cursor = event.target.result; - - if (cursor) { - count += 1; - messages.push(cursor.value); - - if (count >= limit) { - return; - } - - cursor.continue(); - } - }; - request.onerror = () => { - window.Whisper.Database.handleDOMException( - 'getLegacyMessagesNeedingUpgrade request error', - request.error, - reject - ); - }; - }); - } finally { - db.close(); - } -} - -async function getMessagesNeedingUpgrade( - limit, - { maxVersion = MessageType.CURRENT_SCHEMA_VERSION } -) { - const messages = await channels.getMessagesNeedingUpgrade(limit, { - maxVersion, - }); - - return messages; -} - -async function getMessagesWithVisualMediaAttachments( - conversationId, - { limit } -) { - return channels.getMessagesWithVisualMediaAttachments(conversationId, { - limit, - }); -} - -async function getMessagesWithFileAttachments(conversationId, { limit }) { - return channels.getMessagesWithFileAttachments(conversationId, { - limit, - }); -} diff --git a/js/modules/idle_detector.js b/js/modules/idle_detector.js deleted file mode 100644 index 9553b7f6ba..0000000000 --- a/js/modules/idle_detector.js +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-env browser */ - -const EventEmitter = require('events'); - -const POLL_INTERVAL_MS = 5 * 1000; -const IDLE_THRESHOLD_MS = 20; - -class IdleDetector extends EventEmitter { - constructor() { - super(); - this.handle = null; - this.timeoutId = null; - } - - start() { - window.log.info('Start idle detector'); - this._scheduleNextCallback(); - } - - stop() { - if (!this.handle) { - return; - } - - window.log.info('Stop idle detector'); - this._clearScheduledCallbacks(); - } - - _clearScheduledCallbacks() { - if (this.handle) { - cancelIdleCallback(this.handle); - this.handle = null; - } - - if (this.timeoutId) { - clearTimeout(this.timeoutId); - this.timeoutId = null; - } - } - - _scheduleNextCallback() { - this._clearScheduledCallbacks(); - this.handle = window.requestIdleCallback(deadline => { - const { didTimeout } = deadline; - const timeRemaining = deadline.timeRemaining(); - const isIdle = timeRemaining >= IDLE_THRESHOLD_MS; - this.timeoutId = setTimeout( - () => this._scheduleNextCallback(), - POLL_INTERVAL_MS - ); - if (isIdle || didTimeout) { - this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining }); - } - }); - } -} - -module.exports = { - IdleDetector, -}; diff --git a/js/modules/indexeddb.js b/js/modules/indexeddb.js deleted file mode 100644 index 1c74bb92a1..0000000000 --- a/js/modules/indexeddb.js +++ /dev/null @@ -1,168 +0,0 @@ -/* global window, Whisper, textsecure */ - -const { isFunction } = require('lodash'); - -const MessageDataMigrator = require('./messages_data_migrator'); -const { - run, - getLatestVersion, - getDatabase, -} = require('./migrations/migrations'); - -const MESSAGE_MINIMUM_VERSION = 7; - -module.exports = { - doesDatabaseExist, - mandatoryMessageUpgrade, - MESSAGE_MINIMUM_VERSION, - migrateAllToSQLCipher, - removeDatabase, - runMigrations, -}; - -async function runMigrations() { - window.log.info('Run migrations on database with attachment data'); - await run({ - Backbone: window.Backbone, - logger: window.log, - }); - - Whisper.Database.migrations[0].version = getLatestVersion(); -} - -async function mandatoryMessageUpgrade({ upgradeMessageSchema } = {}) { - if (!isFunction(upgradeMessageSchema)) { - throw new Error( - 'mandatoryMessageUpgrade: upgradeMessageSchema must be a function!' - ); - } - - const NUM_MESSAGES_PER_BATCH = 10; - window.log.info( - 'upgradeMessages: Mandatory message schema upgrade started.', - `Target version: ${MESSAGE_MINIMUM_VERSION}` - ); - - let isMigrationWithoutIndexComplete = false; - while (!isMigrationWithoutIndexComplete) { - const database = getDatabase(); - // eslint-disable-next-line no-await-in-loop - const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex( - { - databaseName: database.name, - minDatabaseVersion: database.version, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - maxVersion: MESSAGE_MINIMUM_VERSION, - BackboneMessage: window.models.Message.MessageModel, - saveMessage: window.Signal.Data.saveLegacyMessage, - } - ); - window.log.info( - 'upgradeMessages: upgrade without index', - batchWithoutIndex - ); - isMigrationWithoutIndexComplete = batchWithoutIndex.done; - } - window.log.info('upgradeMessages: upgrade without index complete!'); - - let isMigrationWithIndexComplete = false; - while (!isMigrationWithIndexComplete) { - // eslint-disable-next-line no-await-in-loop - const batchWithIndex = await MessageDataMigrator.processNext({ - BackboneMessage: window.models.Message.MessageModel, - BackboneMessageCollection: window.models.Message.MessageCollection, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - getMessagesNeedingUpgrade: - window.Signal.Data.getLegacyMessagesNeedingUpgrade, - saveMessage: window.Signal.Data.saveLegacyMessage, - maxVersion: MESSAGE_MINIMUM_VERSION, - }); - window.log.info('upgradeMessages: upgrade with index', batchWithIndex); - isMigrationWithIndexComplete = batchWithIndex.done; - } - window.log.info('upgradeMessages: upgrade with index complete!'); - - window.log.info('upgradeMessages: Message schema upgrade complete'); -} - -async function migrateAllToSQLCipher({ writeNewAttachmentData, Views } = {}) { - if (!isFunction(writeNewAttachmentData)) { - throw new Error( - 'migrateAllToSQLCipher: writeNewAttachmentData must be a function' - ); - } - if (!Views) { - throw new Error('migrateAllToSQLCipher: Views must be provided!'); - } - - let totalMessages; - const db = await Whisper.Database.open(); - - function showMigrationStatus(current) { - const status = `${current}/${totalMessages}`; - Views.Initialization.setMessage( - window.i18n('migratingToSQLCipher', [status]) - ); - } - - try { - totalMessages = await MessageDataMigrator.getNumMessages({ - connection: db, - }); - } catch (error) { - window.log.error( - 'background.getNumMessages error:', - error && error.stack ? error.stack : error - ); - totalMessages = 0; - } - - if (totalMessages) { - window.log.info(`About to migrate ${totalMessages} messages`); - showMigrationStatus(0); - } else { - window.log.info('About to migrate non-messages'); - } - - await window.Signal.migrateToSQL({ - db, - clearStores: Whisper.Database.clearStores, - handleDOMException: Whisper.Database.handleDOMException, - arrayBufferToString: textsecure.MessageReceiver.arrayBufferToStringBase64, - countCallback: count => { - window.log.info(`Migration: ${count} messages complete`); - showMigrationStatus(count); - }, - writeNewAttachmentData, - }); - - db.close(); -} - -async function doesDatabaseExist() { - return new Promise((resolve, reject) => { - const { id } = Whisper.Database; - const req = window.indexedDB.open(id); - - let existed = true; - - req.onerror = reject; - req.onsuccess = () => { - req.result.close(); - resolve(existed); - }; - req.onupgradeneeded = () => { - if (req.result.version === 1) { - existed = false; - window.indexedDB.deleteDatabase(id); - } - }; - }); -} - -function removeDatabase() { - window.log.info(`Deleting IndexedDB database '${Whisper.Database.id}'`); - window.indexedDB.deleteDatabase(Whisper.Database.id); -} diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 1fe042ab19..3479c097ca 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -1080,8 +1080,12 @@ class LokiPublicChannelAPI { async getPrivateKey() { if (!this.myPrivateKey) { - const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); - this.myPrivateKey = myKeyPair.privKey; + const item = await window.Signal.Data.getItemById('identityKey'); + const keyPair = (item && item.value) || undefined; + if (!keyPair) { + window.log.error('Could not get our Keypair from getItemById'); + } + this.myPrivateKey = keyPair.privKey; } return this.myPrivateKey; } diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js deleted file mode 100644 index 548e62ef81..0000000000 --- a/js/modules/messages_data_migrator.js +++ /dev/null @@ -1,405 +0,0 @@ -// Module to upgrade the schema of messages, e.g. migrate attachments to disk. -// `dangerouslyProcessAllWithoutIndex` purposely doesn’t rely on our Backbone -// IndexedDB adapter to prevent automatic migrations. Rather, it uses direct -// IndexedDB access. This includes avoiding usage of `storage` module which uses -// Backbone under the hood. - -/* global IDBKeyRange, window */ - -const { isFunction, isNumber, isObject, isString, last } = require('lodash'); - -const database = require('./database'); -const Message = require('./types/message'); -const settings = require('./settings'); - -const MESSAGES_STORE_NAME = 'messages'; - -exports.processNext = async ({ - BackboneMessage, - BackboneMessageCollection, - numMessagesPerBatch, - upgradeMessageSchema, - getMessagesNeedingUpgrade, - saveMessage, - maxVersion = Message.CURRENT_SCHEMA_VERSION, -} = {}) => { - if (!isFunction(BackboneMessage)) { - throw new TypeError( - "'BackboneMessage' (MessageModel) constructor is required" - ); - } - - if (!isFunction(BackboneMessageCollection)) { - throw new TypeError( - "'BackboneMessageCollection' (window.models.Message.MessageCollection)" + - ' constructor is required' - ); - } - - if (!isNumber(numMessagesPerBatch)) { - throw new TypeError("'numMessagesPerBatch' is required"); - } - - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - - const startTime = Date.now(); - - const fetchStartTime = Date.now(); - let messagesRequiringSchemaUpgrade; - try { - messagesRequiringSchemaUpgrade = await getMessagesNeedingUpgrade( - numMessagesPerBatch, - { - maxVersion, - MessageCollection: BackboneMessageCollection, - } - ); - } catch (error) { - window.log.error( - 'processNext error:', - error && error.stack ? error.stack : error - ); - return { - done: true, - numProcessed: 0, - }; - } - const fetchDuration = Date.now() - fetchStartTime; - - const upgradeStartTime = Date.now(); - const upgradedMessages = await Promise.all( - messagesRequiringSchemaUpgrade.map(message => - upgradeMessageSchema(message, { maxVersion }) - ) - ); - const upgradeDuration = Date.now() - upgradeStartTime; - - const saveStartTime = Date.now(); - await Promise.all( - upgradedMessages.map(message => - saveMessage(message, { Message: BackboneMessage }) - ) - ); - const saveDuration = Date.now() - saveStartTime; - - const totalDuration = Date.now() - startTime; - const numProcessed = messagesRequiringSchemaUpgrade.length; - const done = numProcessed < numMessagesPerBatch; - return { - done, - numProcessed, - fetchDuration, - upgradeDuration, - saveDuration, - totalDuration, - }; -}; - -exports.dangerouslyProcessAllWithoutIndex = async ({ - databaseName, - minDatabaseVersion, - numMessagesPerBatch, - upgradeMessageSchema, - logger, - maxVersion = Message.CURRENT_SCHEMA_VERSION, - saveMessage, - BackboneMessage, -} = {}) => { - if (!isString(databaseName)) { - throw new TypeError("'databaseName' must be a string"); - } - - if (!isNumber(minDatabaseVersion)) { - throw new TypeError("'minDatabaseVersion' must be a number"); - } - - if (!isNumber(numMessagesPerBatch)) { - throw new TypeError("'numMessagesPerBatch' must be a number"); - } - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - if (!isFunction(BackboneMessage)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - if (!isFunction(saveMessage)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - - const connection = await database.open(databaseName); - const databaseVersion = connection.version; - const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; - logger.info('Database status', { - databaseVersion, - isValidDatabaseVersion, - minDatabaseVersion, - }); - if (!isValidDatabaseVersion) { - throw new Error( - `Expected database version (${databaseVersion})` + - ` to be at least ${minDatabaseVersion}` - ); - } - - // NOTE: Even if we make this async using `then`, requesting `count` on an - // IndexedDB store blocks all subsequent transactions, so we might as well - // explicitly wait for it here: - const numTotalMessages = await exports.getNumMessages({ connection }); - - const migrationStartTime = Date.now(); - let numCumulativeMessagesProcessed = 0; - // eslint-disable-next-line no-constant-condition - while (true) { - // eslint-disable-next-line no-await-in-loop - const status = await _processBatch({ - connection, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - saveMessage, - BackboneMessage, - }); - if (status.done) { - break; - } - numCumulativeMessagesProcessed += status.numMessagesProcessed; - logger.info( - 'Upgrade message schema:', - Object.assign({}, status, { - numTotalMessages, - numCumulativeMessagesProcessed, - }) - ); - } - - logger.info('Close database connection'); - connection.close(); - - const totalDuration = Date.now() - migrationStartTime; - logger.info('Attachment migration complete:', { - totalDuration, - totalMessagesProcessed: numCumulativeMessagesProcessed, - }); -}; - -exports.processNextBatchWithoutIndex = async ({ - databaseName, - minDatabaseVersion, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - BackboneMessage, - saveMessage, -} = {}) => { - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError("'upgradeMessageSchema' is required"); - } - - const connection = await _getConnection({ databaseName, minDatabaseVersion }); - const batch = await _processBatch({ - connection, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - BackboneMessage, - saveMessage, - }); - return batch; -}; - -// Private API -const _getConnection = async ({ databaseName, minDatabaseVersion }) => { - if (!isString(databaseName)) { - throw new TypeError("'databaseName' must be a string"); - } - - if (!isNumber(minDatabaseVersion)) { - throw new TypeError("'minDatabaseVersion' must be a number"); - } - - const connection = await database.open(databaseName); - const databaseVersion = connection.version; - const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; - if (!isValidDatabaseVersion) { - throw new Error( - `Expected database version (${databaseVersion})` + - ` to be at least ${minDatabaseVersion}` - ); - } - - return connection; -}; - -const _processBatch = async ({ - connection, - numMessagesPerBatch, - upgradeMessageSchema, - maxVersion, - BackboneMessage, - saveMessage, -} = {}) => { - if (!isObject(connection)) { - throw new TypeError('_processBatch: connection must be a string'); - } - - if (!isFunction(upgradeMessageSchema)) { - throw new TypeError('_processBatch: upgradeMessageSchema is required'); - } - - if (!isNumber(numMessagesPerBatch)) { - throw new TypeError('_processBatch: numMessagesPerBatch is required'); - } - if (!isNumber(maxVersion)) { - throw new TypeError('_processBatch: maxVersion is required'); - } - if (!isFunction(BackboneMessage)) { - throw new TypeError('_processBatch: BackboneMessage is required'); - } - if (!isFunction(saveMessage)) { - throw new TypeError('_processBatch: saveMessage is required'); - } - - const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete( - connection - ); - if (isAttachmentMigrationComplete) { - return { - done: true, - }; - } - - const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex( - connection - ); - - const fetchUnprocessedMessagesStartTime = Date.now(); - let unprocessedMessages; - try { - unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex( - { - connection, - count: numMessagesPerBatch, - lastIndex: lastProcessedIndex, - } - ); - } catch (error) { - window.log.error( - '_processBatch error:', - error && error.stack ? error.stack : error - ); - await settings.markAttachmentMigrationComplete(connection); - await settings.deleteAttachmentMigrationLastProcessedIndex(connection); - return { - done: true, - }; - } - const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; - - const upgradeStartTime = Date.now(); - const upgradedMessages = await Promise.all( - unprocessedMessages.map(message => - upgradeMessageSchema(message, { maxVersion }) - ) - ); - const upgradeDuration = Date.now() - upgradeStartTime; - - const saveMessagesStartTime = Date.now(); - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readwrite'); - const transactionCompletion = database.completeTransaction(transaction); - await Promise.all( - upgradedMessages.map(message => - saveMessage(message, { Message: BackboneMessage }) - ) - ); - await transactionCompletion; - const saveDuration = Date.now() - saveMessagesStartTime; - - const numMessagesProcessed = upgradedMessages.length; - const done = numMessagesProcessed < numMessagesPerBatch; - const lastMessage = last(upgradedMessages); - const newLastProcessedIndex = lastMessage ? lastMessage.id : null; - if (!done) { - await settings.setAttachmentMigrationLastProcessedIndex( - connection, - newLastProcessedIndex - ); - } else { - await settings.markAttachmentMigrationComplete(connection); - await settings.deleteAttachmentMigrationLastProcessedIndex(connection); - } - - const batchTotalDuration = Date.now() - fetchUnprocessedMessagesStartTime; - - return { - batchTotalDuration, - done, - fetchDuration, - lastProcessedIndex, - newLastProcessedIndex, - numMessagesProcessed, - saveDuration, - targetSchemaVersion: Message.CURRENT_SCHEMA_VERSION, - upgradeDuration, - }; -}; - -// NOTE: Named ‘dangerous’ because it is not as efficient as using our -// `messages` `schemaVersion` index: -const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({ - connection, - count, - lastIndex, -} = {}) => { - if (!isObject(connection)) { - throw new TypeError("'connection' is required"); - } - - if (!isNumber(count)) { - throw new TypeError("'count' is required"); - } - - if (lastIndex && !isString(lastIndex)) { - throw new TypeError("'lastIndex' must be a string"); - } - - const hasLastIndex = Boolean(lastIndex); - - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); - const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); - - const excludeLowerBound = true; - const range = hasLastIndex - ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) - : undefined; - return new Promise((resolve, reject) => { - const items = []; - const request = messagesStore.openCursor(range); - request.onsuccess = event => { - const cursor = event.target.result; - const hasMoreData = Boolean(cursor); - if (!hasMoreData || items.length === count) { - resolve(items); - return; - } - const item = cursor.value; - items.push(item); - cursor.continue(); - }; - request.onerror = event => reject(event.target.error); - }); -}; - -exports.getNumMessages = async ({ connection } = {}) => { - if (!isObject(connection)) { - throw new TypeError("'connection' is required"); - } - - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); - const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); - const numTotalMessages = await database.getCount({ store: messagesStore }); - await database.completeTransaction(transaction); - - return numTotalMessages; -}; diff --git a/js/modules/migrate_to_sql.js b/js/modules/migrate_to_sql.js deleted file mode 100644 index 4278ad0398..0000000000 --- a/js/modules/migrate_to_sql.js +++ /dev/null @@ -1,409 +0,0 @@ -/* global window, IDBKeyRange */ - -const { includes, isFunction, isString, last, map } = require('lodash'); -const { - bulkAddSessions, - bulkAddIdentityKeys, - bulkAddPreKeys, - bulkAddSignedPreKeys, - bulkAddItems, - - removeSessionById, - removeIdentityKeyById, - removePreKeyById, - removeSignedPreKeyById, - removeItemById, - - saveMessages, - _removeMessages, - - saveUnprocesseds, - removeUnprocessed, - - saveConversations, - _removeConversations, -} = require('./data'); -const { - getMessageExportLastIndex, - setMessageExportLastIndex, - getMessageExportCount, - setMessageExportCount, - getUnprocessedExportLastIndex, - setUnprocessedExportLastIndex, -} = require('./settings'); -const { migrateConversation } = require('./types/conversation'); - -module.exports = { - migrateToSQL, -}; - -async function migrateToSQL({ - db, - clearStores, - handleDOMException, - countCallback, - arrayBufferToString, - writeNewAttachmentData, -}) { - if (!db) { - throw new Error('Need db for IndexedDB connection!'); - } - if (!isFunction(clearStores)) { - throw new Error('Need clearStores function!'); - } - if (!isFunction(arrayBufferToString)) { - throw new Error('Need arrayBufferToString function!'); - } - if (!isFunction(handleDOMException)) { - throw new Error('Need handleDOMException function!'); - } - - window.log.info('migrateToSQL: start'); - - let [lastIndex, doneSoFar] = await Promise.all([ - getMessageExportLastIndex(db), - getMessageExportCount(db), - ]); - let complete = false; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - save: saveMessages, - remove: _removeMessages, - storeName: 'messages', - handleDOMException, - lastIndex, - }); - - ({ complete, lastIndex } = status); - - // eslint-disable-next-line no-await-in-loop - await Promise.all([ - setMessageExportCount(db, doneSoFar), - setMessageExportLastIndex(db, lastIndex), - ]); - - const { count } = status; - doneSoFar += count; - if (countCallback) { - countCallback(doneSoFar); - } - } - window.log.info('migrateToSQL: migrate of messages complete'); - try { - await clearStores(['messages']); - } catch (error) { - window.log.warn('Failed to clear messages store'); - } - - lastIndex = await getUnprocessedExportLastIndex(db); - complete = false; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - save: async array => { - await Promise.all( - map(array, async item => { - // In the new database, we can't store ArrayBuffers, so we turn these two - // fields into strings like MessageReceiver now does before save. - - // Need to set it to version two, since we're using Base64 strings now - // eslint-disable-next-line no-param-reassign - item.version = 2; - - if (item.envelope) { - // eslint-disable-next-line no-param-reassign - item.envelope = await arrayBufferToString(item.envelope); - } - if (item.decrypted) { - // eslint-disable-next-line no-param-reassign - item.decrypted = await arrayBufferToString(item.decrypted); - } - }) - ); - await saveUnprocesseds(array); - }, - remove: removeUnprocessed, - storeName: 'unprocessed', - handleDOMException, - lastIndex, - }); - - ({ complete, lastIndex } = status); - - // eslint-disable-next-line no-await-in-loop - await setUnprocessedExportLastIndex(db, lastIndex); - } - window.log.info('migrateToSQL: migrate of unprocessed complete'); - try { - await clearStores(['unprocessed']); - } catch (error) { - window.log.warn('Failed to clear unprocessed store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: async array => { - const conversations = await Promise.all( - map(array, async conversation => - migrateConversation(conversation, { writeNewAttachmentData }) - ) - ); - - saveConversations(conversations); - }, - remove: _removeConversations, - storeName: 'conversations', - handleDOMException, - lastIndex, - // Because we're doing real-time moves to the filesystem, minimize parallelism - batchSize: 5, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of conversations complete'); - try { - await clearStores(['conversations']); - } catch (error) { - window.log.warn('Failed to clear conversations store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddSessions, - remove: removeSessionById, - storeName: 'sessions', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of sessions complete'); - try { - await clearStores(['sessions']); - } catch (error) { - window.log.warn('Failed to clear sessions store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddIdentityKeys, - remove: removeIdentityKeyById, - storeName: 'identityKeys', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of identityKeys complete'); - try { - await clearStores(['identityKeys']); - } catch (error) { - window.log.warn('Failed to clear identityKeys store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddPreKeys, - remove: removePreKeyById, - storeName: 'preKeys', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of preKeys complete'); - try { - await clearStores(['preKeys']); - } catch (error) { - window.log.warn('Failed to clear preKeys store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddSignedPreKeys, - remove: removeSignedPreKeyById, - storeName: 'signedPreKeys', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of signedPreKeys complete'); - try { - await clearStores(['signedPreKeys']); - } catch (error) { - window.log.warn('Failed to clear signedPreKeys store'); - } - - complete = false; - lastIndex = null; - - while (!complete) { - // eslint-disable-next-line no-await-in-loop - const status = await migrateStoreToSQLite({ - db, - // eslint-disable-next-line no-loop-func - save: bulkAddItems, - remove: removeItemById, - storeName: 'items', - handleDOMException, - lastIndex, - batchSize: 10, - }); - - ({ complete, lastIndex } = status); - } - window.log.info('migrateToSQL: migrate of items complete'); - // Note: we don't clear the items store because it contains important metadata which, - // if this process fails, will be crucial to going through this process again. - - window.log.info('migrateToSQL: complete'); -} - -async function migrateStoreToSQLite({ - db, - save, - remove, - storeName, - handleDOMException, - lastIndex = null, - batchSize = 50, -}) { - if (!db) { - throw new Error('Need db for IndexedDB connection!'); - } - if (!isFunction(save)) { - throw new Error('Need save function!'); - } - if (!isFunction(remove)) { - throw new Error('Need remove function!'); - } - if (!isString(storeName)) { - throw new Error('Need storeName!'); - } - if (!isFunction(handleDOMException)) { - throw new Error('Need handleDOMException for error handling!'); - } - - if (!includes(db.objectStoreNames, storeName)) { - return { - complete: true, - count: 0, - }; - } - - const queryPromise = new Promise((resolve, reject) => { - const items = []; - const transaction = db.transaction(storeName, 'readonly'); - transaction.onerror = () => { - handleDOMException( - 'migrateToSQLite transaction error', - transaction.error, - reject - ); - }; - transaction.oncomplete = () => {}; - - const store = transaction.objectStore(storeName); - const excludeLowerBound = true; - const range = lastIndex - ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) - : undefined; - const request = store.openCursor(range); - request.onerror = () => { - handleDOMException( - 'migrateToSQLite: request error', - request.error, - reject - ); - }; - request.onsuccess = event => { - const cursor = event.target.result; - - if (!cursor || !cursor.value) { - return resolve({ - complete: true, - items, - }); - } - - const item = cursor.value; - items.push(item); - - if (items.length >= batchSize) { - return resolve({ - complete: false, - items, - }); - } - - return cursor.continue(); - }; - }); - - const { items, complete } = await queryPromise; - - if (items.length) { - // Because of the force save and some failed imports, we're going to delete before - // we attempt to insert. - const ids = items.map(item => item.id); - await remove(ids); - - // We need to pass forceSave parameter, because these items already have an - // id key. Normally, this call would be interpreted as an update request. - await save(items, { forceSave: true }); - } - - const lastItem = last(items); - const id = lastItem ? lastItem.id : null; - - return { - complete, - count: items.length, - lastIndex: id, - }; -} diff --git a/js/modules/migrations/18/index.js b/js/modules/migrations/18/index.js deleted file mode 100644 index ef0650a80d..0000000000 --- a/js/modules/migrations/18/index.js +++ /dev/null @@ -1,17 +0,0 @@ -exports.run = ({ transaction, logger }) => { - const messagesStore = transaction.objectStore('messages'); - - logger.info("Create message attachment metadata index: 'hasAttachments'"); - messagesStore.createIndex( - 'hasAttachments', - ['conversationId', 'hasAttachments', 'received_at'], - { unique: false } - ); - - ['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => { - logger.info(`Create message attachment metadata index: '${name}'`); - messagesStore.createIndex(name, ['conversationId', 'received_at', name], { - unique: false, - }); - }); -}; diff --git a/js/modules/migrations/get_placeholder_migrations.js b/js/modules/migrations/get_placeholder_migrations.js deleted file mode 100644 index 3377096eae..0000000000 --- a/js/modules/migrations/get_placeholder_migrations.js +++ /dev/null @@ -1,35 +0,0 @@ -/* global window, Whisper */ - -const Migrations = require('./migrations'); - -exports.getPlaceholderMigrations = () => { - const version = Migrations.getLatestVersion(); - - return [ - { - version, - migrate() { - throw new Error( - 'Unexpected invocation of placeholder migration!' + - '\n\nMigrations must explicitly be run upon application startup instead' + - ' of implicitly via Backbone IndexedDB adapter at any time.' - ); - }, - }, - ]; -}; - -exports.getCurrentVersion = () => - new Promise((resolve, reject) => { - const request = window.indexedDB.open(Whisper.Database.id); - - request.onerror = reject; - request.onupgradeneeded = reject; - - request.onsuccess = () => { - const db = request.result; - const { version } = db; - - return resolve(version); - }; - }); diff --git a/js/modules/migrations/migrations.js b/js/modules/migrations/migrations.js deleted file mode 100644 index 8b23f2bee9..0000000000 --- a/js/modules/migrations/migrations.js +++ /dev/null @@ -1,245 +0,0 @@ -/* global window */ - -const { isString, last } = require('lodash'); - -const { runMigrations } = require('./run_migrations'); -const Migration18 = require('./18'); - -// IMPORTANT: The migrations below are run on a database that may be very large -// due to attachments being directly stored inside the database. Please avoid -// any expensive operations, e.g. modifying all messages / attachments, etc., as -// it may cause out-of-memory errors for users with long histories: -// https://github.com/signalapp/Signal-Desktop/issues/2163 -const migrations = [ - { - version: '12.0', - migrate(transaction, next) { - window.log.info('Migration 12'); - window.log.info('creating object stores'); - const messages = transaction.db.createObjectStore('messages'); - messages.createIndex('conversation', ['conversationId', 'received_at'], { - unique: false, - }); - messages.createIndex('receipt', 'sent_at', { unique: false }); - messages.createIndex('unread', ['conversationId', 'unread'], { - unique: false, - }); - messages.createIndex('expires_at', 'expires_at', { unique: false }); - - const conversations = transaction.db.createObjectStore('conversations'); - conversations.createIndex('inbox', 'active_at', { unique: false }); - conversations.createIndex('group', 'members', { - unique: false, - multiEntry: true, - }); - conversations.createIndex('type', 'type', { - unique: false, - }); - conversations.createIndex('search', 'tokens', { - unique: false, - multiEntry: true, - }); - - transaction.db.createObjectStore('groups'); - - transaction.db.createObjectStore('sessions'); - transaction.db.createObjectStore('identityKeys'); - const preKeys = transaction.db.createObjectStore('preKeys', { - keyPath: 'id', - }); - preKeys.createIndex('recipient', 'recipient', { unique: true }); - - transaction.db.createObjectStore('signedPreKeys'); - transaction.db.createObjectStore('items'); - - const contactPreKeys = transaction.db.createObjectStore( - 'contactPreKeys', - { keyPath: 'id', autoIncrement: true } - ); - contactPreKeys.createIndex('identityKeyString', 'identityKeyString', { - unique: false, - }); - contactPreKeys.createIndex('keyId', 'keyId', { unique: false }); - - const contactSignedPreKeys = transaction.db.createObjectStore( - 'contactSignedPreKeys', - { keyPath: 'id', autoIncrement: true } - ); - contactSignedPreKeys.createIndex( - 'identityKeyString', - 'identityKeyString', - { unique: false } - ); - contactSignedPreKeys.createIndex('keyId', 'keyId', { unique: false }); - - window.log.info('creating debug log'); - transaction.db.createObjectStore('debug'); - - next(); - }, - }, - { - version: '13.0', - migrate(transaction, next) { - window.log.info('Migration 13'); - window.log.info('Adding fields to identity keys'); - const identityKeys = transaction.objectStore('identityKeys'); - const request = identityKeys.openCursor(); - const promises = []; - request.onsuccess = event => { - const cursor = event.target.result; - if (cursor) { - const attributes = cursor.value; - attributes.timestamp = 0; - attributes.firstUse = false; - attributes.nonblockingApproval = false; - attributes.verified = 0; - promises.push( - new Promise((resolve, reject) => { - const putRequest = identityKeys.put(attributes, attributes.id); - putRequest.onsuccess = resolve; - putRequest.onerror = error => { - window.log.error(error && error.stack ? error.stack : error); - reject(error); - }; - }) - ); - cursor.continue(); - } else { - // no more results - // eslint-disable-next-line more/no-then - Promise.all(promises).then(() => { - next(); - }); - } - }; - request.onerror = event => { - window.log.error(event); - }; - }, - }, - { - version: '14.0', - migrate(transaction, next) { - window.log.info('Migration 14'); - window.log.info('Adding unprocessed message store'); - const unprocessed = transaction.db.createObjectStore('unprocessed'); - unprocessed.createIndex('received', 'timestamp', { unique: false }); - next(); - }, - }, - { - version: '15.0', - migrate(transaction, next) { - window.log.info('Migration 15'); - window.log.info('Adding messages index for de-duplication'); - const messages = transaction.objectStore('messages'); - messages.createIndex('unique', ['source', 'sourceDevice', 'sent_at'], { - unique: true, - }); - next(); - }, - }, - { - version: '16.0', - migrate(transaction, next) { - window.log.info('Migration 16'); - window.log.info('Dropping log table, since we now log to disk'); - transaction.db.deleteObjectStore('debug'); - next(); - }, - }, - { - version: 17, - async migrate(transaction, next) { - window.log.info('Migration 17'); - - const start = Date.now(); - - const messagesStore = transaction.objectStore('messages'); - window.log.info( - 'Create index from attachment schema version to attachment' - ); - messagesStore.createIndex('schemaVersion', 'schemaVersion', { - unique: false, - }); - - const duration = Date.now() - start; - - window.log.info( - 'Complete migration to database version 17', - `Duration: ${duration}ms` - ); - next(); - }, - }, - { - version: 18, - migrate(transaction, next) { - window.log.info('Migration 18'); - - const start = Date.now(); - Migration18.run({ transaction, logger: window.log }); - const duration = Date.now() - start; - - window.log.info( - 'Complete migration to database version 18', - `Duration: ${duration}ms` - ); - next(); - }, - }, - { - version: 19, - migrate(transaction, next) { - window.log.info('Migration 19'); - - // Empty because we don't want to cause incompatibility with beta users who have - // already run migration 19 when it was object store removal. - - next(); - }, - }, - { - version: 20, - migrate(transaction, next) { - window.log.info('Migration 20'); - - // Empty because we don't want to cause incompatibility with users who have already - // run migration 20 when it was object store removal. - - next(); - }, - }, -]; - -const database = { - id: 'loki-messenger', - nolog: true, - migrations, -}; - -exports.run = ({ Backbone, databaseName, logger } = {}) => - runMigrations({ - Backbone, - logger, - database: Object.assign( - {}, - database, - isString(databaseName) ? { id: databaseName } : {} - ), - }); - -exports.getDatabase = () => ({ - name: database.id, - version: exports.getLatestVersion(), -}); - -exports.getLatestVersion = () => { - const lastMigration = last(migrations); - if (!lastMigration) { - return null; - } - - return lastMigration.version; -}; diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js deleted file mode 100644 index 35a84cc4a5..0000000000 --- a/js/modules/migrations/run_migrations.js +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-env browser */ - -const { head, isFunction, isObject, isString, last } = require('lodash'); - -const db = require('../database'); -const { deferredToPromise } = require('../deferred_to_promise'); - -const closeDatabaseConnection = ({ Backbone } = {}) => - deferredToPromise(Backbone.sync('closeall')); - -exports.runMigrations = async ({ Backbone, database, logger } = {}) => { - if ( - !isObject(Backbone) || - !isObject(Backbone.Collection) || - !isFunction(Backbone.Collection.extend) - ) { - throw new TypeError('runMigrations: Backbone is required'); - } - - if ( - !isObject(database) || - !isString(database.id) || - !Array.isArray(database.migrations) - ) { - throw new TypeError('runMigrations: database is required'); - } - if (!isObject(logger)) { - throw new TypeError('runMigrations: logger is required'); - } - - const { - firstVersion: firstMigrationVersion, - lastVersion: lastMigrationVersion, - } = getMigrationVersions(database); - - const databaseVersion = await db.getVersion(database.id); - const isAlreadyUpgraded = databaseVersion >= lastMigrationVersion; - - logger.info('Database status', { - firstMigrationVersion, - lastMigrationVersion, - databaseVersion, - isAlreadyUpgraded, - }); - - if (isAlreadyUpgraded) { - return; - } - - const migrationCollection = new (Backbone.Collection.extend({ - database, - storeName: 'items', - }))(); - - // Note: this legacy migration technique is required to bring old clients with - // data in IndexedDB forward into the new world of SQLCipher only. - await deferredToPromise(migrationCollection.fetch({ limit: 1 })); - - logger.info('Close database connection'); - await closeDatabaseConnection({ Backbone }); -}; - -const getMigrationVersions = database => { - if (!isObject(database) || !Array.isArray(database.migrations)) { - throw new TypeError("'database' is required"); - } - - const firstMigration = head(database.migrations); - const lastMigration = last(database.migrations); - - const firstVersion = firstMigration - ? parseInt(firstMigration.version, 10) - : null; - const lastVersion = lastMigration - ? parseInt(lastMigration.version, 10) - : null; - - return { firstVersion, lastVersion }; -}; diff --git a/js/modules/signal.js b/js/modules/signal.js index 8032adf3fa..c836880b67 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -1,15 +1,13 @@ // The idea with this file is to make it webpackable for the style guide const Crypto = require('./crypto'); -const Data = require('./data'); +const Data = require('../../ts/data/data'); const Database = require('./database'); const Emoji = require('../../ts/util/emoji'); -const IndexedDB = require('./indexeddb'); const Notifications = require('../../ts/notifications'); const OS = require('../../ts/OS'); const Settings = require('./settings'); const Util = require('../../ts/util'); -const { migrateToSQL } = require('./migrate_to_sql'); const LinkPreviews = require('./link_previews'); const AttachmentDownloads = require('./attachment_downloads'); const { Message } = require('../../ts/components/conversation/Message'); @@ -57,13 +55,6 @@ const { RemoveModeratorsDialog, } = require('../../ts/components/conversation/ModeratorsRemoveDialog'); -// Migrations -const { - getPlaceholderMigrations, - getCurrentVersion, -} = require('./migrations/get_placeholder_migrations'); -const { run } = require('./migrations/migrations'); - // Types const AttachmentType = require('./types/attachment'); const VisualAttachment = require('./types/visual_attachment'); @@ -77,10 +68,6 @@ const SettingsType = require('../../ts/types/Settings'); // Views const Initialization = require('./views/initialization'); -// Workflow -const { IdleDetector } = require('./idle_detector'); -const MessageDataMigrator = require('./messages_data_migrator'); - function initializeMigrations({ userDataPath, Attachments, @@ -123,14 +110,11 @@ function initializeMigrations({ deleteOnDisk, }), getAbsoluteAttachmentPath, - getPlaceholderMigrations, - getCurrentVersion, loadAttachmentData, loadMessage: MessageType.createAttachmentLoader(loadAttachmentData), loadPreviewData, loadQuoteData, readAttachmentData, - run, processNewAttachment: attachment => MessageType.processNewAttachment(attachment, { writeNewAttachmentData, @@ -213,11 +197,6 @@ exports.setup = (options = {}) => { Initialization, }; - const Workflow = { - IdleDetector, - MessageDataMigrator, - }; - return { AttachmentDownloads, Components, @@ -225,9 +204,7 @@ exports.setup = (options = {}) => { Data, Database, Emoji, - IndexedDB, LinkPreviews, - migrateToSQL, Migrations, Notifications, OS, @@ -235,6 +212,5 @@ exports.setup = (options = {}) => { Types, Util, Views, - Workflow, }; }; diff --git a/js/read_receipts.js b/js/read_receipts.js index 42bc73914f..58b6c92091 100644 --- a/js/read_receipts.js +++ b/js/read_receipts.js @@ -46,10 +46,7 @@ return message; } - const groups = await window.Signal.Data.getAllGroupsInvolvingId(reader, { - ConversationCollection: - window.models.Conversation.ConversationCollection, - }); + const groups = await window.Signal.Data.getAllGroupsInvolvingId(reader); const ids = groups.pluck('id'); ids.push(reader); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index 9e06e17824..ed05459270 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -1,10 +1,7 @@ /* global - dcodeIO, Backbone, _, - textsecure, - stringObject, BlockedNumberController */ @@ -14,342 +11,12 @@ (function() { 'use strict'; - const Direction = { - SENDING: 1, - RECEIVING: 2, - }; - - const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__; - const StaticArrayBufferProto = new ArrayBuffer().__proto__; - const StaticUint8ArrayProto = new Uint8Array().__proto__; - - function isStringable(thing) { - return ( - thing === Object(thing) && - (thing.__proto__ === StaticArrayBufferProto || - thing.__proto__ === StaticUint8ArrayProto || - thing.__proto__ === StaticByteBufferProto) - ); - } - function convertToArrayBuffer(thing) { - if (thing === undefined) { - return undefined; - } - if (thing === Object(thing)) { - if (thing.__proto__ === StaticArrayBufferProto) { - return thing; - } - // TODO: Several more cases here... - } - - if (thing instanceof Array) { - // Assuming Uint16Array from curve25519 - const res = new ArrayBuffer(thing.length * 2); - const uint = new Uint16Array(res); - for (let i = 0; i < thing.length; i += 1) { - uint[i] = thing[i]; - } - return res; - } - - let str; - if (isStringable(thing)) { - str = stringObject(thing); - } else if (typeof thing === 'string') { - str = thing; - } else { - throw new Error( - `Tried to convert a non-stringable thing of type ${typeof thing} to an array buffer` - ); - } - const res = new ArrayBuffer(str.length); - const uint = new Uint8Array(res); - for (let i = 0; i < str.length; i += 1) { - uint[i] = str.charCodeAt(i); - } - return res; - } - - function equalArrayBuffers(ab1, ab2) { - if (!(ab1 instanceof ArrayBuffer && ab2 instanceof ArrayBuffer)) { - return false; - } - if (ab1.byteLength !== ab2.byteLength) { - return false; - } - let result = 0; - const ta1 = new Uint8Array(ab1); - const ta2 = new Uint8Array(ab2); - for (let i = 0; i < ab1.byteLength; i += 1) { - // eslint-disable-next-line no-bitwise - result |= ta1[i] ^ ta2[i]; - } - return result === 0; - } - - const IdentityRecord = Backbone.Model.extend({ - storeName: 'identityKeys', - validAttributes: [ - 'id', - 'publicKey', - 'firstUse', - 'timestamp', - 'nonblockingApproval', - ], - validate(attrs) { - const attributeNames = _.keys(attrs); - const { validAttributes } = this; - const allValid = _.all(attributeNames, attributeName => - _.contains(validAttributes, attributeName) - ); - if (!allValid) { - return new Error('Invalid identity key attribute names'); - } - const allPresent = _.all(validAttributes, attributeName => - _.contains(attributeNames, attributeName) - ); - if (!allPresent) { - return new Error('Missing identity key attributes'); - } - - if (typeof attrs.id !== 'string') { - return new Error('Invalid identity key id'); - } - if (!(attrs.publicKey instanceof ArrayBuffer)) { - return new Error('Invalid identity key publicKey'); - } - if (typeof attrs.firstUse !== 'boolean') { - return new Error('Invalid identity key firstUse'); - } - if (typeof attrs.timestamp !== 'number' || !(attrs.timestamp >= 0)) { - return new Error('Invalid identity key timestamp'); - } - if (typeof attrs.nonblockingApproval !== 'boolean') { - return new Error('Invalid identity key nonblockingApproval'); - } - - return null; - }, - }); - function SignalProtocolStore() {} - async function _hydrateCache(object, field, items, idField) { - const cache = Object.create(null); - for (let i = 0, max = items.length; i < max; i += 1) { - const item = items[i]; - const id = item[idField]; - - cache[id] = item; - } - - window.log.info(`SignalProtocolStore: Finished caching ${field} data`); - // eslint-disable-next-line no-param-reassign - object[field] = cache; - } - SignalProtocolStore.prototype = { constructor: SignalProtocolStore, - async hydrateCaches() { - await Promise.all([ - _hydrateCache( - this, - 'identityKeys', - await window.Signal.Data.getAllIdentityKeys(), - 'id' - ), - _hydrateCache( - this, - 'sessions', - await window.Signal.Data.getAllSessions(), - 'id' - ), - _hydrateCache( - this, - 'preKeys', - await window.Signal.Data.getAllPreKeys(), - 'id' - ), - _hydrateCache( - this, - 'signedPreKeys', - await window.Signal.Data.getAllSignedPreKeys(), - 'id' - ), - ]); - }, - - async getIdentityKeyPair() { - const item = await window.Signal.Data.getItemById('identityKey'); - if (item) { - return item.value; - } - window.log.error('Could not load identityKey from SignalData'); - return undefined; - }, - - // PreKeys - - async clearPreKeyStore() { - this.preKeys = Object.create(null); - await window.Signal.Data.removeAllPreKeys(); - }, - - // Signed PreKeys - async clearSignedPreKeysStore() { - this.signedPreKeys = Object.create(null); - await window.Signal.Data.removeAllSignedPreKeys(); - }, - - // Sessions - async clearSessionStore() { - this.sessions = Object.create(null); - window.Signal.Data.removeAllSessions(); - }, - - // Identity Keys - - async loadIdentityKey(identifier) { - if (identifier === null || identifier === undefined) { - throw new Error('Tried to get identity key for undefined/null key'); - } - const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = this.identityKeys[number]; - - if (identityRecord) { - return identityRecord.publicKey; - } - - return undefined; - }, - async _saveIdentityKey(data) { - const { id } = data; - this.identityKeys[id] = data; - await window.Signal.Data.createOrUpdateIdentityKey(data); - }, - async saveIdentity(identifier, publicKey, nonblockingApproval) { - if (identifier === null || identifier === undefined) { - throw new Error('Tried to put identity key for undefined/null key'); - } - if (!(publicKey instanceof ArrayBuffer)) { - // eslint-disable-next-line no-param-reassign - publicKey = convertToArrayBuffer(publicKey); - } - if (typeof nonblockingApproval !== 'boolean') { - // eslint-disable-next-line no-param-reassign - nonblockingApproval = false; - } - - const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = this.identityKeys[number]; - - if (!identityRecord || !identityRecord.publicKey) { - // Lookup failed, or the current key was removed, so save this one. - window.log.info('Saving new identity...'); - await this._saveIdentityKey({ - id: number, - publicKey, - firstUse: true, - timestamp: Date.now(), - nonblockingApproval, - }); - - return false; - } - - const oldpublicKey = identityRecord.publicKey; - if (!equalArrayBuffers(oldpublicKey, publicKey)) { - window.log.info('Replacing existing identity...'); - - await this._saveIdentityKey({ - id: number, - publicKey, - firstUse: false, - timestamp: Date.now(), - nonblockingApproval, - }); - - return true; - } - - return false; - }, - async saveIdentityWithAttributes(identifier, attributes) { - if (identifier === null || identifier === undefined) { - throw new Error('Tried to put identity key for undefined/null key'); - } - - const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = this.identityKeys[number]; - - const updates = { - id: number, - ...identityRecord, - ...attributes, - }; - - const model = new IdentityRecord(updates); - if (model.isValid()) { - await this._saveIdentityKey(updates); - } else { - throw model.validationError; - } - }, - async setApproval(identifier, nonblockingApproval) { - if (identifier === null || identifier === undefined) { - throw new Error('Tried to set approval for undefined/null identifier'); - } - if (typeof nonblockingApproval !== 'boolean') { - throw new Error('Invalid approval status'); - } - - const number = textsecure.utils.unencodeNumber(identifier)[0]; - const identityRecord = this.identityKeys[number]; - - if (!identityRecord) { - throw new Error(`No identity record for ${number}`); - } - - identityRecord.nonblockingApproval = nonblockingApproval; - await this._saveIdentityKey(identityRecord); - }, - async removeIdentityKey(number) { - delete this.identityKeys[number]; - await window.Signal.Data.removeIdentityKeyById(number); - }, - - // Not yet processed messages - for resiliency - getUnprocessedCount() { - return window.Signal.Data.getUnprocessedCount(); - }, - getAllUnprocessed() { - return window.Signal.Data.getAllUnprocessed(); - }, - getUnprocessedById(id) { - return window.Signal.Data.getUnprocessedById(id); - }, - addUnprocessed(data) { - // We need to pass forceSave because the data has an id already, which will cause - // an update instead of an insert. - return window.Signal.Data.saveUnprocessed(data, { - forceSave: true, - }); - }, - updateUnprocessedAttempts(id, attempts) { - return window.Signal.Data.updateUnprocessedAttempts(id, attempts); - }, - updateUnprocessedWithData(id, data) { - return window.Signal.Data.updateUnprocessedWithData(id, data); - }, - removeUnprocessed(id) { - return window.Signal.Data.removeUnprocessed(id); - }, - removeAllUnprocessed() { - return window.Signal.Data.removeAllUnprocessed(); - }, async removeAllData() { await window.Signal.Data.removeAll(); - await this.hydrateCaches(); window.storage.reset(); await window.storage.fetch(); @@ -359,16 +26,8 @@ await window.getConversationController().load(); await BlockedNumberController.load(); }, - async removeAllConfiguration() { - await window.Signal.Data.removeAllConfiguration(); - await this.hydrateCaches(); - - window.storage.reset(); - await window.storage.fetch(); - }, }; _.extend(SignalProtocolStore.prototype, Backbone.Events); window.SignalProtocolStore = SignalProtocolStore; - window.SignalProtocolStore.prototype.Direction = Direction; })(); diff --git a/libloki/crypto.js b/libloki/crypto.js index 438464196a..701f58131f 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -1,7 +1,6 @@ /* global window, libsignal, - textsecure, StringView, Multibase, TextEncoder, @@ -147,7 +146,8 @@ const serverPubKey = new Uint8Array( dcodeIO.ByteBuffer.fromBase64(serverPubKey64).toArrayBuffer() ); - const keyPair = await textsecure.storage.protocol.getIdentityKeyPair(); + const item = await window.Signal.Data.getItemById('identityKey'); + const keyPair = (item && item.value) || undefined; if (!keyPair) { throw new Error('Failed to get keypair for token decryption'); } diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index 49739aa2ad..9b1e30518e 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -82,7 +82,6 @@ return this.pending; }, async createAccount(identityKeyPair, userAgent, readReceipts) { - const signalingKey = libsignal.crypto.getRandomBytes(32 + 20); let password = btoa(getString(libsignal.crypto.getRandomBytes(16))); password = password.substring(0, password.length - 2); @@ -102,16 +101,6 @@ // update our own identity key, which may have changed // if we're relinking after a reinstall on the master device const pubKeyString = StringView.arrayBufferToHex(identityKeyPair.pubKey); - await textsecure.storage.protocol.saveIdentityWithAttributes( - pubKeyString, - { - id: pubKeyString, - publicKey: identityKeyPair.pubKey, - firstUse: true, - timestamp: Date.now(), - nonblockingApproval: true, - } - ); await textsecure.storage.put('identityKey', identityKeyPair); await textsecure.storage.put('password', password); @@ -130,15 +119,15 @@ await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1); }, async clearSessionsAndPreKeys() { - const store = textsecure.storage.protocol; - window.log.info('clearing all sessions'); - await Promise.all([store.clearSessionStore()]); // During secondary device registration we need to keep our prekeys sent // to other pubkeys await Promise.all([ - store.clearPreKeyStore(), - store.clearSignedPreKeysStore(), + window.Signal.Data.removeAllPreKeys(), + window.Signal.Data.removeAllSignedPreKeys(), + window.Signal.Data.removeAllContactPreKeys(), + window.Signal.Data.removeAllContactSignedPreKeys(), + window.Signal.Data.removeAllSessions(), ]); }, async generateMnemonic(language = 'english') { diff --git a/libtextsecure/storage/unprocessed.js b/libtextsecure/storage/unprocessed.js index ac113362f7..d32603159a 100644 --- a/libtextsecure/storage/unprocessed.js +++ b/libtextsecure/storage/unprocessed.js @@ -1,4 +1,4 @@ -/* global window, textsecure */ +/* global window */ // eslint-disable-next-line func-names (function() { @@ -10,31 +10,30 @@ window.textsecure.storage.unprocessed = { getCount() { - return textsecure.storage.protocol.getUnprocessedCount(); + return window.Signal.Data.getUnprocessedCount(); }, getAll() { - return textsecure.storage.protocol.getAllUnprocessed(); + return window.Signal.Data.getAllUnprocessed(); }, get(id) { - return textsecure.storage.protocol.getUnprocessedById(id); + return window.Signal.Data.getUnprocessedById(id); }, add(data) { - return textsecure.storage.protocol.addUnprocessed(data); + return window.Signal.Data.saveUnprocessed(data, { + forceSave: true, + }); }, updateAttempts(id, attempts) { - return textsecure.storage.protocol.updateUnprocessedAttempts( - id, - attempts - ); + return window.Signal.Data.updateUnprocessedAttempts(id, attempts); }, addDecryptedData(id, data) { - return textsecure.storage.protocol.updateUnprocessedWithData(id, data); + return window.Signal.Data.updateUnprocessedWithData(id, data); }, remove(id) { - return textsecure.storage.protocol.removeUnprocessed(id); + return window.Signal.Data.removeUnprocessed(id); }, removeAll() { - return textsecure.storage.protocol.removeAllUnprocessed(); + return window.Signal.Data.removeAllUnprocessed(); }, }; })(); diff --git a/main.js b/main.js index cc9eb3e265..0224728a22 100644 --- a/main.js +++ b/main.js @@ -756,20 +756,6 @@ async function showMainWindow(sqlKey, passwordAttempt = false) { appStartInitialSpellcheckSetting = await getSpellCheckSetting(); await sqlChannels.initialize(); - try { - const IDB_KEY = 'indexeddb-delete-needed'; - const item = await sql.getItemById(IDB_KEY); - if (item && item.value) { - await sql.removeIndexedDBFiles(); - await sql.removeItemById(IDB_KEY); - } - } catch (error) { - console.log( - '(ready event handler) error deleting IndexedDB:', - error && error.stack ? error.stack : error - ); - } - async function cleanupOrphanedAttachments() { const allAttachments = await attachments.getAllAttachments(userDataPath); const orphanedAttachments = await sql.removeKnownAttachments( diff --git a/preload.js b/preload.js index 358648184d..2abc6c887f 100644 --- a/preload.js +++ b/preload.js @@ -86,7 +86,7 @@ window.isBeforeVersion = (toCheck, baseVersion) => { }; // eslint-disable-next-line func-names -window.CONSTANTS = new (function () { +window.CONSTANTS = new (function() { this.MAX_GROUP_NAME_LENGTH = 64; this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); this.MAX_LINKED_DEVICES = 1; @@ -377,7 +377,7 @@ window.callWorker = (fnName, ...args) => utilWorker.callWorker(fnName, ...args); // Linux seems to periodically let the event loop stop, so this is a global workaround setInterval(() => { - window.nodeSetImmediate(() => { }); + window.nodeSetImmediate(() => {}); }, 1000); const { autoOrientImage } = require('./js/modules/auto_orient_image'); @@ -417,9 +417,11 @@ window.moment.locale(localeForMoment); window.OnionAPI = OnionAPI; window.libsession = require('./ts/session'); - window.models = require('./ts/models'); +window.Signal = window.Signal || {}; +window.Signal.Data = require('./ts/data/data'); + window.getMessageController = () => window.libsession.Messages.MessageController.getInstance(); @@ -446,19 +448,20 @@ window.DataMessageReceiver = require('./ts/receiver/dataMessage'); window.NewSnodeAPI = require('./ts/session/snode_api/serviceNodeAPI'); window.SnodePool = require('./ts/session/snode_api/snodePool'); -const { SwarmPolling } = require('./ts/session/snode_api/swarmPolling'); -const { SwarmPollingStub } = require('./ts/session/snode_api/swarmPollingStub'); - if (process.env.USE_STUBBED_NETWORK) { + const { + SwarmPollingStub, + } = require('./ts/session/snode_api/swarmPollingStub'); window.SwarmPolling = new SwarmPollingStub(); } else { + const { SwarmPolling } = require('./ts/session/snode_api/swarmPolling'); window.SwarmPolling = new SwarmPolling(); } // eslint-disable-next-line no-extend-native,func-names -Promise.prototype.ignore = function () { +Promise.prototype.ignore = function() { // eslint-disable-next-line more/no-then - this.then(() => { }); + this.then(() => {}); }; if ( @@ -474,7 +477,6 @@ if ( tmp: require('tmp'), path: require('path'), basePath: __dirname, - attachmentsPath: window.Signal.Migrations.attachmentsPath, isWindows, }; /* eslint-enable global-require, import/no-extraneous-dependencies */ diff --git a/test/backup_test.js b/test/backup_test.js index 2625491fe4..38f1bc2e77 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -552,12 +552,7 @@ describe('Backup', () => { }); console.log('Backup test: Check conversations'); - const conversationCollection = await window.Signal.Data.getAllConversations( - { - ConversationCollection: - window.models.Conversation.ConversationCollection, - } - ); + const conversationCollection = await window.Signal.Data.getAllConversations(); assert.strictEqual(conversationCollection.length, CONVERSATION_COUNT); // We need to ommit any custom fields we have added diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 441e5af2fd..4a035c987d 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { connect } from 'react-redux'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { Avatar } from '../Avatar'; -import { removeItemById } from '../../../js/modules/data'; import { darkTheme, lightTheme } from '../../state/ducks/SessionTheme'; import { SessionToastContainer } from './SessionToastContainer'; import { mapDispatchToProps } from '../../state/actions'; @@ -16,6 +15,7 @@ import { getOurNumber } from '../../state/selectors/user'; import { UserUtils } from '../../session/utils'; import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; import { DAYS } from '../../session/utils/Number'; +import { removeItemById } from '../../data/data'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index 0bf51680b5..cf739629e2 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -14,7 +14,7 @@ import { StringUtils, ToastUtils } from '../../session/utils'; import { lightTheme } from '../../state/ducks/SessionTheme'; import { ConversationController } from '../../session/conversations'; import { PasswordUtil } from '../../util'; -import { removeAll } from '../../../js/modules/data'; +import { removeAll } from '../../data/data'; export const MAX_USERNAME_LENGTH = 20; diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 81d6d950d0..9948e553fa 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { getMessageById } from '../../../js/modules/data'; +import { getMessageById } from '../../data/data'; import { MessageModel } from '../../models/message'; import { getMessageQueue } from '../../session'; import { ConversationController } from '../../session/conversations'; @@ -125,9 +125,7 @@ export class SessionInboxView extends React.Component { if (!msg || !msg.message) { // otherwise, look for it in the database // nobody is listening to this freshly fetched message .trigger calls - const dbMessage = await getMessageById(m.identifier, { - Message: MessageModel, - }); + const dbMessage = await getMessageById(m.identifier); if (!dbMessage) { return null; diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index d1f11a8c4b..64c4872eb9 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -4,11 +4,9 @@ import { SessionModal } from './SessionModal'; import { SessionButton, SessionButtonColor } from './SessionButton'; import { missingCaseError, PasswordUtil } from '../../util/'; import { ToastUtils } from '../../session/utils'; -import { toast } from 'react-toastify'; -import { SessionToast, SessionToastType } from './SessionToast'; import { SessionIconType } from './icon'; import { DefaultTheme, withTheme } from 'styled-components'; -import { getPasswordHash } from '../../../js/modules/data'; +import { getPasswordHash } from '../../data/data'; export enum PasswordAction { Set = 'set', Change = 'change', diff --git a/ts/components/session/SessionSeedModal.tsx b/ts/components/session/SessionSeedModal.tsx index 539894e68f..5963652efb 100644 --- a/ts/components/session/SessionSeedModal.tsx +++ b/ts/components/session/SessionSeedModal.tsx @@ -5,7 +5,7 @@ import { SessionButton } from './SessionButton'; import { ToastUtils } from '../../session/utils'; import { DefaultTheme, withTheme } from 'styled-components'; import { PasswordUtil } from '../../util'; -import { getPasswordHash } from '../../../js/modules/data'; +import { getPasswordHash } from '../../data/data'; interface Props { onClose: any; diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index eba3c7e61f..437cc71190 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -26,15 +26,15 @@ import * as MIME from '../../../types/MIME'; import { SessionFileDropzone } from './SessionFileDropzone'; import { ConversationType } from '../../../state/ducks/conversations'; import { MessageView } from '../../MainViewController'; -import { - getMessageById, - getPubkeysInPublicConversation, -} from '../../../../js/modules/data'; import { pushUnblockToSend } from '../../../session/utils/Toast'; import { MessageDetail } from '../../conversation/MessageDetail'; import { ConversationController } from '../../../session/conversations'; import { PubKey } from '../../../session/types'; import { MessageModel } from '../../../models/message'; +import { + getMessageById, + getPubkeysInPublicConversation, +} from '../../../data/data'; interface State { // Message sending progress @@ -808,9 +808,7 @@ export class SessionConversation extends React.Component { ); if (quotedMessage) { - const quotedMessageModel = await getMessageById(quotedMessage.id, { - Message: MessageModel, - }); + const quotedMessageModel = await getMessageById(quotedMessage.id); if (quotedMessageModel) { quotedMessageProps = await conversationModel.makeQuote( quotedMessageModel diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index a4a9a0c26e..9242eb301a 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -15,9 +15,9 @@ import { SessionLastSeenIndicator } from './SessionLastSeedIndicator'; import { ToastUtils } from '../../../session/utils'; import { TypingBubble } from '../../conversation/TypingBubble'; import { ConversationController } from '../../../session/conversations'; -import { MessageCollection, MessageModel } from '../../../models/message'; +import { MessageModel } from '../../../models/message'; import { MessageRegularProps } from '../../../models/messageType'; -import { getMessagesBySentAt } from '../../../../js/modules/data'; +import { getMessagesBySentAt } from '../../../data/data'; interface State { showScrollButton: boolean; @@ -555,9 +555,7 @@ export class SessionMessagesList extends React.Component { // If there's no message already in memory, we won't be scrolling. So we'll gather // some more information then show an informative toast to the user. if (!targetMessage) { - const collection = await getMessagesBySentAt(quoteId, { - MessageCollection, - }); + const collection = await getMessagesBySentAt(quoteId); const found = Boolean( collection.find((item: MessageModel) => { const messageAuthor = item.propsForMessage?.authorPhoneNumber; diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 07c97d0cdd..7748b805da 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -20,7 +20,7 @@ import { DefaultTheme, withTheme } from 'styled-components'; import { getMessagesWithFileAttachments, getMessagesWithVisualMediaAttachments, -} from '../../../../js/modules/data'; +} from '../../../data/data'; interface Props { id: string; diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index b6488664b3..6a9cee972a 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -17,7 +17,7 @@ import { getConversations, } from '../../../state/selectors/conversations'; import { connect } from 'react-redux'; -import { getPasswordHash } from '../../../../js/modules/data'; +import { getPasswordHash } from '../../../../ts/data/data'; export enum SessionSettingCategory { Appearance = 'appearance', diff --git a/ts/data/data.ts b/ts/data/data.ts new file mode 100644 index 0000000000..29bcae6262 --- /dev/null +++ b/ts/data/data.ts @@ -0,0 +1,1072 @@ +import Electron from 'electron'; + +const { ipcRenderer } = Electron; +// tslint:disable: function-name no-require-imports no-var-requires one-variable-per-declaration no-void-expression + +import _ from 'lodash'; +import { + ConversationCollection, + ConversationModel, +} from '../models/conversation'; +import { MessageCollection, MessageModel } from '../models/message'; +import { HexKeyPair } from '../receiver/keypairs'; +import { PubKey } from '../session/types'; +import { ConversationType } from '../state/ducks/conversations'; + +const { + base64ToArrayBuffer, + arrayBufferToBase64, +} = require('../../js/modules/crypto'); + +const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes + +const SQL_CHANNEL_KEY = 'sql-channel'; +const ERASE_SQL_KEY = 'erase-sql-key'; +const ERASE_ATTACHMENTS_KEY = 'erase-attachments'; +const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments'; + +export const _jobs = Object.create(null); +const _DEBUG = false; +let _jobCounter = 0; +let _shuttingDown = false; +let _shutdownCallback: any = null; +let _shutdownPromise: any = null; + +const channels = {} as any; + +export type StorageItem = { + id: string; + value: any; +}; + +export type IdentityKey = { + id: string; + publicKey: ArrayBuffer; + firstUse: boolean; + nonblockingApproval: boolean; + secretKey?: string; // found in medium groups +}; + +export type GuardNode = { + ed25519PubKey: string; +}; + +export type SwarmNode = { + address: string; + ip: string; + port: string; + pubkey_ed25519: string; + pubkey_x25519: string; +}; + +export type ServerToken = { + serverUrl: string; + token: string; +}; + +const channelsToMake = { + _cleanData, + + shutdown, + close, + removeDB, + getPasswordHash, + + getIdentityKeyById, + + removeAllIdentityKeys, + getAllIdentityKeys, + + removeAllPreKeys, + removeAllSignedPreKeys, + removeAllContactPreKeys, + removeAllContactSignedPreKeys, + + getGuardNodes, + updateGuardNodes, + + createOrUpdateItem, + getItemById, + getAllItems, + bulkAddItems, + removeItemById, + removeAllItems, + + removeAllSessions, + + getSwarmNodesForPubkey, + updateSwarmNodesForPubkey, + + saveConversation, + saveConversations, + getConversationById, + updateConversation, + removeConversation, + + getAllConversations, + getAllConversationIds, + getAllPublicConversations, + getPublicConversationsByServer, + getPubkeysInPublicConversation, + savePublicServerToken, + getPublicServerTokenByServerUrl, + getAllGroupsInvolvingId, + + searchConversations, + searchMessages, + searchMessagesInConversation, + + saveMessage, + cleanSeenMessages, + cleanLastHashes, + saveSeenMessageHash, + updateLastHash, + saveSeenMessageHashes, + saveMessages, + removeMessage, + _removeMessages, + getUnreadByConversation, + getUnreadCountByConversation, + + removeAllMessagesInConversation, + + getMessageBySender, + getMessageIdsFromServerIds, + getMessageById, + getAllMessages, + getAllUnsentMessages, + getAllMessageIds, + getMessagesBySentAt, + getExpiredMessages, + getOutgoingWithoutExpiresAt, + getNextExpiringMessage, + getMessagesByConversation, + getSeenMessagesByHashList, + getLastHashBySnode, + + getUnprocessedCount, + getAllUnprocessed, + getUnprocessedById, + saveUnprocessed, + saveUnprocesseds, + updateUnprocessedAttempts, + updateUnprocessedWithData, + removeUnprocessed, + removeAllUnprocessed, + + getNextAttachmentDownloadJobs, + saveAttachmentDownloadJob, + resetAttachmentDownloadPending, + setAttachmentDownloadJobPending, + removeAttachmentDownloadJob, + removeAllAttachmentDownloadJobs, + + removeAll, + removeAllConversations, + + removeOtherData, + cleanupOrphanedAttachments, + + // Returning plain JSON + getMessagesWithVisualMediaAttachments, + getMessagesWithFileAttachments, + + getAllEncryptionKeyPairsForGroup, + getLatestClosedGroupEncryptionKeyPair, + addClosedGroupEncryptionKeyPair, + isKeyPairAlreadySaved, + removeAllClosedGroupEncryptionKeyPairs, +}; + +export function init() { + // We listen to a lot of events on ipcRenderer, often on the same channel. This prevents + // any warnings that might be sent to the console in that case. + ipcRenderer.setMaxListeners(0); + + _.forEach(channelsToMake, fn => { + if (_.isFunction(fn)) { + makeChannel(fn.name); + } + }); + + ipcRenderer.on( + `${SQL_CHANNEL_KEY}-done`, + (event, jobId, errorForDisplay, result) => { + const job = _getJob(jobId); + if (!job) { + throw new Error( + `Received SQL channel reply to job ${jobId}, but did not have it in our registry!` + ); + } + + const { resolve, reject, fnName } = job; + + if (errorForDisplay) { + return reject( + new Error( + `Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}` + ) + ); + } + + return resolve(result); + } + ); +} + +// When IPC arguments are prepared for the cross-process send, they are JSON.stringified. +// We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). +export async function _cleanData(data: any): Promise { + const keys = Object.keys(data); + for (let index = 0, max = keys.length; index < max; index += 1) { + const key = keys[index]; + const value = data[key]; + + if (value === null || value === undefined) { + // eslint-disable-next-line no-continue + continue; + } + + if (_.isFunction(value.toNumber)) { + // eslint-disable-next-line no-param-reassign + data[key] = value.toNumber(); + } else if (Array.isArray(value)) { + // eslint-disable-next-line no-param-reassign + data[key] = value.map(_cleanData); + } else if (_.isObject(value)) { + // eslint-disable-next-line no-param-reassign + data[key] = _cleanData(value); + } else if ( + typeof value !== 'string' && + typeof value !== 'number' && + typeof value !== 'boolean' + ) { + window.log.info(`_cleanData: key ${key} had type ${typeof value}`); + } + } + return data; +} + +async function _shutdown() { + if (_shutdownPromise) { + return _shutdownPromise; + } + + _shuttingDown = true; + + const jobKeys = Object.keys(_jobs); + window.log.info( + `data.shutdown: starting process. ${jobKeys.length} jobs outstanding` + ); + + // No outstanding jobs, return immediately + if (jobKeys.length === 0) { + return null; + } + + // Outstanding jobs; we need to wait until the last one is done + _shutdownPromise = new Promise((resolve, reject) => { + _shutdownCallback = (error: any) => { + window.log.info('data.shutdown: process complete'); + if (error) { + return reject(error); + } + + return resolve(undefined); + }; + }); + + return _shutdownPromise; +} + +function _makeJob(fnName: string) { + if (_shuttingDown && fnName !== 'close') { + throw new Error( + `Rejecting SQL channel job (${fnName}); application is shutting down` + ); + } + + _jobCounter += 1; + const id = _jobCounter; + + if (_DEBUG) { + window.log.debug(`SQL channel job ${id} (${fnName}) started`); + } + _jobs[id] = { + fnName, + start: Date.now(), + }; + + return id; +} + +function _updateJob(id: number, data: any) { + const { resolve, reject } = data; + const { fnName, start } = _jobs[id]; + + _jobs[id] = { + ..._jobs[id], + ...data, + resolve: (value: any) => { + _removeJob(id); + // const end = Date.now(); + // const delta = end - start; + // if (delta > 10) { + // window.log.debug( + // `SQL channel job ${id} (${fnName}) succeeded in ${end - start}ms` + // ); + // } + return resolve(value); + }, + reject: (error: any) => { + _removeJob(id); + const end = Date.now(); + window.log.warn( + `SQL channel job ${id} (${fnName}) failed in ${end - start}ms` + ); + return reject(error); + }, + }; +} + +function _removeJob(id: number) { + if (_DEBUG) { + _jobs[id].complete = true; + return; + } + + if (_jobs[id].timer) { + clearTimeout(_jobs[id].timer); + _jobs[id].timer = null; + } + + // tslint:disable-next-line: no-dynamic-delete + delete _jobs[id]; + + if (_shutdownCallback) { + const keys = Object.keys(_jobs); + if (keys.length === 0) { + _shutdownCallback(); + } + } +} + +function _getJob(id: number) { + return _jobs[id]; +} + +function makeChannel(fnName: string) { + channels[fnName] = async (...args: any) => { + const jobId = _makeJob(fnName); + + return new Promise((resolve, reject) => { + ipcRenderer.send(SQL_CHANNEL_KEY, jobId, fnName, ...args); + + _updateJob(jobId, { + resolve, + reject, + args: _DEBUG ? args : null, + }); + + _jobs[jobId].timer = setTimeout( + () => + reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)), + DATABASE_UPDATE_TIMEOUT + ); + }); + }; +} + +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, base64ToArrayBuffer(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, arrayBufferToBase64(value)); + } + } + + return updated; +} + +// Basic +export async function shutdown(): Promise { + // Stop accepting new SQL jobs, flush outstanding queue + await _shutdown(); + await close(); +} +// Note: will need to restart the app after calling this, to set up afresh +export async function close(): Promise { + await channels.close(); +} + +// Note: will need to restart the app after calling this, to set up afresh +export async function removeDB(): Promise { + await channels.removeDB(); +} + +// Password hash + +export async function getPasswordHash(): Promise { + return channels.getPasswordHash(); +} + +// Identity Keys + +const IDENTITY_KEY_KEYS = ['publicKey']; + +// Identity Keys +// TODO: identity key has different shape depending on how it is called, +// so we need to come up with a way to make TS work with all of them + +export async function getIdentityKeyById( + id: string +): Promise { + const data = await channels.getIdentityKeyById(id); + return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); +} + +export async function removeAllIdentityKeys(): Promise { + await channels.removeAllIdentityKeys(); +} +export async function getAllIdentityKeys() { + const keys = await channels.getAllIdentityKeys(); + return keys.map((key: any) => keysToArrayBuffer(IDENTITY_KEY_KEYS, key)); +} + +// Those removeAll are not used anymore except to cleanup the app since we removed all of those tables +export async function removeAllPreKeys(): Promise { + await channels.removeAllPreKeys(); +} +const PRE_KEY_KEYS = ['privateKey', 'publicKey', 'signature']; +export async function removeAllSignedPreKeys(): Promise { + await channels.removeAllSignedPreKeys(); +} +export async function removeAllContactPreKeys(): Promise { + await channels.removeAllContactPreKeys(); +} +export async function removeAllContactSignedPreKeys(): Promise { + await channels.removeAllContactSignedPreKeys(); +} + +// Guard Nodes +export async function getGuardNodes(): Promise> { + return channels.getGuardNodes(); +} +export async function updateGuardNodes(nodes: Array): Promise { + return channels.updateGuardNodes(nodes); +} + +// Items + +const ITEM_KEYS: Object = { + identityKey: ['value.pubKey', 'value.privKey'], + profileKey: ['value'], +}; +export async function createOrUpdateItem(data: StorageItem): Promise { + 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); +} +export async function getItemById( + id: string +): Promise { + const keys = (ITEM_KEYS as any)[id]; + const data = await channels.getItemById(id); + + return Array.isArray(keys) ? keysToArrayBuffer(keys, data) : data; +} +export async function getAllItems(): Promise> { + 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; + }); +} +export async function bulkAddItems(array: Array): Promise { + const updated = _.map(array, data => { + const { id } = data; + const keys = (ITEM_KEYS as any)[id]; + return Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; + }); + await channels.bulkAddItems(updated); +} +export async function removeItemById(id: string): Promise { + await channels.removeItemById(id); +} +export async function removeAllItems(): Promise { + await channels.removeAllItems(); +} +// Sessions +export async function removeAllSessions(): Promise { + await channels.removeAllSessions(); +} + +// Swarm nodes +export async function getSwarmNodesForPubkey( + pubkey: string +): Promise> { + return channels.getSwarmNodesForPubkey(pubkey); +} + +export async function updateSwarmNodesForPubkey( + pubkey: string, + snodeEdKeys: Array +): Promise { + await channels.updateSwarmNodesForPubkey(pubkey, snodeEdKeys); +} + +// Closed group + +/** + * The returned array is ordered based on the timestamp, the latest is at the end. + */ +export async function getAllEncryptionKeyPairsForGroup( + groupPublicKey: string | PubKey +): Promise | undefined> { + const pubkey = (groupPublicKey as PubKey).key || (groupPublicKey as string); + return channels.getAllEncryptionKeyPairsForGroup(pubkey); +} + +export async function getLatestClosedGroupEncryptionKeyPair( + groupPublicKey: string +): Promise { + return channels.getLatestClosedGroupEncryptionKeyPair(groupPublicKey); +} + +export async function addClosedGroupEncryptionKeyPair( + groupPublicKey: string, + keypair: HexKeyPair +): Promise { + await channels.addClosedGroupEncryptionKeyPair(groupPublicKey, keypair); +} + +export async function isKeyPairAlreadySaved( + groupPublicKey: string, + keypair: HexKeyPair +): Promise { + return channels.isKeyPairAlreadySaved(groupPublicKey, keypair); +} + +export async function removeAllClosedGroupEncryptionKeyPairs( + groupPublicKey: string +): Promise { + return channels.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); +} + +// Conversation +export async function saveConversation(data: ConversationType): Promise { + const cleaned = _.omit(data, 'isOnline'); + await channels.saveConversation(cleaned); +} + +export async function saveConversations( + data: Array +): Promise { + const cleaned = data.map(d => _.omit(d, 'isOnline')); + await channels.saveConversations(cleaned); +} + +export async function getConversationById( + id: string +): Promise { + const data = await channels.getConversationById(id); + return new ConversationModel(data); +} + +export async function updateConversation( + id: string, + data: ConversationType +): Promise { + const existing = await getConversationById(id); + if (!existing) { + throw new Error(`Conversation ${id} does not exist!`); + } + + const merged = _.merge({}, existing.attributes, data); + + // Merging is a really bad idea and not what we want here, e.g. + // it will take a union of old and new members and that's not + // what we want for member deletion, so: + merged.members = data.members; + + // Don't save the online status of the object + const cleaned = _.omit(merged, 'isOnline'); + await channels.updateConversation(cleaned); +} + +export async function removeConversation(id: string): Promise { + 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(); + } +} + +export async function getAllConversations(): Promise { + const conversations = await channels.getAllConversations(); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + +export async function getAllConversationIds(): Promise> { + const ids = await channels.getAllConversationIds(); + return ids; +} + +export async function getAllPublicConversations(): Promise< + ConversationCollection +> { + const conversations = await channels.getAllPublicConversations(); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + +export async function getPubkeysInPublicConversation( + id: string +): Promise> { + return channels.getPubkeysInPublicConversation(id); +} + +export async function savePublicServerToken(data: ServerToken): Promise { + await channels.savePublicServerToken(data); +} + +export async function getPublicServerTokenByServerUrl( + serverUrl: string +): Promise { + const token = await channels.getPublicServerTokenByServerUrl(serverUrl); + return token; +} + +export async function getPublicConversationsByServer( + server: string +): Promise { + const conversations = await channels.getPublicConversationsByServer(server); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + +export async function getAllGroupsInvolvingId( + id: string +): Promise { + const conversations = await channels.getAllGroupsInvolvingId(id); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + +export async function searchConversations(query: string): Promise> { + const conversations = await channels.searchConversations(query); + return conversations; +} + +export async function searchMessages( + query: string, + { limit }: any = {} +): Promise> { + const messages = await channels.searchMessages(query, { limit }); + return messages; +} + +/** + * Returns just json objects not MessageModel + */ +export async function searchMessagesInConversation( + query: string, + conversationId: string, + options: { limit: number } | undefined +): Promise { + const messages = await channels.searchMessagesInConversation( + query, + conversationId, + { limit: options?.limit } + ); + return messages; +} + +// Message + +export async function cleanSeenMessages(): Promise { + await channels.cleanSeenMessages(); +} + +export async function cleanLastHashes(): Promise { + await channels.cleanLastHashes(); +} + +// TODO: Strictly type the following +export async function saveSeenMessageHashes(data: any): Promise { + await channels.saveSeenMessageHashes(_cleanData(data)); +} + +export async function updateLastHash(data: any): Promise { + await channels.updateLastHash(_cleanData(data)); +} + +export async function saveSeenMessageHash(data: { + expiresAt: number; + hash: string; +}): Promise { + await channels.saveSeenMessageHash(_cleanData(data)); +} + +export async function saveMessage( + data: MessageModel, + options?: { forceSave: boolean } +): Promise { + const id = await channels.saveMessage(_cleanData(data), { + forceSave: options?.forceSave, + }); + window.Whisper.ExpiringMessagesListener.update(); + return id; +} + +export async function saveMessages( + arrayOfMessages: any, + options?: { forceSave: boolean } +): Promise { + await channels.saveMessages(_cleanData(arrayOfMessages), { + forceSave: options?.forceSave, + }); +} + +export async function removeMessage(id: string): Promise { + const message = await getMessageById(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 (message) { + await channels.removeMessage(id); + await message.cleanup(); + } +} + +// Note: this method will not clean up external files, just delete from SQL +export async function _removeMessages(ids: Array): Promise { + await channels.removeMessage(ids); +} + +export async function getMessageIdsFromServerIds( + serverIds: Array, + conversationId: string +) { + return channels.getMessageIdsFromServerIds(serverIds, conversationId); +} + +export async function getMessageById(id: string): Promise { + const message = await channels.getMessageById(id); + if (!message) { + return null; + } + + return new MessageModel(message); +} + +// For testing only +export async function getAllMessages(): Promise { + const messages = await channels.getAllMessages(); + return new MessageCollection(messages); +} + +export async function getAllUnsentMessages(): Promise { + const messages = await channels.getAllUnsentMessages(); + return new MessageCollection(messages); +} + +export async function getAllMessageIds(): Promise> { + const ids = await channels.getAllMessageIds(); + return ids; +} + +export async function getMessageBySender( + // eslint-disable-next-line camelcase + { + source, + sourceDevice, + sent_at, + }: { source: string; sourceDevice: number; sent_at: number } +): Promise { + const messages = await channels.getMessageBySender({ + source, + sourceDevice, + sent_at, + }); + if (!messages || !messages.length) { + return null; + } + + return new MessageModel(messages[0]); +} + +export async function getUnreadByConversation( + conversationId: string +): Promise { + const messages = await channels.getUnreadByConversation(conversationId); + return new MessageCollection(messages); +} + +// might throw +export async function getUnreadCountByConversation( + conversationId: string +): Promise { + return channels.getUnreadCountByConversation(conversationId); +} + +export async function getMessagesByConversation( + conversationId: string, + { limit = 100, receivedAt = Number.MAX_VALUE, type = '%' } +): Promise { + const messages = await channels.getMessagesByConversation(conversationId, { + limit, + receivedAt, + type, + }); + + return new MessageCollection(messages); +} + +export async function getLastHashBySnode( + convoId: string, + snode: string +): Promise { + return channels.getLastHashBySnode(convoId, snode); +} + +export async function getSeenMessagesByHashList( + hashes: Array +): Promise { + return channels.getSeenMessagesByHashList(hashes); +} + +export async function removeAllMessagesInConversation( + conversationId: string +): Promise { + let messages; + do { + // Yes, we really want the await in the loop. We're deleting 100 at a + // time so we don't use too much memory. + // eslint-disable-next-line no-await-in-loop + messages = await getMessagesByConversation(conversationId, { + limit: 100, + }); + + if (!messages.length) { + return; + } + + const ids = messages.map(message => message.id); + + // 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. + // eslint-disable-next-line no-await-in-loop + await Promise.all(messages.map(message => message.cleanup())); + + // eslint-disable-next-line no-await-in-loop + await channels.removeMessage(ids); + } while (messages.length > 0); +} + +export async function getMessagesBySentAt( + sentAt: number +): Promise { + const messages = await channels.getMessagesBySentAt(sentAt); + return new MessageCollection(messages); +} + +export async function getExpiredMessages(): Promise { + const messages = await channels.getExpiredMessages(); + return new MessageCollection(messages); +} + +export async function getOutgoingWithoutExpiresAt(): Promise< + MessageCollection +> { + const messages = await channels.getOutgoingWithoutExpiresAt(); + return new MessageCollection(messages); +} + +export async function getNextExpiringMessage(): Promise { + const messages = await channels.getNextExpiringMessage(); + return new MessageCollection(messages); +} + +// Unprocessed + +export async function getUnprocessedCount(): Promise { + return channels.getUnprocessedCount(); +} + +export async function getAllUnprocessed(): Promise { + return channels.getAllUnprocessed(); +} + +export async function getUnprocessedById(id: string): Promise { + return channels.getUnprocessedById(id); +} + +export async function saveUnprocessed( + data: any, + options?: { + forceSave: boolean; + } +): Promise { + const id = await channels.saveUnprocessed( + _cleanData(data), + options?.forceSave || false + ); + return id; +} + +export async function saveUnprocesseds( + arrayOfUnprocessed: Array, + options?: { + forceSave: boolean; + } +): Promise { + await channels.saveUnprocesseds(_cleanData(arrayOfUnprocessed), { + forceSave: options?.forceSave || false, + }); +} + +export async function updateUnprocessedAttempts( + id: string, + attempts: number +): Promise { + await channels.updateUnprocessedAttempts(id, attempts); +} +export async function updateUnprocessedWithData( + id: string, + data: any +): Promise { + await channels.updateUnprocessedWithData(id, data); +} + +export async function removeUnprocessed(id: string): Promise { + await channels.removeUnprocessed(id); +} + +export async function removeAllUnprocessed(): Promise { + await channels.removeAllUnprocessed(); +} + +// Attachment downloads + +export async function getNextAttachmentDownloadJobs( + limit: number +): Promise { + return channels.getNextAttachmentDownloadJobs(limit); +} +export async function saveAttachmentDownloadJob(job: any): Promise { + await channels.saveAttachmentDownloadJob(job); +} +export async function setAttachmentDownloadJobPending( + id: string, + pending: boolean +): Promise { + await channels.setAttachmentDownloadJobPending(id, pending); +} +export async function resetAttachmentDownloadPending(): Promise { + await channels.resetAttachmentDownloadPending(); +} +export async function removeAttachmentDownloadJob(id: string): Promise { + await channels.removeAttachmentDownloadJob(id); +} +export async function removeAllAttachmentDownloadJobs(): Promise { + await channels.removeAllAttachmentDownloadJobs(); +} + +// Other + +export async function removeAll(): Promise { + await channels.removeAll(); +} + +export async function removeAllConversations(): Promise { + await channels.removeAllConversations(); +} + +export async function cleanupOrphanedAttachments(): Promise { + await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY); +} + +// Note: will need to restart the app after calling this, to set up afresh +export async function removeOtherData(): Promise { + await Promise.all([ + callChannel(ERASE_SQL_KEY), + callChannel(ERASE_ATTACHMENTS_KEY), + ]); +} + +async function callChannel(name: string): Promise { + return new Promise((resolve, reject) => { + ipcRenderer.send(name); + ipcRenderer.once(`${name}-done`, (event, error) => { + if (error) { + return reject(error); + } + + return resolve(undefined); + }); + + setTimeout( + () => reject(new Error(`callChannel call to ${name} timed out`)), + DATABASE_UPDATE_TIMEOUT + ); + }); +} + +// Functions below here return plain JSON instead of Backbone Models + +export async function getMessagesWithVisualMediaAttachments( + conversationId: string, + options?: { limit: number } +): Promise { + return channels.getMessagesWithVisualMediaAttachments(conversationId, { + limit: options?.limit, + }); +} + +export async function getMessagesWithFileAttachments( + conversationId: string, + options?: { limit: number } +): Promise { + return channels.getMessagesWithFileAttachments(conversationId, { + limit: options?.limit, + }); +} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 7deeb37173..e1cf5ad709 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -29,7 +29,7 @@ import { removeAllMessagesInConversation, removeMessage as dataRemoveMessage, updateConversation, -} from '../../js/modules/data'; +} from '../../ts/data/data'; export interface OurLokiProfile { displayName: string; @@ -520,12 +520,11 @@ export class ConversationModel extends Backbone.Model { } public async getUnread() { - return getUnreadByConversation(this.id, { - MessageCollection: MessageCollection, - }); + return getUnreadByConversation(this.id); } public async getUnreadCount() { + window.log.warn('getUnreadCount is slow'); return getUnreadCountByConversation(this.id); } @@ -867,7 +866,6 @@ export class ConversationModel extends Backbone.Model { } const messages = await getMessagesByConversation(this.id, { limit: 1, - MessageCollection: MessageCollection, }); const lastMessageModel = messages.at(0); const lastMessageJSON = lastMessageModel ? lastMessageModel.toJSON() : null; @@ -1009,9 +1007,7 @@ export class ConversationModel extends Backbone.Model { } public async commit() { - await updateConversation(this.id, this.attributes, { - Conversation: ConversationModel, - }); + await updateConversation(this.id, this.attributes); this.trigger('change', this); } @@ -1058,7 +1054,7 @@ export class ConversationModel extends Backbone.Model { conversationId, }) ); - let unreadMessages = await this.getUnread(); + let unreadMessages = (await this.getUnread()).models; const oldUnread = unreadMessages.filter( (message: any) => message.get('received_at') <= newestUnreadDate @@ -1467,9 +1463,7 @@ export class ConversationModel extends Backbone.Model { } public async removeMessage(messageId: any) { - await dataRemoveMessage(messageId, { - Message: MessageModel, - }); + await dataRemoveMessage(messageId); window.Whisper.events.trigger('messageDeleted', { conversationKey: this.id, messageId, @@ -1494,9 +1488,7 @@ export class ConversationModel extends Backbone.Model { } public async destroyMessages() { - await removeAllMessagesInConversation(this.id, { - MessageCollection, - }); + await removeAllMessagesInConversation(this.id); window.Whisper.events.trigger('conversationReset', { conversationKey: this.id, diff --git a/ts/models/message.ts b/ts/models/message.ts index 71324c7d71..2e0252be12 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -21,7 +21,7 @@ import { } from './messageType'; import autoBind from 'auto-bind'; -import { saveMessage } from '../../js/modules/data'; +import { saveMessage } from '../../ts/data/data'; import { ConversationModel } from './conversation'; export class MessageModel extends Backbone.Model { public propsForTimerNotification: any; @@ -34,14 +34,12 @@ export class MessageModel extends Backbone.Model { const filledAttrs = fillMessageAttributesWithDefaults(attributes); super(filledAttrs); - if (_.isObject(filledAttrs)) { - this.set( - window.Signal.Types.Message.initializeSchemaVersion({ - message: filledAttrs, - logger: window.log, - }) - ); - } + this.set( + window.Signal.Types.Message.initializeSchemaVersion({ + message: filledAttrs, + logger: window.log, + }) + ); // this.on('expired', this.onExpired); void this.setToExpire(); @@ -1276,7 +1274,6 @@ export class MessageModel extends Backbone.Model { // TODO investigate the meaning of the forceSave const id = await saveMessage(this.attributes, { forceSave, - Message: MessageModel, }); this.trigger('change'); return id; diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index e0c1ea867e..f65d19b4ad 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { MessageModel } from '../models/message'; -import { saveMessage } from '../../js/modules/data'; +import { saveMessage } from '../../ts/data/data'; export async function downloadAttachment(attachment: any) { const serverUrl = new URL(attachment.url).origin; @@ -240,9 +240,7 @@ export async function queueAttachmentDownloads( } if (count > 0) { - await saveMessage(message.attributes, { - Message: Whisper.Message, - }); + await saveMessage(message.attributes); return true; } diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 472292b420..fcba22c6da 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -18,13 +18,13 @@ import { getLatestClosedGroupEncryptionKeyPair, isKeyPairAlreadySaved, removeAllClosedGroupEncryptionKeyPairs, -} from '../../js/modules/data'; +} from '../../ts/data/data'; import { ClosedGroupNewMessage, ClosedGroupNewMessageParams, } from '../session/messages/outgoing/content/data/group/ClosedGroupNewMessage'; -import { ECKeyPair } from './keypairs'; +import { ECKeyPair, HexKeyPair } from './keypairs'; import { UserUtils } from '../session/utils'; import { ConversationModel } from '../models/conversation'; import _ from 'lodash'; @@ -786,6 +786,9 @@ async function sendLatestKeyPairToUsers( return; } + const keyPairToUse = + inMemoryKeyPair || ECKeyPair.fromHexKeyPair(latestKeyPair as HexKeyPair); + const expireTimer = groupConvo.get('expireTimer') || 0; await Promise.all( @@ -800,7 +803,7 @@ async function sendLatestKeyPairToUsers( const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( [member], - inMemoryKeyPair || ECKeyPair.fromHexKeyPair(latestKeyPair) + keyPairToUse ); const keypairsMessage = new ClosedGroupEncryptionPairReplyMessage({ diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 765da65470..8e621dd57b 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -15,7 +15,7 @@ import { createOrUpdateItem, getAllEncryptionKeyPairsForGroup, getItemById, -} from '../../js/modules/data'; +} from '../../ts/data/data'; import { ECKeyPair } from './keypairs'; import { handleNewClosedGroup } from './closedGroups'; import { KeyPairRequestManager } from './keyPairRequestManager'; diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index de5ef4baf2..f4091dbef7 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -14,7 +14,7 @@ import { ConversationController } from '../session/conversations'; import { handleClosedGroupControlMessage } from './closedGroups'; import { MessageModel } from '../models/message'; import { MessageModelType } from '../models/messageType'; -import { getMessageBySender } from '../../js/modules/data'; +import { getMessageBySender } from '../../ts/data/data'; export async function updateProfile( conversation: any, @@ -354,12 +354,11 @@ async function isMessageDuplicate({ const { Errors } = window.Signal.Types; try { - const result = await getMessageBySender( - { source, sourceDevice, sent_at: timestamp }, - { - Message: MessageModel, - } - ); + const result = await getMessageBySender({ + source, + sourceDevice, + sent_at: timestamp, + }); if (!result) { return false; diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index adc8c8614a..3213002498 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -9,7 +9,7 @@ import { ConversationController } from '../session/conversations'; import { ConversationModel } from '../models/conversation'; import { MessageCollection, MessageModel } from '../models/message'; import { MessageController } from '../session/messages'; -import { getMessageById, getMessagesBySentAt } from '../../js/modules/data'; +import { getMessageById, getMessagesBySentAt } from '../../ts/data/data'; async function handleGroups( conversation: ConversationModel, @@ -99,9 +99,7 @@ async function copyFromQuotedMessage( const { attachments, id, author } = quote; const firstAttachment = attachments[0]; - const collection = await getMessagesBySentAt(id, { - MessageCollection, - }); + const collection = await getMessagesBySentAt(id); const found = collection.find((item: any) => { const messageAuthor = item.getContact(); @@ -555,9 +553,7 @@ export async function handleMessageJob( // We go to the database here because, between the message save above and // the previous line's trigger() call, we might have marked all messages // unread in the database. This message might already be read! - const fetched = await getMessageById(message.get('id'), { - Message: MessageModel, - }); + const fetched = await getMessageById(message.get('id')); const previousUnread = message.get('unread'); diff --git a/ts/session/conversations/index.ts b/ts/session/conversations/index.ts index 40aa423d4d..7ff8312a36 100644 --- a/ts/session/conversations/index.ts +++ b/ts/session/conversations/index.ts @@ -3,7 +3,7 @@ import { getAllGroupsInvolvingId, removeConversation, saveConversation, -} from '../../../js/modules/data'; +} from '../../../ts/data/data'; import { ConversationAttributes, ConversationCollection, @@ -196,9 +196,7 @@ export class ConversationController { } public async getAllGroupsInvolvingId(id: string) { - const groups = await getAllGroupsInvolvingId(id, { - ConversationCollection, - }); + const groups = await getAllGroupsInvolvingId(id); return groups.map((group: any) => this.conversations.add(group)); } @@ -232,9 +230,7 @@ export class ConversationController { await conversation.destroyMessages(); - await removeConversation(id, { - Conversation: ConversationModel, - }); + await removeConversation(id); conversation.off('change', this.updateReduxConvoChanged); this.conversations.remove(conversation); if (window.inboxStore) { @@ -257,9 +253,7 @@ export class ConversationController { const load = async () => { try { - const collection = await getAllConversations({ - ConversationCollection, - }); + const collection = await getAllConversations(); this.conversations.add(collection.models); diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts index 28e32ff5b8..e0823f0d79 100644 --- a/ts/session/crypto/MessageEncrypter.ts +++ b/ts/session/crypto/MessageEncrypter.ts @@ -4,7 +4,7 @@ import { PubKey } from '../types'; import { concatUInt8Array, getSodium } from '.'; import { fromHexToArray } from '../utils/String'; export { concatUInt8Array, getSodium }; -import { getLatestClosedGroupEncryptionKeyPair } from '../../../js/modules/data'; +import { getLatestClosedGroupEncryptionKeyPair } from '../../../ts/data/data'; import { UserUtils } from '../utils'; /** diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 560d90e4bd..4e46c5efa0 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -12,7 +12,7 @@ import { getIdentityKeyById, getLatestClosedGroupEncryptionKeyPair, removeAllClosedGroupEncryptionKeyPairs, -} from '../../../js/modules/data'; +} from '../../../ts/data/data'; import uuid from 'uuid'; import { SignalService } from '../../protobuf'; import { generateCurve25519KeyPairWithoutPrefix } from '../crypto'; diff --git a/ts/session/messages/MessageController.ts b/ts/session/messages/MessageController.ts index 6dab7cc60e..26b40baf50 100644 --- a/ts/session/messages/MessageController.ts +++ b/ts/session/messages/MessageController.ts @@ -1,7 +1,7 @@ // You can see MessageController for in memory registered messages. // Ee register messages to it everytime we send one, so that when an event happens we can find which message it was based on this id. -import { getMessagesByConversation } from '../../../js/modules/data'; +import { getMessagesByConversation } from '../../../ts/data/data'; import { ConversationModel } from '../../models/conversation'; import { MessageCollection, MessageModel } from '../../models/message'; @@ -71,19 +71,4 @@ export class MessageController { public get(identifier: string) { return this.messageLookup.get(identifier); } - - public async getMessagesByKeyFromDb(key: string) { - // loadLive gets messages live, not from the database which can lag behind. - - let messages = []; - const messageSet = await getMessagesByConversation(key, { - limit: 100, - MessageCollection, - }); - - messages = messageSet.models.map( - (conv: ConversationModel) => conv.attributes - ); - return messages; - } } diff --git a/ts/session/onions/index.ts b/ts/session/onions/index.ts index 825a54fdc6..1895ae0aac 100644 --- a/ts/session/onions/index.ts +++ b/ts/session/onions/index.ts @@ -1,5 +1,5 @@ import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; -import { getGuardNodes } from '../../../js/modules/data'; +import { getGuardNodes } from '../../../ts/data/data'; import * as SnodePool from '../snode_api/snodePool'; import _ from 'lodash'; import fetch from 'node-fetch'; diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 4f02d256a1..28b9b8708e 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; +import { createOrUpdateItem, getItemById } from '../../../ts/data/data'; import { PartialRawMessage, RawMessage } from '../types/RawMessage'; import { ContentMessage } from '../messages/outgoing'; import { PubKey } from '../types'; diff --git a/ts/session/snode_api/snodePool.ts b/ts/session/snode_api/snodePool.ts index b5c61cf980..c860351c12 100644 --- a/ts/session/snode_api/snodePool.ts +++ b/ts/session/snode_api/snodePool.ts @@ -12,7 +12,7 @@ import { import { getSwarmNodesForPubkey, updateSwarmNodesForPubkey, -} from '../../../js/modules/data'; +} from '../../../ts/data/data'; import semver from 'semver'; import _ from 'lodash'; diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index 5ae433956d..5681e8a703 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -9,7 +9,7 @@ import { getSeenMessagesByHashList, saveSeenMessageHashes, updateLastHash, -} from '../../../js/modules/data'; +} from '../../../ts/data/data'; import { StringUtils } from '../../session/utils'; import { ConversationController } from '../conversations'; diff --git a/ts/session/types/OpenGroup.ts b/ts/session/types/OpenGroup.ts index 3593e4db28..b06ba0afd2 100644 --- a/ts/session/types/OpenGroup.ts +++ b/ts/session/types/OpenGroup.ts @@ -135,7 +135,7 @@ export class OpenGroup { // Try to connect to server try { conversation = await PromiseUtils.timeout( - window.attemptConnection(prefixedServer, channel), + OpenGroup.attemptConnection(prefixedServer, channel), 20000 ); @@ -239,4 +239,61 @@ export class OpenGroup { return `http${hasSSL ? 's' : ''}://${server}`; } + + // Attempts a connection to an open group server + private static async attemptConnection(serverURL: string, channelId: number) { + let completeServerURL = serverURL.toLowerCase(); + const valid = OpenGroup.validate(completeServerURL); + if (!valid) { + return new Promise((_resolve, reject) => { + reject(window.i18n('connectToServerFail')); + }); + } + + // Add http or https prefix to server + completeServerURL = OpenGroup.prefixify(completeServerURL); + + const rawServerURL = serverURL + .replace(/^https?:\/\//i, '') + .replace(/[/\\]+$/i, ''); + + const conversationId = `publicChat:${channelId}@${rawServerURL}`; + + // Quickly peak to make sure we don't already have it + const conversationExists = ConversationController.getInstance().get( + conversationId + ); + if (conversationExists) { + // We are already a member of this public chat + return new Promise((_resolve, reject) => { + reject(window.i18n('publicChatExists')); + }); + } + + // Get server + const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer( + completeServerURL + ); + // SSL certificate failure or offline + if (!serverAPI) { + // Url incorrect or server not compatible + return new Promise((_resolve, reject) => { + reject(window.i18n('connectToServerFail')); + }); + } + + // Create conversation + const conversation = await ConversationController.getInstance().getOrCreateAndWait( + conversationId, + 'group' + ); + + // Convert conversation to a public one + await conversation.setPublicSource(completeServerURL, channelId); + + // and finally activate it + void conversation.getPublicSendData(); // may want "await" if you want to use the API + + return conversation; + } } diff --git a/ts/session/utils/Messages.ts b/ts/session/utils/Messages.ts index 61da963b32..cd5d1ff868 100644 --- a/ts/session/utils/Messages.ts +++ b/ts/session/utils/Messages.ts @@ -11,7 +11,7 @@ import { ConfigurationMessageClosedGroup, } from '../messages/outgoing/content/ConfigurationMessage'; import uuid from 'uuid'; -import { getLatestClosedGroupEncryptionKeyPair } from '../../../js/modules/data'; +import { getLatestClosedGroupEncryptionKeyPair } from '../../../ts/data/data'; import { UserUtils } from '.'; import { ECKeyPair } from '../../receiver/keypairs'; import _ from 'lodash'; diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index ab3478d7b4..d71a51d319 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { UserUtils } from '.'; -import { getItemById } from '../../../js/modules/data'; +import { getItemById } from '../../../ts/data/data'; import { KeyPair } from '../../../libtextsecure/libsignal-protocol'; import { PubKey } from '../types'; import { toHex } from './String'; diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index dc0a62b2ea..db17203908 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -1,4 +1,4 @@ -import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; +import { createOrUpdateItem, getItemById } from '../../../ts/data/data'; import { getMessageQueue } from '..'; import { ConversationController } from '../conversations'; import { getCurrentConfigurationMessage } from './Messages'; diff --git a/ts/shims/Signal.ts b/ts/shims/Signal.ts index 80dc3eee87..937a0ae5a2 100644 --- a/ts/shims/Signal.ts +++ b/ts/shims/Signal.ts @@ -1,4 +1,4 @@ -import { getPasswordHash } from '../../js/modules/data'; +import { getPasswordHash } from '../../ts/data/data'; export async function hasPassword() { const hash = await getPasswordHash(); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 09e3e3241f..de7578dce3 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -4,7 +4,7 @@ import { Constants } from '../../session'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ConversationController } from '../../session/conversations'; import { MessageCollection, MessageModel } from '../../models/message'; -import { getMessagesByConversation } from '../../../js/modules/data'; +import { getMessagesByConversation } from '../../data/data'; // State @@ -100,7 +100,7 @@ async function getMessages( window.log.error('Failed to get convo on reducer.'); return []; } - const unreadCount = (await conversation.getUnreadCount()) as number; + const unreadCount = await conversation.getUnreadCount(); let msgCount = numMessages || Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) + unreadCount; @@ -115,15 +115,15 @@ async function getMessages( const messageSet = await getMessagesByConversation(conversationKey, { limit: msgCount, - MessageCollection, }); // Set first member of series here. const messageModels = messageSet.models; const isPublic = conversation.isPublic(); + const messagesPickedUp = messageModels.map(makeMessageTypeFromMessageModel); - const sortedMessage = sortMessages(messageModels, isPublic); + const sortedMessage = sortMessages(messagesPickedUp, isPublic); // no need to do that `firstMessageOfSeries` on a private chat if (conversation.isPrivate()) { @@ -438,6 +438,10 @@ function getEmptyState(): ConversationsStateType { }; } +const makeMessageTypeFromMessageModel = (message: MessageModel) => { + return _.pick(message as any, toPickFromMessageModel) as MessageTypeInConvo; +}; + function sortMessages( messages: Array, isPublic: boolean @@ -472,10 +476,7 @@ function handleMessageAdded( const { messages } = state; const { conversationKey, messageModel } = action.payload; if (conversationKey === state.selectedConversation) { - const addedMessage = _.pick( - messageModel as any, - toPickFromMessageModel - ) as MessageTypeInConvo; + const addedMessage = makeMessageTypeFromMessageModel(messageModel); const messagesWithNewMessage = [...messages, addedMessage]; const convo = state.conversationLookup[state.selectedConversation]; const isPublic = convo?.isPublic || false; diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 1c5940c23f..f039a91eb8 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -2,7 +2,7 @@ import { omit, reject } from 'lodash'; import { AdvancedSearchOptions, SearchOptions } from '../../types/Search'; import { cleanSearchTerm } from '../../util/cleanSearchTerm'; -import { searchConversations, searchMessages } from '../../../js/modules/data'; +import { searchConversations, searchMessages } from '../../../ts/data/data'; import { makeLookup } from '../../util/makeLookup'; import { diff --git a/ts/test/session/unit/receiving/ConfigurationMessage_test.ts b/ts/test/session/unit/receiving/ConfigurationMessage_test.ts index 6651697a3f..94b96b0f21 100644 --- a/ts/test/session/unit/receiving/ConfigurationMessage_test.ts +++ b/ts/test/session/unit/receiving/ConfigurationMessage_test.ts @@ -10,7 +10,7 @@ import { TestUtils } from '../../../test-utils'; import Sinon, * as sinon from 'sinon'; import * as cache from '../../../../receiver/cache'; -import * as data from '../../../../../js/modules/data'; +import * as data from '../../../../../ts/data/data'; import { EnvelopePlus } from '../../../../receiver/types'; import chaiAsPromised from 'chai-as-promised'; diff --git a/ts/test/test-utils/utils/stubbing.ts b/ts/test/test-utils/utils/stubbing.ts index ce004f8a27..4607006f04 100644 --- a/ts/test/test-utils/utils/stubbing.ts +++ b/ts/test/test-utils/utils/stubbing.ts @@ -1,5 +1,5 @@ import * as sinon from 'sinon'; -import * as DataShape from '../../../../js/modules/data'; +import * as DataShape from '../../../../ts/data/data'; import { Application } from 'spectron'; const globalAny: any = global; @@ -8,7 +8,7 @@ const sandbox = sinon.createSandbox(); // We have to do this in a weird way because Data uses module.exports // which doesn't play well with sinon or ImportMock // tslint:disable-next-line: no-require-imports no-var-requires -const Data = require('../../../../js/modules/data'); +const Data = require('../../../../ts/data/data'); type DataFunction = typeof DataShape; /** diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index 97001cd1f7..6330fb1f50 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -1,4 +1,4 @@ -import { createOrUpdateItem, getItemById } from '../../js/modules/data'; +import { createOrUpdateItem, getItemById } from '../../ts/data/data'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; diff --git a/ts/window.d.ts b/ts/window.d.ts index bf91cdefe8..673de4fdec 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -9,7 +9,6 @@ import { LokiMessageInterface } from '../js/modules/loki_message_api'; import { SwarmPolling } from './session/snode_api/swarmPolling'; import { LibTextsecure } from '../libtextsecure'; -import { ConversationType } from '../js/modules/data'; import { RecoveryPhraseUtil } from '../libloki/modules/mnemonic'; import { ConfirmationDialogParams } from '../background'; import {} from 'styled-components/cssprop'; @@ -21,6 +20,7 @@ import { MessageController } from './session/messages/MessageController'; import { DefaultTheme } from 'styled-components'; import { ConversationCollection } from './models/conversation'; +import { ConversationType } from './state/ducks/conversations'; /* We declare window stuff here instead of global.d.ts because we are importing other declarations. @@ -42,7 +42,6 @@ declare global { StubAppDotNetApi: any; StubMessageAPI: any; Whisper: any; - attemptConnection: ConversationType; clearLocalData: any; clipboard: any; confirmationDialog: (params: ConfirmationDialogParams) => any; From 370158951a7fd5f151be1edc1dd0b08a575f0e8f Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 16 Feb 2021 18:12:49 +1100 Subject: [PATCH 034/109] move events from MessageQueue to MessageSentHandler --- js/background.js | 18 --- ts/components/session/ActionsPanel.tsx | 5 +- ts/components/session/SessionInboxView.tsx | 66 --------- ts/models/conversation.ts | 50 +++---- ts/models/message.ts | 38 +---- ts/session/group/index.ts | 2 +- ts/session/index.ts | 3 +- ts/session/instance.ts | 12 -- ts/session/messages/MessageController.ts | 5 + ts/session/sending/MessageQueue.ts | 120 +++++++-------- ts/session/sending/MessageSentHandler.ts | 93 ++++++++++++ ts/session/sending/PendingMessageCache.ts | 1 - ts/session/utils/syncUtils.ts | 35 ++--- .../session/unit/sending/MessageQueue_test.ts | 139 +++++++++++------- 14 files changed, 276 insertions(+), 311 deletions(-) delete mode 100644 ts/session/instance.ts create mode 100644 ts/session/sending/MessageSentHandler.ts diff --git a/js/background.js b/js/background.js index 1248aeadf0..a36ef16c84 100644 --- a/js/background.js +++ b/js/background.js @@ -773,24 +773,6 @@ } }); - Whisper.events.on( - 'publicMessageSent', - ({ identifier, pubKey, timestamp, serverId, serverTimestamp }) => { - try { - const conversation = window.getConversationController().get(pubKey); - conversation.onPublicMessageSent({ - identifier, - pubKey, - timestamp, - serverId, - serverTimestamp, - }); - } catch (e) { - window.log.error('Error setting public on message'); - } - } - ); - Whisper.events.on('password-updated', () => { if (appView && appView.inboxView) { appView.inboxView.trigger('password-updated'); diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 4a035c987d..e5b23b4ff3 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -13,7 +13,10 @@ import { getFocusedSection } from '../../state/selectors/section'; import { getTheme } from '../../state/selectors/theme'; import { getOurNumber } from '../../state/selectors/user'; import { UserUtils } from '../../session/utils'; -import { syncConfigurationIfNeeded } from '../../session/utils/syncUtils'; +import { + forceSyncConfigurationNowIfNeeded, + syncConfigurationIfNeeded, +} from '../../session/utils/syncUtils'; import { DAYS } from '../../session/utils/Number'; import { removeItemById } from '../../data/data'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 9948e553fa..9238214586 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -46,11 +46,6 @@ export class SessionInboxView extends React.Component { isExpired: false, }; - this.fetchHandleMessageSentData = this.fetchHandleMessageSentData.bind( - this - ); - this.handleMessageSentFailure = this.handleMessageSentFailure.bind(this); - this.handleMessageSentSuccess = this.handleMessageSentSuccess.bind(this); this.showSessionSettingsCategory = this.showSessionSettingsCategory.bind( this ); @@ -117,51 +112,6 @@ export class SessionInboxView extends React.Component { ); } - private async fetchHandleMessageSentData(m: RawMessage | OpenGroupMessage) { - // if a message was sent and this message was created after the last app restart, - // this message is still in memory in the MessageController - const msg = MessageController.getInstance().get(m.identifier); - - if (!msg || !msg.message) { - // otherwise, look for it in the database - // nobody is listening to this freshly fetched message .trigger calls - const dbMessage = await getMessageById(m.identifier); - - if (!dbMessage) { - return null; - } - return { msg: dbMessage }; - } - - return { msg: msg.message }; - } - - private async handleMessageSentSuccess( - sentMessage: RawMessage | OpenGroupMessage, - wrappedEnvelope: any - ) { - const fetchedData = await this.fetchHandleMessageSentData(sentMessage); - if (!fetchedData) { - return; - } - const { msg } = fetchedData; - - void msg.handleMessageSentSuccess(sentMessage, wrappedEnvelope); - } - - private async handleMessageSentFailure( - sentMessage: RawMessage | OpenGroupMessage, - error: any - ) { - const fetchedData = await this.fetchHandleMessageSentData(sentMessage); - if (!fetchedData) { - return; - } - const { msg } = fetchedData; - - await msg.handleMessageSentFailure(sentMessage, error); - } - private async setupLeftPane() { // Here we set up a full redux store with initial state for our LeftPane Root const convoCollection = ConversationController.getInstance().getConversations(); @@ -206,22 +156,6 @@ export class SessionInboxView extends React.Component { this.store.dispatch ); - this.fetchHandleMessageSentData = this.fetchHandleMessageSentData.bind( - this - ); - this.handleMessageSentFailure = this.handleMessageSentFailure.bind(this); - this.handleMessageSentSuccess = this.handleMessageSentSuccess.bind(this); - - getMessageQueue().events.addListener( - 'sendSuccess', - this.handleMessageSentSuccess - ); - - getMessageQueue().events.addListener( - 'sendFail', - this.handleMessageSentFailure - ); - window.Whisper.events.on('messageExpired', messageExpired); window.Whisper.events.on('messageChanged', messageChanged); window.Whisper.events.on('messageAdded', messageAdded); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index e1cf5ad709..36c06bcb03 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -228,6 +228,15 @@ export class ConversationModel extends Backbone.Model { public isMediumGroup() { return this.get('is_medium_group'); } + /** + * Returns true if this conversation is active + * i.e. the conversation is visibie on the left pane. (Either we or another user created this convo). + * This is useful because we do not want bumpTyping on the first message typing to a new convo to + * send a message. + */ + public isActive() { + return Boolean(this.get('active_at')); + } public async block() { if (!this.id || this.isPublic()) { return; @@ -251,13 +260,15 @@ export class ConversationModel extends Backbone.Model { await this.commit(); } public async bumpTyping() { - if (this.isPublic() || this.isMediumGroup()) { - return; - } // We don't send typing messages if the setting is disabled // or we blocked that user - - if (!window.storage.get('typing-indicators-setting') || this.isBlocked()) { + if ( + this.isPublic() || + this.isMediumGroup() || + !this.isActive() || + !window.storage.get('typing-indicators-setting') || + this.isBlocked() + ) { return; } @@ -408,27 +419,6 @@ export class ConversationModel extends Backbone.Model { await Promise.all(messages.map((m: any) => m.setCalculatingPoW())); } - public async onPublicMessageSent({ - identifier, - serverId, - serverTimestamp, - }: { - identifier: string; - serverId: number; - serverTimestamp: number; - }) { - const registeredMessage = MessageController.getInstance().get(identifier); - - if (!registeredMessage || !registeredMessage.message) { - return null; - } - const model = registeredMessage.message; - await model.setIsPublic(true); - await model.setServerId(serverId); - await model.setServerTimestamp(serverTimestamp); - return undefined; - } - public format() { return this.cachedProps; } @@ -679,7 +669,7 @@ export class ConversationModel extends Backbone.Model { }; const openGroupMessage = new OpenGroupMessage(openGroupParams); // we need the return await so that errors are caught in the catch {} - await getMessageQueue().sendToGroup(openGroupMessage); + await getMessageQueue().sendToOpenGroup(openGroupMessage); return; } const chatMessageParams: ChatMessageParams = { @@ -800,19 +790,17 @@ export class ConversationModel extends Backbone.Model { messageWithSchema.source = UserUtils.getOurPubKeyStrFromCache(); messageWithSchema.sourceDevice = 1; + // set the serverTimestamp only if this conversation is a public one. const attributes: MessageAttributesOptionals = { ...messageWithSchema, groupInvitation, conversationId: this.id, destination: isPrivate ? destination : undefined, + serverTimestamp: this.isPublic() ? new Date().getTime() : undefined, }; const model = await this.addSingleMessage(attributes); - if (this.isPublic()) { - await model.setServerTimestamp(new Date().getTime()); - } - this.set({ lastMessage: model.getNotificationText(), lastMessageStatus: 'sending', diff --git a/ts/models/message.ts b/ts/models/message.ts index 2e0252be12..bca525922b 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -872,7 +872,7 @@ export class MessageModel extends Backbone.Model { ...uploaded, }; const openGroupMessage = new OpenGroupMessage(openGroupParams); - return getMessageQueue().sendToGroup(openGroupMessage); + return getMessageQueue().sendToOpenGroup(openGroupMessage); } const { body, attachments, preview, quote } = await this.uploadData(); @@ -1148,42 +1148,6 @@ export class MessageModel extends Backbone.Model { await this.commit(); } - public async setServerId(serverId: number) { - if (_.isEqual(this.get('serverId'), serverId)) { - return; - } - - this.set({ - serverId, - }); - - await this.commit(); - } - - public async setServerTimestamp(serverTimestamp?: number) { - if (_.isEqual(this.get('serverTimestamp'), serverTimestamp)) { - return; - } - - this.set({ - serverTimestamp, - }); - - await this.commit(); - } - - public async setIsPublic(isPublic: boolean) { - if (_.isEqual(this.get('isPublic'), isPublic)) { - return; - } - - this.set({ - isPublic: !!isPublic, - }); - - await this.commit(); - } - public async sendSyncMessageOnly(dataMessage: any) { const now = Date.now(); this.set({ diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 4e46c5efa0..152b2059ac 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -6,7 +6,6 @@ import { fromHex, fromHexToArray, toHex } from '../utils/String'; import { BlockedNumberController } from '../../util/blockedNumberController'; import { ConversationController } from '../conversations'; import { updateOpenGroup } from '../../receiver/openGroups'; -import { getMessageQueue } from '../instance'; import { addClosedGroupEncryptionKeyPair, getIdentityKeyById, @@ -33,6 +32,7 @@ import { MessageModel } from '../../models/message'; import { MessageModelType } from '../../models/messageType'; import { MessageController } from '../messages'; import { distributingClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; +import { getMessageQueue } from '..'; export interface GroupInfo { id: string; diff --git a/ts/session/index.ts b/ts/session/index.ts index 639c2eb171..37486ad13e 100644 --- a/ts/session/index.ts +++ b/ts/session/index.ts @@ -6,7 +6,7 @@ import * as Sending from './sending'; import * as Constants from './constants'; import * as ClosedGroup from './group'; -export * from './instance'; +const getMessageQueue = Sending.getMessageQueue; export { Conversations, @@ -16,4 +16,5 @@ export { Sending, Constants, ClosedGroup, + getMessageQueue, }; diff --git a/ts/session/instance.ts b/ts/session/instance.ts deleted file mode 100644 index cf7dc55a3d..0000000000 --- a/ts/session/instance.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MessageQueue } from './sending/'; - -let messageQueue: MessageQueue; - -function getMessageQueue(): MessageQueue { - if (!messageQueue) { - messageQueue = new MessageQueue(); - } - return messageQueue; -} - -export { getMessageQueue }; diff --git a/ts/session/messages/MessageController.ts b/ts/session/messages/MessageController.ts index 26b40baf50..66a84e494f 100644 --- a/ts/session/messages/MessageController.ts +++ b/ts/session/messages/MessageController.ts @@ -30,6 +30,11 @@ export class MessageController { } public register(id: string, message: MessageModel) { + if (!(message instanceof MessageModel)) { + throw new Error( + 'Only MessageModels can be registered to the MessageController.' + ); + } const existing = this.messageLookup.get(id); if (existing) { this.messageLookup.set(id, { diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index d4475426e8..f742678d83 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -1,15 +1,12 @@ -import { EventEmitter } from 'events'; import { - ChatMessage, ClosedGroupChatMessage, ClosedGroupNewMessage, ContentMessage, - DataMessage, ExpirationTimerUpdateMessage, OpenGroupMessage, } from '../messages/outgoing'; import { PendingMessageCache } from './PendingMessageCache'; -import { JobQueue, TypedEventEmitter, UserUtils } from '../utils'; +import { JobQueue, UserUtils } from '../utils'; import { PubKey, RawMessage } from '../types'; import { MessageSender } from '.'; import { ClosedGroupMessage } from '../messages/outgoing/content/data/group/ClosedGroupMessage'; @@ -23,9 +20,9 @@ import { ClosedGroupUpdateMessage, } from '../messages/outgoing/content/data/group'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/content/data/group/ClosedGroupMemberLeftMessage'; +import { MessageSentHandler } from './MessageSentHandler'; -export type GroupMessageType = - | OpenGroupMessage +type ClosedGroupMessageType = | ClosedGroupChatMessage | ClosedGroupAddedMembersMessage | ClosedGroupRemovedMembersMessage @@ -37,21 +34,12 @@ export type GroupMessageType = | ClosedGroupEncryptionPairRequestMessage; // ClosedGroupEncryptionPairReplyMessage must be sent to a user pubkey. Not a group. -export interface MessageQueueInterfaceEvents { - sendSuccess: ( - message: RawMessage | OpenGroupMessage, - wrappedEnvelope?: Uint8Array - ) => void; - sendFail: (message: RawMessage | OpenGroupMessage, error: Error) => void; -} export class MessageQueue { - public readonly events: TypedEventEmitter; private readonly jobQueues: Map = new Map(); private readonly pendingMessageCache: PendingMessageCache; constructor(cache?: PendingMessageCache) { - this.events = new EventEmitter(); this.pendingMessageCache = cache ?? new PendingMessageCache(); void this.processAllPending(); } @@ -70,18 +58,37 @@ export class MessageQueue { await this.process(user, message, sentCb); } - public async send( - device: PubKey, - message: ContentMessage, - sentCb?: (message: RawMessage) => Promise - ): Promise { - if ( - message instanceof ConfigurationMessage || - !!(message as any).syncTarget - ) { - throw new Error('SyncMessage needs to be sent with sendSyncMessage'); + /** + * This function is synced. It will wait for the message to be delivered to the open + * group to return. + * So there is no need for a sendCb callback + * + */ + public async sendToOpenGroup(message: OpenGroupMessage) { + // Open groups + if (!(message instanceof OpenGroupMessage)) { + throw new Error('sendToOpenGroup can only be used with OpenGroupMessage'); + } + // No queue needed for Open Groups; send directly + const error = new Error('Failed to send message to open group.'); + + // This is absolutely yucky ... we need to make it not use Promise + try { + const result = await MessageSender.sendToOpenGroup(message); + // sendToOpenGroup returns -1 if failed or an id if succeeded + if (result.serverId < 0) { + void MessageSentHandler.handleMessageSentFailure(message, error); + } else { + void MessageSentHandler.handleMessageSentSuccess(message); + void MessageSentHandler.handlePublicMessageSentSuccess(message, result); + } + } catch (e) { + window?.log?.warn( + `Failed to send message to open group: ${message.group.server}`, + e + ); + void MessageSentHandler.handleMessageSentFailure(message, error); } - await this.process(device, message, sentCb); } /** @@ -89,43 +96,9 @@ export class MessageQueue { * @param sentCb currently only called for medium groups sent message */ public async sendToGroup( - message: GroupMessageType, + message: ClosedGroupMessageType, sentCb?: (message: RawMessage) => Promise ): Promise { - // Open groups - if (message instanceof OpenGroupMessage) { - // No queue needed for Open Groups; send directly - const error = new Error('Failed to send message to open group.'); - - // This is absolutely yucky ... we need to make it not use Promise - try { - const result = await MessageSender.sendToOpenGroup(message); - // sendToOpenGroup returns -1 if failed or an id if succeeded - if (result.serverId < 0) { - this.events.emit('sendFail', message, error); - } else { - const messageEventData = { - identifier: message.identifier, - pubKey: message.group.groupId, - timestamp: message.timestamp, - serverId: result.serverId, - serverTimestamp: result.serverTimestamp, - }; - this.events.emit('sendSuccess', message); - - window.Whisper.events.trigger('publicMessageSent', messageEventData); - } - } catch (e) { - window?.log?.warn( - `Failed to send message to open group: ${message.group.server}`, - e - ); - this.events.emit('sendFail', message, error); - } - - return; - } - let groupId: PubKey | undefined; if ( message instanceof ExpirationTimerUpdateMessage || @@ -138,7 +111,7 @@ export class MessageQueue { throw new Error('Invalid group message passed in sendToGroup.'); } // if groupId is set here, it means it's for a medium group. So send it as it - return this.send(PubKey.cast(groupId), message, sentCb); + return this.sendToPubKey(PubKey.cast(groupId), message, sentCb); } public async sendSyncMessage( @@ -157,10 +130,6 @@ export class MessageQueue { const ourPubKey = UserUtils.getOurPubKeyStrFromCache(); - if (!ourPubKey) { - throw new Error('ourNumber is not set'); - } - await this.process(PubKey.cast(ourPubKey), message, sentCb); } @@ -176,7 +145,11 @@ export class MessageQueue { const job = async () => { try { const wrappedEnvelope = await MessageSender.send(message); - this.events.emit('sendSuccess', message, wrappedEnvelope); + void MessageSentHandler.handleMessageSentSuccess( + message, + wrappedEnvelope + ); + const cb = this.pendingMessageCache.callbacks.get( message.identifier ); @@ -185,8 +158,8 @@ export class MessageQueue { await cb(message); } this.pendingMessageCache.callbacks.delete(message.identifier); - } catch (e) { - this.events.emit('sendFail', message, e); + } catch (error) { + void MessageSentHandler.handleMessageSentFailure(message, error); } finally { // Remove from the cache because retrying is done in the sender void this.pendingMessageCache.remove(message); @@ -243,3 +216,12 @@ export class MessageQueue { return queue; } } + +let messageQueue: MessageQueue; + +export function getMessageQueue(): MessageQueue { + if (!messageQueue) { + messageQueue = new MessageQueue(); + } + return messageQueue; +} diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts new file mode 100644 index 0000000000..dd30f5777a --- /dev/null +++ b/ts/session/sending/MessageSentHandler.ts @@ -0,0 +1,93 @@ +import { getMessageById } from '../../data/data'; +import { MessageController } from '../messages'; +import { OpenGroupMessage } from '../messages/outgoing'; +import { RawMessage } from '../types'; + +export class MessageSentHandler { + /** + * This function tries to find a message by messageId by first looking on the MessageController. + * The MessageController holds all messages being in memory. + * Those are the messages sent recently, recieved recently, or the one shown to the user. + * + * If the app restarted, it's very likely those messages won't be on the memory anymore. + * In this case, this function will look for it in the database and return it. + * If the message is found on the db, it will also register it to the MessageController so our subsequent calls are quicker. + */ + private static async fetchHandleMessageSentData( + m: RawMessage | OpenGroupMessage + ) { + // if a message was sent and this message was created after the last app restart, + // this message is still in memory in the MessageController + const msg = MessageController.getInstance().get(m.identifier); + + if (!msg || !msg.message) { + // otherwise, look for it in the database + // nobody is listening to this freshly fetched message .trigger calls + const dbMessage = await getMessageById(m.identifier); + + if (!dbMessage) { + return null; + } + MessageController.getInstance().register(m.identifier, dbMessage); + return dbMessage; + } + + return msg.message; + } + + public static async handlePublicMessageSentSuccess( + sentMessage: OpenGroupMessage, + result: { serverId: number; serverTimestamp: number } + ) { + const { serverId, serverTimestamp } = result; + try { + const foundMessage = await MessageSentHandler.fetchHandleMessageSentData( + sentMessage + ); + + if (!foundMessage) { + throw new Error( + 'handlePublicMessageSentSuccess(): The message should be in memory for an openGroup message' + ); + } + + foundMessage.set({ + serverTimestamp, + serverId, + isPublic: true, + }); + await foundMessage.commit(); + } catch (e) { + window.log.error('Error setting public on message'); + } + } + + public static async handleMessageSentSuccess( + sentMessage: RawMessage | OpenGroupMessage, + wrappedEnvelope?: Uint8Array + ) { + // The wrappedEnvelope will be set only if the message is not one of OpenGroupMessage type. + const fetchedMessage = await MessageSentHandler.fetchHandleMessageSentData( + sentMessage + ); + if (!fetchedMessage) { + return; + } + + void fetchedMessage.handleMessageSentSuccess(sentMessage, wrappedEnvelope); + } + + public static async handleMessageSentFailure( + sentMessage: RawMessage | OpenGroupMessage, + error: any + ) { + const fetchedMessage = await MessageSentHandler.fetchHandleMessageSentData( + sentMessage + ); + if (!fetchedMessage) { + return; + } + + await fetchedMessage.handleMessageSentFailure(sentMessage, error); + } +} diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts index 28b9b8708e..fd67424788 100644 --- a/ts/session/sending/PendingMessageCache.ts +++ b/ts/session/sending/PendingMessageCache.ts @@ -4,7 +4,6 @@ import { PartialRawMessage, RawMessage } from '../types/RawMessage'; import { ContentMessage } from '../messages/outgoing'; import { PubKey } from '../types'; import { MessageUtils } from '../utils'; -import { GroupMessageType } from '.'; // This is an abstraction for storing pending messages. // Ideally we want to store pending messages in the database so that diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index db17203908..d8cba68b66 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -45,36 +45,33 @@ export const forceSyncConfigurationNowIfNeeded = async ( ) => { const allConvos = ConversationController.getInstance().getConversations(); const configMessage = await getCurrentConfigurationMessage(allConvos); - window.log.info('forceSyncConfigurationNowIfNeeded with', configMessage); - const waitForMessageSentEvent = new Promise(resolve => { - const ourResolver = (message: any) => { + async function waitForMessageSentEvent(message: RawMessage) { + return new Promise(resolve => { if (message.identifier === configMessage.identifier) { - getMessageQueue().events.off('sendSuccess', ourResolver); - getMessageQueue().events.off('sendFail', ourResolver); resolve(true); } - }; - getMessageQueue().events.on('sendSuccess', ourResolver); - getMessageQueue().events.on('sendFail', ourResolver); - }); + }); + } try { - // this just adds the message to the sending queue. - // if waitForMessageSent is set, we need to effectively wait until then - await Promise.all([ - getMessageQueue().sendSyncMessage(configMessage), - waitForMessageSentEvent, - ]); + // passing the callback like that + if (waitForMessageSent) { + await getMessageQueue().sendSyncMessage( + configMessage, + waitForMessageSentEvent as any + ); + return Promise.resolve(); + } else { + await getMessageQueue().sendSyncMessage(configMessage); + return waitForMessageSentEvent; + } } catch (e) { window.log.warn( 'Caught an error while sending our ConfigurationMessage:', e ); } - if (!waitForMessageSent) { - return; - } - return waitForMessageSentEvent; + return Promise.resolve(); }; diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index 778125d4ff..9ad6102ff1 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -18,6 +18,7 @@ import { PendingMessageCacheStub } from '../../../test-utils/stubs'; import { ClosedGroupMessage } from '../../../../session/messages/outgoing/content/data/group/ClosedGroupMessage'; import chaiAsPromised from 'chai-as-promised'; +import { MessageSentHandler } from '../../../../session/sending/MessageSentHandler'; chai.use(chaiAsPromised as any); chai.should(); @@ -32,6 +33,9 @@ describe('MessageQueue', () => { // Initialize new stubbed queue let pendingMessageCache: PendingMessageCacheStub; + let messageSentHandlerFailedStub: sinon.SinonStub; + let messageSentHandlerSuccessStub: sinon.SinonStub; + let messageSentPublicHandlerSuccessStub: sinon.SinonStub; let messageQueueStub: MessageQueue; // Message Sender Stubs @@ -47,6 +51,15 @@ describe('MessageQueue', () => { // Message Sender Stubs sendStub = sandbox.stub(MessageSender, 'send').resolves(); + messageSentHandlerFailedStub = sandbox + .stub(MessageSentHandler as any, 'handleMessageSentFailure') + .resolves(); + messageSentHandlerSuccessStub = sandbox + .stub(MessageSentHandler as any, 'handleMessageSentSuccess') + .resolves(); + messageSentPublicHandlerSuccessStub = sandbox + .stub(MessageSentHandler as any, 'handlePublicMessageSentSuccess') + .resolves(); // Init Queue pendingMessageCache = new PendingMessageCacheStub(); @@ -59,16 +72,22 @@ describe('MessageQueue', () => { }); describe('processPending', () => { - it('will send messages', async () => { + it('will send messages', done => { const device = TestUtils.generateFakePubKey(); - await pendingMessageCache.add(device, TestUtils.generateChatMessage()); - const successPromise = PromiseUtils.waitForTask(done => { - messageQueueStub.events.once('sendSuccess', done); + const waitForMessageSentEvent = new Promise(resolve => { + resolve(true); + done(); }); - await messageQueueStub.processPending(device); - // tslint:disable-next-line: no-unused-expression - expect(successPromise).to.eventually.be.fulfilled; + + pendingMessageCache.add( + device, + TestUtils.generateChatMessage(), + waitForMessageSentEvent as any + ); + + messageQueueStub.processPending(device); + expect(waitForMessageSentEvent).to.be.fulfilled; }); it('should remove message from cache', async () => { @@ -96,46 +115,48 @@ describe('MessageQueue', () => { }).timeout(15000); describe('events', () => { - it('should send a success event if message was sent', async () => { + it('should send a success event if message was sent', done => { const device = TestUtils.generateFakePubKey(); const message = TestUtils.generateChatMessage(); - await pendingMessageCache.add(device, message); - - const eventPromise = PromiseUtils.waitForTask< - RawMessage | OpenGroupMessage - >(complete => { - messageQueueStub.events.once('sendSuccess', complete); + const waitForMessageSentEvent = new Promise(resolve => { + resolve(true); + done(); }); - await messageQueueStub.processPending(device); - - const rawMessage = await eventPromise; - expect(rawMessage.identifier).to.equal(message.identifier); + pendingMessageCache + .add(device, message, waitForMessageSentEvent as any) + .then(() => messageQueueStub.processPending(device)) + .then(() => { + expect(messageSentHandlerSuccessStub.callCount).to.be.equal(1); + expect( + messageSentHandlerSuccessStub.lastCall.args[0].identifier + ).to.be.equal(message.identifier); + }); }); - it('should send a fail event if something went wrong while sending', async () => { + it('should send a fail event if something went wrong while sending', done => { sendStub.throws(new Error('failure')); - const spy = sandbox.spy(); - messageQueueStub.events.on('sendFail', spy); - const device = TestUtils.generateFakePubKey(); const message = TestUtils.generateChatMessage(); - await pendingMessageCache.add(device, message); - - const eventPromise = PromiseUtils.waitForTask< - [RawMessage | OpenGroupMessage, Error] - >(complete => { - messageQueueStub.events.once('sendFail', (...args) => { - complete(args); - }); + const waitForMessageSentEvent = new Promise(resolve => { + resolve(true); + done(); }); - await messageQueueStub.processPending(device); - - const [rawMessage, error] = await eventPromise; - expect(rawMessage.identifier).to.equal(message.identifier); - expect(error.message).to.equal('failure'); + pendingMessageCache + .add(device, message, waitForMessageSentEvent as any) + .then(() => messageQueueStub.processPending(device)) + .then(() => { + expect(messageSentHandlerFailedStub.callCount).to.be.equal(1); + expect( + messageSentHandlerFailedStub.lastCall.args[0].identifier + ).to.be.equal(message.identifier); + expect( + messageSentHandlerFailedStub.lastCall.args[1].message + ).to.equal('failure'); + expect(waitForMessageSentEvent).to.be.eventually.fulfilled; + }); }); }); }); @@ -155,12 +176,11 @@ describe('MessageQueue', () => { }); describe('sendToGroup', () => { - it('should throw an error if invalid non-group message was passed', () => { - // const chatMessage = TestUtils.generateChatMessage(); - // await expect( - // messageQueueStub.sendToGroup(chatMessage) - // ).to.be.rejectedWith('Invalid group message passed in sendToGroup.'); - // Cannot happen with typescript as this function only accept group message now + it('should throw an error if invalid non-group message was passed', async () => { + const chatMessage = TestUtils.generateChatMessage(); + await expect( + messageQueueStub.sendToGroup(chatMessage as any) + ).to.be.rejectedWith('Invalid group message passed in sendToGroup.'); }); describe('closed groups', () => { @@ -170,7 +190,7 @@ describe('MessageQueue', () => { ); sandbox.stub(GroupUtils, 'getGroupMembers').resolves(members); - const send = sandbox.stub(messageQueueStub, 'send').resolves(); + const send = sandbox.stub(messageQueueStub, 'sendToPubKey').resolves(); const message = TestUtils.generateClosedGroupMessage(); await messageQueueStub.sendToGroup(message); @@ -196,34 +216,43 @@ describe('MessageQueue', () => { it('can send to open group', async () => { const message = TestUtils.generateOpenGroupMessage(); - await messageQueueStub.sendToGroup(message); + await messageQueueStub.sendToOpenGroup(message); expect(sendToOpenGroupStub.callCount).to.equal(1); }); it('should emit a success event when send was successful', async () => { sendToOpenGroupStub.resolves({ serverId: 5125, - serverTimestamp: 5125, + serverTimestamp: 5126, }); const message = TestUtils.generateOpenGroupMessage(); - const eventPromise = PromiseUtils.waitForTask(complete => { - messageQueueStub.events.once('sendSuccess', complete); - }, 2000); - - await messageQueueStub.sendToGroup(message); - return eventPromise.should.be.fulfilled; + await messageQueueStub.sendToOpenGroup(message); + expect(messageSentHandlerSuccessStub.callCount).to.equal(1); + expect( + messageSentHandlerSuccessStub.lastCall.args[0].identifier + ).to.equal(message.identifier); + expect(messageSentPublicHandlerSuccessStub.callCount).to.equal(1); + expect( + messageSentPublicHandlerSuccessStub.lastCall.args[0].identifier + ).to.equal(message.identifier); + expect( + messageSentPublicHandlerSuccessStub.lastCall.args[1].serverId + ).to.equal(5125); + expect( + messageSentPublicHandlerSuccessStub.lastCall.args[1].serverTimestamp + ).to.equal(5126); }); it('should emit a fail event if something went wrong', async () => { sendToOpenGroupStub.resolves({ serverId: -1, serverTimestamp: -1 }); const message = TestUtils.generateOpenGroupMessage(); - const eventPromise = PromiseUtils.waitForTask(complete => { - messageQueueStub.events.once('sendFail', complete); - }, 2000); - await messageQueueStub.sendToGroup(message); - return eventPromise.should.be.fulfilled; + await messageQueueStub.sendToOpenGroup(message); + expect(messageSentHandlerFailedStub.callCount).to.equal(1); + expect( + messageSentHandlerFailedStub.lastCall.args[0].identifier + ).to.equal(message.identifier); }); }); }); From 3f43ae48ade4d6b44235ab8b7bf0d3f9df5b84c8 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 17 Feb 2021 11:17:50 +1100 Subject: [PATCH 035/109] WIP --- ts/session/messages/outgoing/content/data/ChatMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 147f65dff3..6e4677bf1d 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -92,7 +92,7 @@ export class ChatMessage extends DataMessage { profileKey: dataMessage.profileKey, }; - if ((dataMessage as any)?.$type?.name !== 'DataMessage') { + if ((dataMessage as any)?.$type?.name !== 'DataMessage' && !(dataMessage instanceof DataMessage)) { throw new Error( 'Tried to build a sync message from something else than a DataMessage' ); From d844c5141ec9241a1905b14d88d107987d334aeb Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 10:35:11 +1100 Subject: [PATCH 036/109] remove unprocessed from store. Nothing was stored in the store it was simply as passthrough to the Data file. No we directly call the data file instead --- Gruntfile.js | 2 -- libtextsecure/storage/unprocessed.js | 39 ---------------------------- ts/receiver/cache.ts | 38 +++++++++++++++------------ ts/receiver/receiver.ts | 2 +- ts/session/utils/syncUtils.ts | 2 +- 5 files changed, 23 insertions(+), 60 deletions(-) delete mode 100644 libtextsecure/storage/unprocessed.js diff --git a/Gruntfile.js b/Gruntfile.js index 557e0085b3..d09285c67d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -76,12 +76,10 @@ module.exports = grunt => { 'libtextsecure/errors.js', 'libtextsecure/libsignal-protocol.js', 'libtextsecure/protocol_wrapper.js', - 'libtextsecure/crypto.js', 'libtextsecure/storage.js', 'libtextsecure/storage/user.js', 'libtextsecure/storage/groups.js', - 'libtextsecure/storage/unprocessed.js', 'libtextsecure/protobufs.js', 'libtextsecure/helpers.js', 'libtextsecure/stringview.js', diff --git a/libtextsecure/storage/unprocessed.js b/libtextsecure/storage/unprocessed.js deleted file mode 100644 index d32603159a..0000000000 --- a/libtextsecure/storage/unprocessed.js +++ /dev/null @@ -1,39 +0,0 @@ -/* global window */ - -// eslint-disable-next-line func-names -(function() { - /** *************************************** - *** Not-yet-processed message storage *** - **************************************** */ - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - - window.textsecure.storage.unprocessed = { - getCount() { - return window.Signal.Data.getUnprocessedCount(); - }, - getAll() { - return window.Signal.Data.getAllUnprocessed(); - }, - get(id) { - return window.Signal.Data.getUnprocessedById(id); - }, - add(data) { - return window.Signal.Data.saveUnprocessed(data, { - forceSave: true, - }); - }, - updateAttempts(id, attempts) { - return window.Signal.Data.updateUnprocessedAttempts(id, attempts); - }, - addDecryptedData(id, data) { - return window.Signal.Data.updateUnprocessedWithData(id, data); - }, - remove(id) { - return window.Signal.Data.removeUnprocessed(id); - }, - removeAll() { - return window.Signal.Data.removeAllUnprocessed(); - }, - }; -})(); diff --git a/ts/receiver/cache.ts b/ts/receiver/cache.ts index 02a0c2be9a..6c94fe805a 100644 --- a/ts/receiver/cache.ts +++ b/ts/receiver/cache.ts @@ -1,12 +1,22 @@ import { EnvelopePlus } from './types'; import { StringUtils } from '../session/utils'; import _ from 'lodash'; +import { + getAllUnprocessed, + getUnprocessedById, + getUnprocessedCount, + removeAllUnprocessed, + removeUnprocessed, + saveUnprocessed, + updateUnprocessedAttempts, + updateUnprocessedWithData, +} from '../data/data'; export async function removeFromCache(envelope: EnvelopePlus) { const { id } = envelope; window.log.info(`removing from cache envelope: ${id}`); - return window.textsecure.storage.unprocessed.remove(id); + return removeUnprocessed(id); } export async function addToCache( @@ -28,23 +38,23 @@ export async function addToCache( data.senderIdentity = envelope.senderIdentity; } - return window.textsecure.storage.unprocessed.add(data); + return saveUnprocessed(data, { forceSave: true }); } async function fetchAllFromCache(): Promise> { const { textsecure } = window; - const count = await textsecure.storage.unprocessed.getCount(); + const count = await getUnprocessedCount(); if (count > 1500) { - await textsecure.storage.unprocessed.removeAll(); + await removeAllUnprocessed(); window.log.warn( `There were ${count} messages in cache. Deleted all instead of reprocessing` ); return []; } - const items = await textsecure.storage.unprocessed.getAll(); + const items = await getAllUnprocessed(); return items; } @@ -65,12 +75,9 @@ export async function getAllFromCache() { 'getAllFromCache final attempt for envelope', item.id ); - await textsecure.storage.unprocessed.remove(item.id); + await removeUnprocessed(item.id); } else { - await textsecure.storage.unprocessed.updateAttempts( - item.id, - attempts - ); + await updateUnprocessedAttempts(item.id, attempts); } } catch (error) { window.log.error( @@ -109,12 +116,9 @@ export async function getAllFromCacheForSource(source: string) { 'getAllFromCache final attempt for envelope', item.id ); - await textsecure.storage.unprocessed.remove(item.id); + await removeUnprocessed(item.id); } else { - await textsecure.storage.unprocessed.updateAttempts( - item.id, - attempts - ); + await updateUnprocessedAttempts(item.id, attempts); } } catch (error) { window.log.error( @@ -133,7 +137,7 @@ export async function updateCache( plaintext: ArrayBuffer ): Promise { const { id } = envelope; - const item = await window.textsecure.storage.unprocessed.get(id); + const item = await getUnprocessedById(id); if (!item) { window.log.error(`updateCache: Didn't find item ${id} in cache to update`); return; @@ -148,5 +152,5 @@ export async function updateCache( item.decrypted = StringUtils.decode(plaintext, 'base64'); - return window.textsecure.storage.unprocessed.addDecryptedData(item.id, item); + return updateUnprocessedWithData(item.id, item); } diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index cbea0a2cf7..982ca0f60d 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -233,7 +233,7 @@ async function queueCached(item: any) { try { const { id } = item; - await textsecure.storage.unprocessed.remove(id); + await removeUnprocessed(id); } catch (deleteError) { window.log.error( 'queueCached error deleting item', diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index d8cba68b66..e9ab76191a 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -25,7 +25,7 @@ export const syncConfigurationIfNeeded = async () => { const allConvos = ConversationController.getInstance().getConversations(); const configMessage = await getCurrentConfigurationMessage(allConvos); try { - window.log.info('syncConfigurationIfNeeded with', configMessage); + // window.log.info('syncConfigurationIfNeeded with', configMessage); await getMessageQueue().sendSyncMessage(configMessage); } catch (e) { From 7e77a3f3b6675d91249aaf4e7771d13ff564dc2e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 11:57:23 +1100 Subject: [PATCH 037/109] remove unused functions in Data.ts and sql.js --- app/sql.js | 151 +++++++----------------------------------------- ts/data/data.ts | 71 ----------------------- 2 files changed, 22 insertions(+), 200 deletions(-) diff --git a/app/sql.js b/app/sql.js index 444bde45da..7923c481c6 100644 --- a/app/sql.js +++ b/app/sql.js @@ -33,8 +33,6 @@ module.exports = { removePasswordHash, getIdentityKeyById, - removeAllIdentityKeys, - getAllIdentityKeys, removeAllSignedPreKeys, removeAllContactPreKeys, @@ -45,9 +43,7 @@ module.exports = { createOrUpdateItem, getItemById, getAllItems, - bulkAddItems, removeItemById, - removeAllItems, getSwarmNodesForPubkey, updateSwarmNodesForPubkey, @@ -56,7 +52,6 @@ module.exports = { getConversationCount, saveConversation, - saveConversations, getConversationById, savePublicServerToken, getPublicServerTokenByServerUrl, @@ -64,7 +59,6 @@ module.exports = { removeConversation, getAllConversations, getAllPublicConversations, - getPublicConversationsByServer, getPubkeysInPublicConversation, getAllConversationIds, getAllGroupsInvolvingId, @@ -90,7 +84,6 @@ module.exports = { getMessageById, getAllMessages, getAllMessageIds, - getAllUnsentMessages, getMessagesBySentAt, getSeenMessagesByHashList, getLastHashBySnode, @@ -105,7 +98,6 @@ module.exports = { updateUnprocessedAttempts, updateUnprocessedWithData, getUnprocessedById, - saveUnprocesseds, removeUnprocessed, removeAllUnprocessed, @@ -130,10 +122,6 @@ module.exports = { removeAllClosedGroupEncryptionKeyPairs, }; -function generateUUID() { - return uuidv4(); -} - function objectToJSON(data) { return JSON.stringify(data); } @@ -1265,35 +1253,30 @@ const IDENTITY_KEYS_TABLE = 'identityKeys'; async function getIdentityKeyById(id, instance) { return getById(IDENTITY_KEYS_TABLE, id, instance); } -async function removeAllIdentityKeys() { - return removeAllFromTable(IDENTITY_KEYS_TABLE); -} -async function getAllIdentityKeys() { - return getAllFromTable(IDENTITY_KEYS_TABLE); -} +// those removeAll calls are currently only used to cleanup the db from old data +// TODO remove those and move those removeAll in a migration const PRE_KEYS_TABLE = 'preKeys'; - async function removeAllPreKeys() { return removeAllFromTable(PRE_KEYS_TABLE); } - const CONTACT_PRE_KEYS_TABLE = 'contactPreKeys'; - async function removeAllContactPreKeys() { return removeAllFromTable(CONTACT_PRE_KEYS_TABLE); } - const CONTACT_SIGNED_PRE_KEYS_TABLE = 'contactSignedPreKeys'; async function removeAllContactSignedPreKeys() { return removeAllFromTable(CONTACT_SIGNED_PRE_KEYS_TABLE); } - const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys'; async function removeAllSignedPreKeys() { return removeAllFromTable(SIGNED_PRE_KEYS_TABLE); } +const SESSIONS_TABLE = 'sessions'; +async function removeAllSessions() { + return removeAllFromTable(SESSIONS_TABLE); +} const GUARD_NODE_TABLE = 'guardNodes'; @@ -1342,20 +1325,9 @@ async function getAllItems() { const rows = await db.all('SELECT json FROM items ORDER BY id ASC;'); return map(rows, row => jsonToObject(row.json)); } -async function bulkAddItems(array) { - return bulkAdd(ITEMS_TABLE, array); -} async function removeItemById(id) { return removeById(ITEMS_TABLE, id); } -async function removeAllItems() { - return removeAllFromTable(ITEMS_TABLE); -} - -const SESSIONS_TABLE = 'sessions'; -async function removeAllSessions() { - return removeAllFromTable(SESSIONS_TABLE); -} async function createOrUpdate(table, data) { const { id } = data; @@ -1378,20 +1350,6 @@ async function createOrUpdate(table, data) { ); } -async function bulkAdd(table, array) { - let promise; - - db.serialize(() => { - promise = Promise.all([ - db.run('BEGIN TRANSACTION;'), - ...map(array, data => createOrUpdate(table, data)), - db.run('COMMIT TRANSACTION;'), - ]); - }); - - await promise; -} - async function getById(table, id, instance) { const row = await (db || instance).get( `SELECT * FROM ${table} WHERE id = $id;`, @@ -1428,11 +1386,6 @@ async function removeAllFromTable(table) { await db.run(`DELETE FROM ${table};`); } -async function getAllFromTable(table) { - const rows = await db.all(`SELECT json FROM ${table};`); - return rows.map(row => jsonToObject(row.json)); -} - // Conversations async function getSwarmNodesForPubkey(pubkey) { @@ -1524,22 +1477,6 @@ async function saveConversation(data) { ); } -async function saveConversations(arrayOfConversations) { - let promise; - - db.serialize(() => { - promise = Promise.all([ - db.run('BEGIN TRANSACTION;'), - ...map(arrayOfConversations, conversation => - saveConversation(conversation) - ), - db.run('COMMIT TRANSACTION;'), - ]); - }); - - await promise; -} - async function updateConversation(data) { const { id, @@ -1667,19 +1604,6 @@ async function getAllPublicConversations() { return map(rows, row => jsonToObject(row.json)); } -async function getPublicConversationsByServer(server) { - const rows = await db.all( - `SELECT * FROM ${CONVERSATIONS_TABLE} WHERE - server = $server - ORDER BY id ASC;`, - { - $server: server, - } - ); - - return map(rows, row => jsonToObject(row.json)); -} - async function getPubkeysInPublicConversation(id) { const rows = await db.all( `SELECT DISTINCT source FROM ${MESSAGES_TABLE} WHERE @@ -1874,7 +1798,7 @@ async function saveMessage(data, { forceSave } = {}) { const toCreate = { ...data, - id: id || generateUUID(), + id: id || uuidv4(), }; await db.run( @@ -2105,16 +2029,6 @@ async function getMessageBySender({ source, sourceDevice, sent_at }) { return map(rows, row => jsonToObject(row.json)); } -async function getAllUnsentMessages() { - const rows = await db.all(` - SELECT json FROM ${MESSAGES_TABLE} WHERE - type IN ('outgoing') AND - NOT sent - ORDER BY sent_at DESC; - `); - return map(rows, row => jsonToObject(row.json)); -} - async function getUnreadByConversation(conversationId) { const rows = await db.all( `SELECT json FROM ${MESSAGES_TABLE} WHERE @@ -2257,6 +2171,7 @@ async function getNextExpiringMessage() { return map(rows, row => jsonToObject(row.json)); } +/* Unproccessed a received messages not yet processed */ async function saveUnprocessed(data, { forceSave } = {}) { const { id, timestamp, version, attempts, envelope, senderIdentity } = data; if (!id) { @@ -2314,22 +2229,6 @@ async function saveUnprocessed(data, { forceSave } = {}) { return id; } -async function saveUnprocesseds(arrayOfUnprocessed, { forceSave } = {}) { - let promise; - - db.serialize(() => { - promise = Promise.all([ - db.run('BEGIN TRANSACTION;'), - ...map(arrayOfUnprocessed, unprocessed => - saveUnprocessed(unprocessed, { forceSave }) - ), - db.run('COMMIT TRANSACTION;'), - ]); - }); - - await promise; -} - async function updateUnprocessedAttempts(id, attempts) { await db.run('UPDATE unprocessed SET attempts = $attempts WHERE id = $id;', { $id: id, @@ -2484,7 +2383,20 @@ async function removeAll() { db.serialize(() => { promise = Promise.all([ db.run('BEGIN TRANSACTION;'), - ...getRemoveConfigurationPromises(), + db.run('DELETE FROM identityKeys;'), + db.run('DELETE FROM items;'), + db.run('DELETE FROM preKeys;'), + db.run('DELETE FROM sessions;'), + db.run('DELETE FROM signedPreKeys;'), + db.run('DELETE FROM unprocessed;'), + db.run('DELETE FROM contactPreKeys;'), + db.run('DELETE FROM contactSignedPreKeys;'), + db.run('DELETE FROM servers;'), + db.run('DELETE FROM lastHashes;'), + db.run(`DELETE FROM ${SENDER_KEYS_TABLE};`), + db.run(`DELETE FROM ${NODES_FOR_PUBKEY_TABLE};`), + db.run(`DELETE FROM ${CLOSED_GROUP_V2_KEY_PAIRS_TABLE};`), + db.run('DELETE FROM seenMessages;'), db.run(`DELETE FROM ${CONVERSATIONS_TABLE};`), db.run(`DELETE FROM ${MESSAGES_TABLE};`), db.run('DELETE FROM attachment_downloads;'), @@ -2496,25 +2408,6 @@ async function removeAll() { await promise; } -function getRemoveConfigurationPromises() { - return [ - db.run('DELETE FROM identityKeys;'), - db.run('DELETE FROM items;'), - db.run('DELETE FROM preKeys;'), - db.run('DELETE FROM sessions;'), - db.run('DELETE FROM signedPreKeys;'), - db.run('DELETE FROM unprocessed;'), - db.run('DELETE FROM contactPreKeys;'), - db.run('DELETE FROM contactSignedPreKeys;'), - db.run('DELETE FROM servers;'), - db.run('DELETE FROM lastHashes;'), - db.run(`DELETE FROM ${SENDER_KEYS_TABLE};`), - db.run(`DELETE FROM ${NODES_FOR_PUBKEY_TABLE};`), - db.run(`DELETE FROM ${CLOSED_GROUP_V2_KEY_PAIRS_TABLE};`), - db.run('DELETE FROM seenMessages;'), - ]; -} - async function removeAllConversations() { await removeAllFromTable(CONVERSATIONS_TABLE); } diff --git a/ts/data/data.ts b/ts/data/data.ts index 29bcae6262..73804a6a39 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -73,10 +73,6 @@ const channelsToMake = { getPasswordHash, getIdentityKeyById, - - removeAllIdentityKeys, - getAllIdentityKeys, - removeAllPreKeys, removeAllSignedPreKeys, removeAllContactPreKeys, @@ -88,9 +84,7 @@ const channelsToMake = { createOrUpdateItem, getItemById, getAllItems, - bulkAddItems, removeItemById, - removeAllItems, removeAllSessions, @@ -98,7 +92,6 @@ const channelsToMake = { updateSwarmNodesForPubkey, saveConversation, - saveConversations, getConversationById, updateConversation, removeConversation, @@ -106,7 +99,6 @@ const channelsToMake = { getAllConversations, getAllConversationIds, getAllPublicConversations, - getPublicConversationsByServer, getPubkeysInPublicConversation, savePublicServerToken, getPublicServerTokenByServerUrl, @@ -119,7 +111,6 @@ const channelsToMake = { saveMessage, cleanSeenMessages, cleanLastHashes, - saveSeenMessageHash, updateLastHash, saveSeenMessageHashes, saveMessages, @@ -134,7 +125,6 @@ const channelsToMake = { getMessageIdsFromServerIds, getMessageById, getAllMessages, - getAllUnsentMessages, getAllMessageIds, getMessagesBySentAt, getExpiredMessages, @@ -148,7 +138,6 @@ const channelsToMake = { getAllUnprocessed, getUnprocessedById, saveUnprocessed, - saveUnprocesseds, updateUnprocessedAttempts, updateUnprocessedWithData, removeUnprocessed, @@ -442,14 +431,6 @@ export async function getIdentityKeyById( return keysToArrayBuffer(IDENTITY_KEY_KEYS, data); } -export async function removeAllIdentityKeys(): Promise { - await channels.removeAllIdentityKeys(); -} -export async function getAllIdentityKeys() { - const keys = await channels.getAllIdentityKeys(); - return keys.map((key: any) => keysToArrayBuffer(IDENTITY_KEY_KEYS, key)); -} - // Those removeAll are not used anymore except to cleanup the app since we removed all of those tables export async function removeAllPreKeys(): Promise { await channels.removeAllPreKeys(); @@ -508,20 +489,9 @@ export async function getAllItems(): Promise> { return Array.isArray(keys) ? keysToArrayBuffer(keys, item) : item; }); } -export async function bulkAddItems(array: Array): Promise { - const updated = _.map(array, data => { - const { id } = data; - const keys = (ITEM_KEYS as any)[id]; - return Array.isArray(keys) ? keysFromArrayBuffer(keys, data) : data; - }); - await channels.bulkAddItems(updated); -} export async function removeItemById(id: string): Promise { await channels.removeItemById(id); } -export async function removeAllItems(): Promise { - await channels.removeAllItems(); -} // Sessions export async function removeAllSessions(): Promise { await channels.removeAllSessions(); @@ -585,13 +555,6 @@ export async function saveConversation(data: ConversationType): Promise { await channels.saveConversation(cleaned); } -export async function saveConversations( - data: Array -): Promise { - const cleaned = data.map(d => _.omit(d, 'isOnline')); - await channels.saveConversations(cleaned); -} - export async function getConversationById( id: string ): Promise { @@ -670,17 +633,6 @@ export async function getPublicServerTokenByServerUrl( const token = await channels.getPublicServerTokenByServerUrl(serverUrl); return token; } - -export async function getPublicConversationsByServer( - server: string -): Promise { - const conversations = await channels.getPublicConversationsByServer(server); - - const collection = new ConversationCollection(); - collection.add(conversations); - return collection; -} - export async function getAllGroupsInvolvingId( id: string ): Promise { @@ -739,13 +691,6 @@ export async function updateLastHash(data: any): Promise { await channels.updateLastHash(_cleanData(data)); } -export async function saveSeenMessageHash(data: { - expiresAt: number; - hash: string; -}): Promise { - await channels.saveSeenMessageHash(_cleanData(data)); -} - export async function saveMessage( data: MessageModel, options?: { forceSave: boolean } @@ -804,11 +749,6 @@ export async function getAllMessages(): Promise { return new MessageCollection(messages); } -export async function getAllUnsentMessages(): Promise { - const messages = await channels.getAllUnsentMessages(); - return new MessageCollection(messages); -} - export async function getAllMessageIds(): Promise> { const ids = await channels.getAllMessageIds(); return ids; @@ -953,17 +893,6 @@ export async function saveUnprocessed( return id; } -export async function saveUnprocesseds( - arrayOfUnprocessed: Array, - options?: { - forceSave: boolean; - } -): Promise { - await channels.saveUnprocesseds(_cleanData(arrayOfUnprocessed), { - forceSave: options?.forceSave || false, - }); -} - export async function updateUnprocessedAttempts( id: string, attempts: number From 3ee0ccfac8ba08ebdb4057afd83b79067e71952c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 13:01:24 +1100 Subject: [PATCH 038/109] remove the forceSave logic for a message, always insert or replace --- app/sql.js | 60 +++++-------------- preload.js | 1 - test/backup_test.js | 1 - ts/data/data.ts | 14 +---- ts/models/conversation.ts | 1 - ts/models/message.ts | 19 +++--- ts/models/messageType.ts | 16 ++++- ts/receiver/cache.ts | 5 -- ts/receiver/receiver.ts | 4 +- .../outgoing/content/data/ChatMessage.ts | 5 +- ts/window.d.ts | 1 - 11 files changed, 50 insertions(+), 77 deletions(-) diff --git a/app/sql.js b/app/sql.js index 7923c481c6..168b8a4470 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1716,7 +1716,7 @@ async function getMessageCount() { return row['count(*)']; } -async function saveMessage(data, { forceSave } = {}) { +async function saveMessage(data) { const { body, conversationId, @@ -1742,6 +1742,14 @@ async function saveMessage(data, { forceSave } = {}) { expirationStartTimestamp, } = data; + if (!id) { + throw new Error('id is required'); + } + + if (!conversationId) { + throw new Error('conversationId is required'); + } + const payload = { $id: id, $json: objectToJSON(data), @@ -1766,46 +1774,10 @@ async function saveMessage(data, { forceSave } = {}) { $unread: unread, }; - if (id && !forceSave) { - await db.run( - `UPDATE messages SET - json = $json, - serverId = $serverId, - serverTimestamp = $serverTimestamp, - body = $body, - conversationId = $conversationId, - expirationStartTimestamp = $expirationStartTimestamp, - expires_at = $expires_at, - expireTimer = $expireTimer, - hasAttachments = $hasAttachments, - hasFileAttachments = $hasFileAttachments, - hasVisualMediaAttachments = $hasVisualMediaAttachments, - id = $id, - received_at = $received_at, - schemaVersion = $schemaVersion, - sent = $sent, - sent_at = $sent_at, - source = $source, - sourceDevice = $sourceDevice, - type = $type, - unread = $unread - WHERE id = $id;`, - payload - ); - - return id; - } - - const toCreate = { - ...data, - id: id || uuidv4(), - }; - await db.run( - `INSERT INTO messages ( + `INSERT OR REPLACE INTO ${MESSAGES_TABLE} ( id, json, - serverId, serverTimestamp, body, @@ -1827,7 +1799,6 @@ async function saveMessage(data, { forceSave } = {}) { ) values ( $id, $json, - $serverId, $serverTimestamp, $body, @@ -1849,12 +1820,11 @@ async function saveMessage(data, { forceSave } = {}) { );`, { ...payload, - $id: toCreate.id, - $json: objectToJSON(toCreate), + $json: objectToJSON(data), } ); - return toCreate.id; + return id; } async function saveSeenMessageHashes(arrayOfHashes) { @@ -1926,13 +1896,13 @@ async function cleanSeenMessages() { }); } -async function saveMessages(arrayOfMessages, { forceSave } = {}) { +async function saveMessages(arrayOfMessages) { let promise; db.serialize(() => { promise = Promise.all([ db.run('BEGIN TRANSACTION;'), - ...map(arrayOfMessages, message => saveMessage(message, { forceSave })), + ...map(arrayOfMessages, message => saveMessage(message)), db.run('COMMIT TRANSACTION;'), ]); }); @@ -2175,7 +2145,7 @@ async function getNextExpiringMessage() { async function saveUnprocessed(data, { forceSave } = {}) { const { id, timestamp, version, attempts, envelope, senderIdentity } = data; if (!id) { - throw new Error('saveUnprocessed: id was falsey'); + throw new Error(`saveUnprocessed: id was falsey: ${id}`); } if (forceSave) { diff --git a/preload.js b/preload.js index 2abc6c887f..d2842f0209 100644 --- a/preload.js +++ b/preload.js @@ -386,7 +386,6 @@ window.autoOrientImage = autoOrientImage; window.loadImage = require('blueimp-load-image'); window.dataURLToBlobSync = require('blueimp-canvas-to-blob'); window.filesize = require('filesize'); -window.getGuid = require('uuid/v4'); window.profileImages = require('./app/profile_images'); window.React = require('react'); diff --git a/test/backup_test.js b/test/backup_test.js index 38f1bc2e77..8d15a14cc4 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -489,7 +489,6 @@ describe('Backup', () => { const message = await upgradeMessageSchema(messageWithAttachments); await window.Signal.Data.saveMessage(message, { Message: window.models.Message.MessageModel, - forceSave: true, }); const conversation = { diff --git a/ts/data/data.ts b/ts/data/data.ts index 73804a6a39..a1330bd8e8 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -65,8 +65,6 @@ export type ServerToken = { }; const channelsToMake = { - _cleanData, - shutdown, close, removeDB, @@ -205,7 +203,7 @@ export function init() { // When IPC arguments are prepared for the cross-process send, they are JSON.stringified. // We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). -export async function _cleanData(data: any): Promise { +function _cleanData(data: any): any { const keys = Object.keys(data); for (let index = 0, max = keys.length; index < max; index += 1) { const key = keys[index]; @@ -691,13 +689,8 @@ export async function updateLastHash(data: any): Promise { await channels.updateLastHash(_cleanData(data)); } -export async function saveMessage( - data: MessageModel, - options?: { forceSave: boolean } -): Promise { - const id = await channels.saveMessage(_cleanData(data), { - forceSave: options?.forceSave, - }); +export async function saveMessage(data: MessageModel): Promise { + const id = await channels.saveMessage(_cleanData(data)); window.Whisper.ExpiringMessagesListener.update(); return id; } @@ -797,7 +790,6 @@ export async function getMessagesByConversation( receivedAt, type, }); - return new MessageCollection(messages); } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 36c06bcb03..1d6d4e3233 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -936,7 +936,6 @@ export class ConversationModel extends Backbone.Model { destination: this.id, recipients: isOutgoing ? this.getRecipients() : undefined, }; - const message = await this.addSingleMessage(messageAttributes); // tell the UI this conversation was updated diff --git a/ts/models/message.ts b/ts/models/message.ts index bca525922b..dc5461c5b8 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -44,6 +44,7 @@ export class MessageModel extends Backbone.Model { // this.on('expired', this.onExpired); void this.setToExpire(); autoBind(this); + this.markRead = this.markRead.bind(this); // Keep props ready const generateProps = (triggerEvent = true) => { @@ -207,14 +208,16 @@ export class MessageModel extends Backbone.Model { return window.i18n('mediaMessage'); } if (this.isExpirationTimerUpdate()) { - const { expireTimer } = this.get('expirationTimerUpdate'); - if (!expireTimer) { + const expireTimerUpdate = this.get('expirationTimerUpdate'); + if (!expireTimerUpdate || !expireTimerUpdate.expireTimer) { return window.i18n('disappearingMessagesDisabled'); } return window.i18n( 'timerSetTo', - window.Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0) + window.Whisper.ExpirationTimerOptions.getAbbreviated( + expireTimerUpdate.expireTimer || 0 + ) ); } const contacts = this.get('contact'); @@ -1234,11 +1237,11 @@ export class MessageModel extends Backbone.Model { await this.commit(); } - public async commit(forceSave = false) { - // TODO investigate the meaning of the forceSave - const id = await saveMessage(this.attributes, { - forceSave, - }); + public async commit() { + if (!this.attributes.id) { + throw new Error('A message always needs an id'); + } + const id = await saveMessage(this.attributes); this.trigger('change'); return id; } diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index a54230c60a..e19e937ddd 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -1,5 +1,6 @@ import { DefaultTheme } from 'styled-components'; import _ from 'underscore'; +import uuidv4 from 'uuid'; import { QuotedAttachmentType } from '../components/conversation/Quote'; import { AttachmentType } from '../types/Attachment'; import { Contact } from '../types/Contact'; @@ -43,7 +44,12 @@ export interface MessageAttributes { hasFileAttachments: boolean; hasVisualMediaAttachments: boolean; schemaVersion: number; - expirationTimerUpdate?: any; + expirationTimerUpdate?: { + expireTimer: number; + source: string; + fromSync?: boolean; + fromGroupUpdate?: boolean; + }; unread: boolean; group?: any; timestamp?: number; @@ -91,7 +97,12 @@ export interface MessageAttributesOptionals { hasFileAttachments?: boolean; hasVisualMediaAttachments?: boolean; schemaVersion?: number; - expirationTimerUpdate?: any; + expirationTimerUpdate?: { + expireTimer: number; + source: string; + fromSync?: boolean; + fromGroupUpdate?: boolean; + }; unread?: boolean; group?: any; timestamp?: number; @@ -120,6 +131,7 @@ export const fillMessageAttributesWithDefaults = ( //FIXME audric to do put the default return _.defaults(optAttributes, { expireTimer: 0, // disabled + id: uuidv4(), }); }; diff --git a/ts/receiver/cache.ts b/ts/receiver/cache.ts index 6c94fe805a..9c63c619d3 100644 --- a/ts/receiver/cache.ts +++ b/ts/receiver/cache.ts @@ -37,13 +37,10 @@ export async function addToCache( if (envelope.senderIdentity) { data.senderIdentity = envelope.senderIdentity; } - return saveUnprocessed(data, { forceSave: true }); } async function fetchAllFromCache(): Promise> { - const { textsecure } = window; - const count = await getUnprocessedCount(); if (count > 1500) { @@ -63,7 +60,6 @@ export async function getAllFromCache() { const items = await fetchAllFromCache(); window.log.info('getAllFromCache loaded', items.length, 'saved envelopes'); - const { textsecure } = window; return Promise.all( _.map(items, async (item: any) => { @@ -104,7 +100,6 @@ export async function getAllFromCacheForSource(source: string) { itemsFromSource.length, 'saved envelopes' ); - const { textsecure } = window; return Promise.all( _.map(items, async (item: any) => { diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 982ca0f60d..65e553c488 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -2,6 +2,7 @@ import { EnvelopePlus } from './types'; export { downloadAttachment } from './attachments'; +import uuidv4 from 'uuid'; import { addToCache, @@ -27,6 +28,7 @@ import { getEnvelopeId } from './common'; import { StringUtils, UserUtils } from '../session/utils'; import { SignalService } from '../protobuf'; import { ConversationController } from '../session/conversations'; +import { removeUnprocessed } from '../data/data'; // TODO: check if some of these exports no longer needed @@ -131,7 +133,7 @@ async function handleRequestDetail( envelope.senderIdentity = senderIdentity; } - envelope.id = envelope.serverGuid || window.getGuid(); + envelope.id = envelope.serverGuid || uuidv4(); envelope.serverTimestamp = envelope.serverTimestamp ? envelope.serverTimestamp.toNumber() : null; diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 6e4677bf1d..619272ce73 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -92,7 +92,10 @@ export class ChatMessage extends DataMessage { profileKey: dataMessage.profileKey, }; - if ((dataMessage as any)?.$type?.name !== 'DataMessage' && !(dataMessage instanceof DataMessage)) { + if ( + (dataMessage as any)?.$type?.name !== 'DataMessage' && + !(dataMessage instanceof DataMessage) + ) { throw new Error( 'Tried to build a sync message from something else than a DataMessage' ); diff --git a/ts/window.d.ts b/ts/window.d.ts index 673de4fdec..5cd849f316 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -90,7 +90,6 @@ declare global { versionInfo: any; getStoragePubKey: (key: string) => string; getConversations: () => ConversationCollection; - getGuid: any; SwarmPolling: SwarmPolling; SnodePool: { getSnodesFor: (string) => any; From 25e03eba357e1d6d4f38caf1ffff9a2f75de28e1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 15:13:43 +1100 Subject: [PATCH 039/109] fix lint --- app/sql.js | 1 - ts/models/messageType.ts | 2 +- ts/receiver/receiver.ts | 2 +- ts/session/sending/MessageSentHandler.ts | 63 ++++++++++--------- .../session/unit/sending/MessageQueue_test.ts | 24 ++++--- 5 files changed, 48 insertions(+), 44 deletions(-) diff --git a/app/sql.js b/app/sql.js index 168b8a4470..ccbc57378c 100644 --- a/app/sql.js +++ b/app/sql.js @@ -7,7 +7,6 @@ const { redactAll } = require('../js/modules/privacy'); const { remove: removeUserConfig } = require('./user_config'); const pify = require('pify'); -const uuidv4 = require('uuid/v4'); const { map, isString, diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index e19e937ddd..24b4580844 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -1,6 +1,6 @@ import { DefaultTheme } from 'styled-components'; import _ from 'underscore'; -import uuidv4 from 'uuid'; +import { v4 as uuidv4 } from 'uuid'; import { QuotedAttachmentType } from '../components/conversation/Quote'; import { AttachmentType } from '../types/Attachment'; import { Contact } from '../types/Contact'; diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 65e553c488..8a6ff7b768 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -2,7 +2,7 @@ import { EnvelopePlus } from './types'; export { downloadAttachment } from './attachments'; -import uuidv4 from 'uuid'; +import { v4 as uuidv4 } from 'uuid'; import { addToCache, diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index dd30f5777a..4c7aeb6e25 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -3,38 +3,8 @@ import { MessageController } from '../messages'; import { OpenGroupMessage } from '../messages/outgoing'; import { RawMessage } from '../types'; +// tslint:disable-next-line no-unnecessary-class export class MessageSentHandler { - /** - * This function tries to find a message by messageId by first looking on the MessageController. - * The MessageController holds all messages being in memory. - * Those are the messages sent recently, recieved recently, or the one shown to the user. - * - * If the app restarted, it's very likely those messages won't be on the memory anymore. - * In this case, this function will look for it in the database and return it. - * If the message is found on the db, it will also register it to the MessageController so our subsequent calls are quicker. - */ - private static async fetchHandleMessageSentData( - m: RawMessage | OpenGroupMessage - ) { - // if a message was sent and this message was created after the last app restart, - // this message is still in memory in the MessageController - const msg = MessageController.getInstance().get(m.identifier); - - if (!msg || !msg.message) { - // otherwise, look for it in the database - // nobody is listening to this freshly fetched message .trigger calls - const dbMessage = await getMessageById(m.identifier); - - if (!dbMessage) { - return null; - } - MessageController.getInstance().register(m.identifier, dbMessage); - return dbMessage; - } - - return msg.message; - } - public static async handlePublicMessageSentSuccess( sentMessage: OpenGroupMessage, result: { serverId: number; serverTimestamp: number } @@ -90,4 +60,35 @@ export class MessageSentHandler { await fetchedMessage.handleMessageSentFailure(sentMessage, error); } + + /** + * This function tries to find a message by messageId by first looking on the MessageController. + * The MessageController holds all messages being in memory. + * Those are the messages sent recently, recieved recently, or the one shown to the user. + * + * If the app restarted, it's very likely those messages won't be on the memory anymore. + * In this case, this function will look for it in the database and return it. + * If the message is found on the db, it will also register it to the MessageController so our subsequent calls are quicker. + */ + private static async fetchHandleMessageSentData( + m: RawMessage | OpenGroupMessage + ) { + // if a message was sent and this message was created after the last app restart, + // this message is still in memory in the MessageController + const msg = MessageController.getInstance().get(m.identifier); + + if (!msg || !msg.message) { + // otherwise, look for it in the database + // nobody is listening to this freshly fetched message .trigger calls + const dbMessage = await getMessageById(m.identifier); + + if (!dbMessage) { + return null; + } + MessageController.getInstance().register(m.identifier, dbMessage); + return dbMessage; + } + + return msg.message; + } } diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index 9ad6102ff1..695616f77b 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -80,14 +80,18 @@ describe('MessageQueue', () => { done(); }); - pendingMessageCache.add( - device, - TestUtils.generateChatMessage(), - waitForMessageSentEvent as any - ); - - messageQueueStub.processPending(device); - expect(waitForMessageSentEvent).to.be.fulfilled; + void pendingMessageCache + .add( + device, + TestUtils.generateChatMessage(), + waitForMessageSentEvent as any + ) + .then(async () => { + return messageQueueStub.processPending(device); + }) + .then(() => { + expect(waitForMessageSentEvent).to.be.fulfilled; + }); }); it('should remove message from cache', async () => { @@ -123,7 +127,7 @@ describe('MessageQueue', () => { done(); }); - pendingMessageCache + void pendingMessageCache .add(device, message, waitForMessageSentEvent as any) .then(() => messageQueueStub.processPending(device)) .then(() => { @@ -144,7 +148,7 @@ describe('MessageQueue', () => { done(); }); - pendingMessageCache + void pendingMessageCache .add(device, message, waitForMessageSentEvent as any) .then(() => messageQueueStub.processPending(device)) .then(() => { From 8a800cf58c789ed0fd686e34c4d9e71623934f57 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 16:43:50 +1100 Subject: [PATCH 040/109] move the logic of handling when a message is sent to MessageSentHandler --- libtextsecure/errors.js | 13 --- libtextsecure/index.d.ts | 1 - ts/models/message.ts | 142 +---------------------- ts/session/sending/MessageQueue.ts | 3 +- ts/session/sending/MessageSentHandler.ts | 127 +++++++++++++++++++- 5 files changed, 126 insertions(+), 160 deletions(-) diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 0d5810f706..aa17a97105 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -47,18 +47,6 @@ } inherit(ReplayableError, IncomingIdentityKeyError); - function OutgoingIdentityKeyError(number, message, timestamp, identityKey) { - // eslint-disable-next-line prefer-destructuring - this.number = number.split('.')[0]; - this.identityKey = identityKey; - - ReplayableError.call(this, { - name: 'OutgoingIdentityKeyError', - message: `The identity of ${this.number} has changed.`, - }); - } - inherit(ReplayableError, OutgoingIdentityKeyError); - function SendMessageNetworkError(number, jsonData, httpError) { this.number = number; this.code = httpError.code; @@ -207,7 +195,6 @@ window.textsecure.SendMessageNetworkError = SendMessageNetworkError; window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; - window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError; window.textsecure.ReplayableError = ReplayableError; window.textsecure.MessageError = MessageError; window.textsecure.EmptySwarmError = EmptySwarmError; diff --git a/libtextsecure/index.d.ts b/libtextsecure/index.d.ts index ec8452f1f1..894521dc2c 100644 --- a/libtextsecure/index.d.ts +++ b/libtextsecure/index.d.ts @@ -6,7 +6,6 @@ export interface LibTextsecure { storage: any; SendMessageNetworkError: any; IncomingIdentityKeyError: any; - OutgoingIdentityKeyError: any; ReplayableError: any; MessageError: any; EmptySwarmError: any; diff --git a/ts/models/message.ts b/ts/models/message.ts index dc5461c5b8..cf34456e61 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -12,7 +12,7 @@ import { OpenGroupMessage, } from '../../ts/session/messages/outgoing'; import { ClosedGroupChatMessage } from '../../ts/session/messages/outgoing/content/data/group/ClosedGroupChatMessage'; -import { EncryptionType, PubKey } from '../../ts/session/types'; +import { EncryptionType, PubKey, RawMessage } from '../../ts/session/types'; import { ToastUtils, UserUtils } from '../../ts/session/utils'; import { fillMessageAttributesWithDefaults, @@ -665,7 +665,6 @@ export class MessageModel extends Backbone.Model { public async getPropsForMessageDetail() { const newIdentity = window.i18n('newIdentity'); - const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; // We include numbers we didn't successfully send to so we can display errors. // Older messages don't have the recipients included on the message, so we fall @@ -681,11 +680,6 @@ export class MessageModel extends Backbone.Model { // This will make the error message for outgoing key errors a bit nicer const allErrors = (this.get('errors') || []).map((error: any) => { - if (error.name === OUTGOING_KEY_ERROR) { - // eslint-disable-next-line no-param-reassign - error.message = newIdentity; - } - return error; }); @@ -696,9 +690,7 @@ export class MessageModel extends Backbone.Model { const finalContacts = await Promise.all( (phoneNumbers || []).map(async id => { const errorsForContact = errorsGroupedById[id]; - const isOutgoingKeyError = Boolean( - _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) - ); + const isOutgoingKeyError = false; const contact = this.findAndFormatContact(id); return { @@ -940,140 +932,12 @@ export class MessageModel extends Backbone.Model { this.get('errors'), e => e.number === number && - (e.name === 'MessageError' || - e.name === 'SendMessageNetworkError' || - e.name === 'OutgoingIdentityKeyError') + (e.name === 'MessageError' || e.name === 'SendMessageNetworkError') ); this.set({ errors: errors[1] }); return errors[0][0]; } - /** - * This function is called by inbox_view.js when a message was successfully sent for one device. - * So it might be called several times for the same message - */ - public async handleMessageSentSuccess( - sentMessage: any, - wrappedEnvelope: any - ) { - let sentTo = this.get('sent_to') || []; - - let isOurDevice = false; - if (sentMessage.device) { - isOurDevice = UserUtils.isUsFromCache(sentMessage.device); - } - // FIXME this is not correct and will cause issues with syncing - // At this point the only way to check for medium - // group is by comparing the encryption type - const isClosedGroupMessage = - sentMessage.encryption === EncryptionType.ClosedGroup; - - const isOpenGroupMessage = - !!sentMessage.group && sentMessage.group instanceof Types.OpenGroup; - - // We trigger a sync message only when the message is not to one of our devices, AND - // the message is not for an open group (there is no sync for opengroups, each device pulls all messages), AND - // if we did not sync or trigger a sync message for this specific message already - const shouldTriggerSyncMessage = - !isOurDevice && - !isClosedGroupMessage && - !this.get('synced') && - !this.get('sentSync'); - - // A message is synced if we triggered a sync message (sentSync) - // and the current message was sent to our device (so a sync message) - const shouldMarkMessageAsSynced = isOurDevice && this.get('sentSync'); - - const isSessionOrClosedMessage = !isOpenGroupMessage; - - if (isSessionOrClosedMessage) { - const contentDecoded = SignalService.Content.decode( - sentMessage.plainTextBuffer - ); - const { dataMessage } = contentDecoded; - - /** - * We should hit the notify endpoint for push notification only if: - * • It's a one-to-one chat or a closed group - * • The message has either text or attachments - */ - const hasBodyOrAttachments = Boolean( - dataMessage && - (dataMessage.body || - (dataMessage.attachments && dataMessage.attachments.length)) - ); - const shouldNotifyPushServer = - hasBodyOrAttachments && isSessionOrClosedMessage; - - if (shouldNotifyPushServer) { - // notify the push notification server if needed - if (!wrappedEnvelope) { - window.log.warn('Should send PN notify but no wrapped envelope set.'); - } else { - if (!window.LokiPushNotificationServer) { - window.LokiPushNotificationServer = new window.LokiPushNotificationServerApi(); - } - - window.LokiPushNotificationServer.notify( - wrappedEnvelope, - sentMessage.device - ); - } - } - - // Handle the sync logic here - if (shouldTriggerSyncMessage) { - if (dataMessage) { - await this.sendSyncMessage( - dataMessage as SignalService.DataMessage, - sentMessage.timestamp - ); - } - } else if (shouldMarkMessageAsSynced) { - this.set({ synced: true }); - } - - sentTo = _.union(sentTo, [sentMessage.device]); - } - - this.set({ - sent_to: sentTo, - sent: true, - expirationStartTimestamp: Date.now(), - sent_at: sentMessage.timestamp, - }); - - await this.commit(); - - this.getConversation()?.updateLastMessage(); - } - - public async handleMessageSentFailure(sentMessage: any, error: any) { - if (error instanceof Error) { - await this.saveErrors(error); - if (error.name === 'OutgoingIdentityKeyError') { - const c = ConversationController.getInstance().get(sentMessage.device); - await c.getProfiles(); - } - } - let isOurDevice = false; - if (sentMessage.device) { - isOurDevice = UserUtils.isUsFromCache(sentMessage.device); - } - - const expirationStartTimestamp = Date.now(); - if (isOurDevice && !this.get('sync')) { - this.set({ sentSync: false }); - } - this.set({ - sent: true, - expirationStartTimestamp, - }); - await this.commit(); - - this.getConversation()?.updateLastMessage(); - } - public getConversation(): ConversationModel | undefined { // This needs to be an unsafe call, because this method is called during // initial module setup. We may be in the middle of the initial fetch to diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index f742678d83..0d49140836 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -79,7 +79,6 @@ export class MessageQueue { if (result.serverId < 0) { void MessageSentHandler.handleMessageSentFailure(message, error); } else { - void MessageSentHandler.handleMessageSentSuccess(message); void MessageSentHandler.handlePublicMessageSentSuccess(message, result); } } catch (e) { @@ -145,7 +144,7 @@ export class MessageQueue { const job = async () => { try { const wrappedEnvelope = await MessageSender.send(message); - void MessageSentHandler.handleMessageSentSuccess( + await MessageSentHandler.handleMessageSentSuccess( message, wrappedEnvelope ); diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 4c7aeb6e25..9e3b8ae8a4 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -1,7 +1,11 @@ +import _ from 'lodash'; import { getMessageById } from '../../data/data'; +import { SignalService } from '../../protobuf'; +import { ConversationController } from '../conversations'; import { MessageController } from '../messages'; import { OpenGroupMessage } from '../messages/outgoing'; -import { RawMessage } from '../types'; +import { EncryptionType, RawMessage } from '../types'; +import { UserUtils } from '../utils'; // tslint:disable-next-line no-unnecessary-class export class MessageSentHandler { @@ -25,15 +29,21 @@ export class MessageSentHandler { serverTimestamp, serverId, isPublic: true, + sent: true, + sent_at: sentMessage.timestamp, + sync: true, + synced: true, + sentSync: true, }); await foundMessage.commit(); + foundMessage.getConversation()?.updateLastMessage(); } catch (e) { window.log.error('Error setting public on message'); } } public static async handleMessageSentSuccess( - sentMessage: RawMessage | OpenGroupMessage, + sentMessage: RawMessage, wrappedEnvelope?: Uint8Array ) { // The wrappedEnvelope will be set only if the message is not one of OpenGroupMessage type. @@ -44,7 +54,88 @@ export class MessageSentHandler { return; } - void fetchedMessage.handleMessageSentSuccess(sentMessage, wrappedEnvelope); + let sentTo = fetchedMessage.get('sent_to') || []; + + let isOurDevice = false; + if (sentMessage.device) { + isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + } + // FIXME this is not correct and will cause issues with syncing + // At this point the only way to check for medium + // group is by comparing the encryption type + const isClosedGroupMessage = + sentMessage.encryption === EncryptionType.ClosedGroup; + + // We trigger a sync message only when the message is not to one of our devices, AND + // the message is not for an open group (there is no sync for opengroups, each device pulls all messages), AND + // if we did not sync or trigger a sync message for this specific message already + const shouldTriggerSyncMessage = + !isOurDevice && + !isClosedGroupMessage && + !fetchedMessage.get('synced') && + !fetchedMessage.get('sentSync'); + + // A message is synced if we triggered a sync message (sentSync) + // and the current message was sent to our device (so a sync message) + const shouldMarkMessageAsSynced = + isOurDevice && fetchedMessage.get('sentSync'); + + const contentDecoded = SignalService.Content.decode( + sentMessage.plainTextBuffer + ); + const { dataMessage } = contentDecoded; + + /** + * We should hit the notify endpoint for push notification only if: + * • It's a one-to-one chat or a closed group + * • The message has either text or attachments + */ + const hasBodyOrAttachments = Boolean( + dataMessage && + (dataMessage.body || + (dataMessage.attachments && dataMessage.attachments.length)) + ); + const shouldNotifyPushServer = hasBodyOrAttachments && !isOurDevice; + + if (shouldNotifyPushServer) { + // notify the push notification server if needed + if (!wrappedEnvelope) { + window.log.warn('Should send PN notify but no wrapped envelope set.'); + } else { + if (!window.LokiPushNotificationServer) { + window.LokiPushNotificationServer = new window.LokiPushNotificationServerApi(); + } + + window.LokiPushNotificationServer.notify( + wrappedEnvelope, + sentMessage.device + ); + } + } + + // Handle the sync logic here + if (shouldTriggerSyncMessage) { + if (dataMessage) { + await fetchedMessage.sendSyncMessage( + dataMessage as SignalService.DataMessage, + sentMessage.timestamp + ); + } + } else if (shouldMarkMessageAsSynced) { + fetchedMessage.set({ synced: true }); + } + + sentTo = _.union(sentTo, [sentMessage.device]); + + fetchedMessage.set({ + sent_to: sentTo, + sent: true, + expirationStartTimestamp: Date.now(), + sent_at: sentMessage.timestamp, + }); + + await fetchedMessage.commit(); + fetchedMessage.getConversation()?.updateLastMessage(); } public static async handleMessageSentFailure( @@ -58,7 +149,32 @@ export class MessageSentHandler { return; } - await fetchedMessage.handleMessageSentFailure(sentMessage, error); + if (error instanceof Error) { + await fetchedMessage.saveErrors(error); + } + + if (!(sentMessage instanceof OpenGroupMessage)) { + const isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + // if this message was for ourself, and it was not already synced, + // it means that we failed to sync it. + // so just remove the flag saying that we are currently sending the sync message + if (isOurDevice && !fetchedMessage.get('sync')) { + fetchedMessage.set({ sentSync: false }); + } + + fetchedMessage.set({ + expirationStartTimestamp: Date.now(), + }); + } + + // always mark the message as sent. + // the fact that we have errors on the sent is based on the saveErrors() + fetchedMessage.set({ + sent: true, + }); + + await fetchedMessage.commit(); + await fetchedMessage.getConversation()?.updateLastMessage(); } /** @@ -73,13 +189,14 @@ export class MessageSentHandler { private static async fetchHandleMessageSentData( m: RawMessage | OpenGroupMessage ) { - // if a message was sent and this message was created after the last app restart, + // if a message was sent and this message was sent after the last app restart, // this message is still in memory in the MessageController const msg = MessageController.getInstance().get(m.identifier); if (!msg || !msg.message) { // otherwise, look for it in the database // nobody is listening to this freshly fetched message .trigger calls + // so we can just update the fields on the database const dbMessage = await getMessageById(m.identifier); if (!dbMessage) { From bb3641b39aa642ab7c5e4420ec285abed6b408af Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 17:46:13 +1100 Subject: [PATCH 041/109] remove the forceSave option for unprocessed message too --- app/sql.js | 54 +++++++++++--------------------------- js/modules/backup.js | 4 +-- ts/data/data.ts | 16 +++-------- ts/models/messageType.ts | 2 +- ts/receiver/cache.ts | 3 ++- ts/receiver/dataMessage.ts | 4 +-- ts/receiver/receiver.ts | 1 - 7 files changed, 24 insertions(+), 60 deletions(-) diff --git a/app/sql.js b/app/sql.js index ccbc57378c..670247469f 100644 --- a/app/sql.js +++ b/app/sql.js @@ -2141,50 +2141,28 @@ async function getNextExpiringMessage() { } /* Unproccessed a received messages not yet processed */ -async function saveUnprocessed(data, { forceSave } = {}) { +async function saveUnprocessed(data) { const { id, timestamp, version, attempts, envelope, senderIdentity } = data; if (!id) { throw new Error(`saveUnprocessed: id was falsey: ${id}`); } - if (forceSave) { - await db.run( - `INSERT INTO unprocessed ( - id, - timestamp, - version, - attempts, - envelope, - senderIdentity - ) values ( - $id, - $timestamp, - $version, - $attempts, - $envelope, - $senderIdentity - );`, - { - $id: id, - $timestamp: timestamp, - $version: version, - $attempts: attempts, - $envelope: envelope, - $senderIdentity: senderIdentity, - } - ); - - return id; - } - await db.run( - `UPDATE unprocessed SET - timestamp = $timestamp, - version = $version, - attempts = $attempts, - envelope = $envelope, - senderIdentity = $senderIdentity - WHERE id = $id;`, + `INSERT OR REPLACE INTO unprocessed ( + id, + timestamp, + version, + attempts, + envelope, + senderIdentity + ) values ( + $id, + $timestamp, + $version, + $attempts, + $envelope, + $senderIdentity + );`, { $id: id, $timestamp: timestamp, diff --git a/js/modules/backup.js b/js/modules/backup.js index cf971f3729..66da841fd3 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -982,9 +982,7 @@ async function saveAllMessages(rawMessages) { const { conversationId } = messages[0]; - await window.Signal.Data.saveMessages(messages, { - forceSave: true, - }); + await window.Signal.Data.saveMessages(messages); window.log.info( 'Saved', diff --git a/ts/data/data.ts b/ts/data/data.ts index a1330bd8e8..1b918e875c 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -699,9 +699,7 @@ export async function saveMessages( arrayOfMessages: any, options?: { forceSave: boolean } ): Promise { - await channels.saveMessages(_cleanData(arrayOfMessages), { - forceSave: options?.forceSave, - }); + await channels.saveMessages(_cleanData(arrayOfMessages)); } export async function removeMessage(id: string): Promise { @@ -872,16 +870,8 @@ export async function getUnprocessedById(id: string): Promise { return channels.getUnprocessedById(id); } -export async function saveUnprocessed( - data: any, - options?: { - forceSave: boolean; - } -): Promise { - const id = await channels.saveUnprocessed( - _cleanData(data), - options?.forceSave || false - ); +export async function saveUnprocessed(data: any): Promise { + const id = await channels.saveUnprocessed(_cleanData(data)); return id; } diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 24b4580844..32e5aae697 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -128,7 +128,7 @@ export interface MessageAttributesOptionals { export const fillMessageAttributesWithDefaults = ( optAttributes: MessageAttributesOptionals ): MessageAttributes => { - //FIXME audric to do put the default + //FIXME to do put the default return _.defaults(optAttributes, { expireTimer: 0, // disabled id: uuidv4(), diff --git a/ts/receiver/cache.ts b/ts/receiver/cache.ts index 9c63c619d3..7e055a1968 100644 --- a/ts/receiver/cache.ts +++ b/ts/receiver/cache.ts @@ -24,6 +24,7 @@ export async function addToCache( plaintext: ArrayBuffer ) { const { id } = envelope; + window.log.info(`adding to cache envelope: ${id}`); const encodedEnvelope = StringUtils.decode(plaintext, 'base64'); const data: any = { @@ -37,7 +38,7 @@ export async function addToCache( if (envelope.senderIdentity) { data.senderIdentity = envelope.senderIdentity; } - return saveUnprocessed(data, { forceSave: true }); + return saveUnprocessed(data); } async function fetchAllFromCache(): Promise> { diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index f4091dbef7..28824cc3b9 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -456,7 +456,6 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel { serverTimestamp, } = data; - const type = 'incoming'; const messageGroupId = message?.group?.id; let groupId = messageGroupId && messageGroupId.length > 0 ? messageGroupId : null; @@ -473,7 +472,7 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel { serverTimestamp, received_at: receivedAt || Date.now(), conversationId: groupId ?? source, - type, + type: 'incoming', direction: 'incoming', // + unread: 1, // + isPublic, // + @@ -484,7 +483,6 @@ export function initIncomingMessage(data: MessageCreationData): MessageModel { function createSentMessage(data: MessageCreationData): MessageModel { const now = Date.now(); - let sentTo = []; const { timestamp, diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 8a6ff7b768..7507ccd052 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -109,7 +109,6 @@ async function handleRequestDetail( options: ReqOptions, lastPromise: Promise ): Promise { - const { textsecure } = window; const envelope: any = SignalService.Envelope.decode(plaintext); // After this point, decoding errors are not the server's From 66a6190f2ba5885443c52f42e617b4050b4a8143 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 17:47:13 +1100 Subject: [PATCH 042/109] remove unused replyable error types --- libtextsecure/errors.js | 48 ---------------------------------------- libtextsecure/index.d.ts | 4 ---- ts/models/message.ts | 4 +--- 3 files changed, 1 insertion(+), 55 deletions(-) diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index aa17a97105..b09aad4566 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -35,18 +35,6 @@ } inherit(Error, ReplayableError); - function IncomingIdentityKeyError(number, message, key) { - // eslint-disable-next-line prefer-destructuring - this.number = number.split('.')[0]; - this.identityKey = key; - - ReplayableError.call(this, { - name: 'IncomingIdentityKeyError', - message: `The identity of ${this.number} has changed.`, - }); - } - inherit(ReplayableError, IncomingIdentityKeyError); - function SendMessageNetworkError(number, jsonData, httpError) { this.number = number; this.code = httpError.code; @@ -60,18 +48,6 @@ } inherit(ReplayableError, SendMessageNetworkError); - function MessageError(message, httpError) { - this.code = httpError.code; - - ReplayableError.call(this, { - name: 'MessageError', - message: httpError.message, - }); - - appendStack(this, httpError); - } - inherit(ReplayableError, MessageError); - function EmptySwarmError(number, message) { // eslint-disable-next-line prefer-destructuring this.number = number.split('.')[0]; @@ -83,16 +59,6 @@ } inherit(ReplayableError, EmptySwarmError); - function DNSResolutionError(message) { - // eslint-disable-next-line prefer-destructuring - - ReplayableError.call(this, { - name: 'DNSResolutionError', - message: `Error resolving url: ${message}`, - }); - } - inherit(ReplayableError, DNSResolutionError); - function NotFoundError(message, error) { this.name = 'NotFoundError'; this.message = message; @@ -161,16 +127,6 @@ } } - function PublicTokenError(message) { - this.name = 'PublicTokenError'; - - ReplayableError.call(this, { - name: 'PublicTokenError', - message, - }); - } - inherit(ReplayableError, PublicTokenError); - function TimestampError(message) { this.name = 'TimeStampError'; @@ -194,17 +150,13 @@ } window.textsecure.SendMessageNetworkError = SendMessageNetworkError; - window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; window.textsecure.ReplayableError = ReplayableError; - window.textsecure.MessageError = MessageError; window.textsecure.EmptySwarmError = EmptySwarmError; window.textsecure.SeedNodeError = SeedNodeError; - window.textsecure.DNSResolutionError = DNSResolutionError; window.textsecure.HTTPError = HTTPError; window.textsecure.NotFoundError = NotFoundError; window.textsecure.WrongSwarmError = WrongSwarmError; window.textsecure.WrongDifficultyError = WrongDifficultyError; window.textsecure.TimestampError = TimestampError; window.textsecure.PublicChatError = PublicChatError; - window.textsecure.PublicTokenError = PublicTokenError; })(); diff --git a/libtextsecure/index.d.ts b/libtextsecure/index.d.ts index 894521dc2c..1b65814d14 100644 --- a/libtextsecure/index.d.ts +++ b/libtextsecure/index.d.ts @@ -5,18 +5,14 @@ export interface LibTextsecure { crypto: LibTextsecureCryptoInterface; storage: any; SendMessageNetworkError: any; - IncomingIdentityKeyError: any; ReplayableError: any; - MessageError: any; EmptySwarmError: any; SeedNodeError: any; - DNSResolutionError: any; HTTPError: any; NotFoundError: any; WrongSwarmError: any; WrongDifficultyError: any; TimestampError: any; PublicChatError: any; - PublicTokenError: any; createTaskWithTimeout(task: any, id: any, options?: any): Promise; } diff --git a/ts/models/message.ts b/ts/models/message.ts index cf34456e61..f260487b98 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -930,9 +930,7 @@ export class MessageModel extends Backbone.Model { public removeOutgoingErrors(number: string) { const errors = _.partition( this.get('errors'), - e => - e.number === number && - (e.name === 'MessageError' || e.name === 'SendMessageNetworkError') + e => e.number === number && e.name === 'SendMessageNetworkError' ); this.set({ errors: errors[1] }); return errors[0][0]; From 8716fbf49544d9d4eaade971b7d8b4f546a2962c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 19 Feb 2021 17:47:54 +1100 Subject: [PATCH 043/109] improve sent message handling by setting the correct convoId at start --- ts/models/message.ts | 9 +++++++- ts/models/messageType.ts | 4 ++-- ts/receiver/attachments.ts | 8 +------- ts/receiver/dataMessage.ts | 26 ++++++++++-------------- ts/receiver/receiver.ts | 9 ++++---- ts/session/sending/MessageSentHandler.ts | 12 +++++++---- 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/ts/models/message.ts b/ts/models/message.ts index f260487b98..75bc5ffa17 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -41,6 +41,13 @@ export class MessageModel extends Backbone.Model { }) ); + if (!this.attributes.id) { + throw new Error('A message always needs to have an id.'); + } + if (!this.attributes.conversationId) { + throw new Error('A message always needs to have an conversationId.'); + } + // this.on('expired', this.onExpired); void this.setToExpire(); autoBind(this); @@ -239,7 +246,7 @@ export class MessageModel extends Backbone.Model { public getPropsForTimerNotification() { const timerUpdate = this.get('expirationTimerUpdate'); - if (!timerUpdate) { + if (!timerUpdate || !timerUpdate.source) { return null; } diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 32e5aae697..c7139479ae 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -37,7 +37,7 @@ export interface MessageAttributes { groupInvitation?: any; attachments?: any; contact?: any; - conversationId: any; + conversationId: string; errors?: any; flags?: number; hasAttachments: boolean; @@ -90,7 +90,7 @@ export interface MessageAttributesOptionals { groupInvitation?: any; attachments?: any; contact?: any; - conversationId: any; + conversationId: string; errors?: any; flags?: number; hasAttachments?: boolean; diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index f65d19b4ad..91e5e18189 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -222,9 +222,7 @@ async function processGroupAvatar(message: MessageModel): Promise { export async function queueAttachmentDownloads( message: MessageModel -): Promise { - const { Whisper } = window; - +): Promise { let count = 0; count += await processNormalAttachments(message, message.get('attachments')); @@ -241,9 +239,5 @@ export async function queueAttachmentDownloads( if (count > 0) { await saveMessage(message.attributes); - - return true; } - - return false; } diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 28824cc3b9..82f59b594c 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -491,32 +491,28 @@ function createSentMessage(data: MessageCreationData): MessageModel { isPublic, receivedAt, sourceDevice, - unidentifiedStatus, expirationStartTimestamp, destination, + message, } = data; - let unidentifiedDeliveries; - - if (unidentifiedStatus && unidentifiedStatus.length) { - sentTo = unidentifiedStatus.map((item: any) => item.destination); - const unidentified = _.filter(unidentifiedStatus, (item: any) => - Boolean(item.unidentified) - ); - // eslint-disable-next-line no-param-reassign - unidentifiedDeliveries = unidentified.map((item: any) => item.destination); - } - const sentSpecificFields = { - sent_to: sentTo, + sent_to: [], sent: true, - unidentifiedDeliveries: unidentifiedDeliveries || [], expirationStartTimestamp: Math.min( expirationStartTimestamp || data.timestamp || now, now ), }; + const messageGroupId = message?.group?.id; + let groupId = + messageGroupId && messageGroupId.length > 0 ? messageGroupId : null; + + if (groupId) { + groupId = PubKey.removeTextSecurePrefixIfNeeded(groupId); + } + const messageData = { source: UserUtils.getOurPubKeyStrFromCache(), sourceDevice, @@ -525,7 +521,7 @@ function createSentMessage(data: MessageCreationData): MessageModel { sent_at: timestamp, received_at: isPublic ? receivedAt : now, isPublic, - conversationId: destination, // conversation ID will might change later (if it is a group) + conversationId: groupId ?? destination, type: 'outgoing' as MessageModelType, ...sentSpecificFields, }; diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 7507ccd052..9aef77f2aa 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -194,14 +194,15 @@ export async function queueAllCached() { export async function queueAllCachedFromSource(source: string) { const items = await getAllFromCacheForSource(source); - items.forEach(async item => { + + // queue all cached for this source, but keep the order + await items.reduce(async (promise, item) => { + await promise; await queueCached(item); - }); + }, Promise.resolve()); } async function queueCached(item: any) { - const { textsecure } = window; - try { const envelopePlaintext = StringUtils.encode(item.envelope, 'base64'); const envelopeArray = new Uint8Array(envelopePlaintext); diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 9e3b8ae8a4..e7c7bb9200 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -116,10 +116,14 @@ export class MessageSentHandler { // Handle the sync logic here if (shouldTriggerSyncMessage) { if (dataMessage) { - await fetchedMessage.sendSyncMessage( - dataMessage as SignalService.DataMessage, - sentMessage.timestamp - ); + try { + await fetchedMessage.sendSyncMessage( + dataMessage as SignalService.DataMessage, + sentMessage.timestamp + ); + } catch (e) { + window.log.warn('Got an error while trying to sendSyncMessage():', e); + } } } else if (shouldMarkMessageAsSynced) { fetchedMessage.set({ synced: true }); From 5ab3680903d938cd2aceddf70ef6eb25798a7839 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 09:34:47 +1100 Subject: [PATCH 044/109] make OnionPath a singleton and build path on app Start only --- js/background.js | 8 -------- js/expire.js | 15 ++++++++++++++- js/modules/loki_app_dot_net_api.js | 4 ++-- preload.js | 4 ++-- ts/components/session/ActionsPanel.tsx | 10 ++++++++++ ts/session/onions/index.ts | 15 ++++++++++++--- ts/session/snode_api/onions.ts | 8 ++++---- 7 files changed, 44 insertions(+), 20 deletions(-) diff --git a/js/background.js b/js/background.js index a36ef16c84..158ba5155a 100644 --- a/js/background.js +++ b/js/background.js @@ -181,14 +181,6 @@ // Update zoom window.updateZoomFactor(); - if ( - window.lokiFeatureFlags.useOnionRequests || - window.lokiFeatureFlags.useFileOnionRequests - ) { - // Initialize paths for onion requests - window.OnionAPI.buildNewOnionPaths(); - } - const currentPoWDifficulty = storage.get('PoWDifficulty', null); if (!currentPoWDifficulty) { storage.put('PoWDifficulty', window.getDefaultPoWDifficulty()); diff --git a/js/expire.js b/js/expire.js index b7f3992c0a..23345bdf0a 100644 --- a/js/expire.js +++ b/js/expire.js @@ -17,9 +17,23 @@ let nextWaitSeconds = 5; const checkForUpgrades = async () => { + try { + window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); + } catch (e) { + // give it a minute + log.warn( + 'Could not check to see if newer version is available cause our pubkey is not set' + ); + nextWaitSeconds = 60; + setTimeout(async () => { + await checkForUpgrades(); + }, nextWaitSeconds * 1000); // wait a minute + return; + } const result = await window.tokenlessFileServerAdnAPI.serverRequest( 'loki/v1/version/client/desktop' ); + if ( result && result.response && @@ -133,5 +147,4 @@ window.clientClockSynced = Math.abs(timeDifferential) < maxTimeDifferential; return window.clientClockSynced; }; - window.setClockParams(); })(); diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 3479c097ca..942b850a06 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -64,7 +64,7 @@ const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => { // eslint-disable-next-line no-param-reassign options.retry = 0; // eslint-disable-next-line no-param-reassign - options.requestNumber = window.OnionAPI.assignOnionRequestNumber(); + options.requestNumber = window.OnionPaths.getInstance().assignOnionRequestNumber(); } const payloadObj = { @@ -97,7 +97,7 @@ const sendViaOnion = async (srvPubKey, url, fetchOptions, options = {}) => { let pathNodes = []; try { - pathNodes = await window.OnionAPI.getOnionPath(); + pathNodes = await window.OnionPaths.getInstance().getOnionPath(); } catch (e) { log.error( `loki_app_dot_net:::sendViaOnion #${options.requestNumber} - getOnionPath Error ${e.code} ${e.message}` diff --git a/preload.js b/preload.js index d2842f0209..78e302266c 100644 --- a/preload.js +++ b/preload.js @@ -395,7 +395,7 @@ window.clipboard = clipboard; window.seedNodeList = JSON.parse(config.seedNodeList); -const { OnionAPI } = require('./ts/session/onions'); +const { OnionPaths } = require('./ts/session/onions'); const { locale } = config; window.i18n = i18n.setup(locale, localeMessages); @@ -413,7 +413,7 @@ window.moment.updateLocale(localeForMoment, { }); window.moment.locale(localeForMoment); -window.OnionAPI = OnionAPI; +window.OnionPaths = OnionPaths; window.libsession = require('./ts/session'); window.models = require('./ts/models'); diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index e5b23b4ff3..c7a72cefa1 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -19,6 +19,7 @@ import { } from '../../session/utils/syncUtils'; import { DAYS } from '../../session/utils/Number'; import { removeItemById } from '../../data/data'; +import { OnionPaths } from '../../session/onions'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -54,6 +55,15 @@ class ActionsPanelPrivate extends React.Component { // fetch the user saved theme from the db, and apply it on mount. public componentDidMount() { + void window.setClockParams(); + if ( + window.lokiFeatureFlags.useOnionRequests || + window.lokiFeatureFlags.useFileOnionRequests + ) { + // Initialize paths for onion requests + void OnionPaths.getInstance().buildNewOnionPaths(); + } + const theme = window.Events.getThemeSetting(); window.setTheme(theme); diff --git a/ts/session/onions/index.ts b/ts/session/onions/index.ts index 1895ae0aac..a3b5b6e4a0 100644 --- a/ts/session/onions/index.ts +++ b/ts/session/onions/index.ts @@ -12,7 +12,9 @@ interface SnodePath { bad: boolean; } -class OnionPaths { +export class OnionPaths { + private static instance: OnionPaths | null; + private onionPaths: Array = []; // This array is meant to store nodes will full info, @@ -20,6 +22,15 @@ class OnionPaths { // some naming issue here it seems) private guardNodes: Array = []; private onionRequestCounter = 0; // Request index for debugging + private constructor() {} + + public static getInstance() { + if (OnionPaths.instance) { + return OnionPaths.instance; + } + OnionPaths.instance = new OnionPaths(); + return OnionPaths.instance; + } public async buildNewOnionPaths() { // this function may be called concurrently make sure we only have one inflight @@ -304,5 +315,3 @@ class OnionPaths { log.info(`Built ${this.onionPaths.length} onion paths`, this.onionPaths); } } - -export const OnionAPI = new OnionPaths(); diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts index 34d6df983e..49efb13bc2 100644 --- a/ts/session/snode_api/onions.ts +++ b/ts/session/snode_api/onions.ts @@ -4,7 +4,7 @@ import https from 'https'; import { Snode } from './snodePool'; import ByteBuffer from 'bytebuffer'; import { StringUtils } from '../utils'; -import { OnionAPI } from '../onions'; +import { OnionPaths } from '../onions'; let onionPayload = 0; @@ -522,8 +522,8 @@ export async function lokiOnionFetch( while (true) { // Get a path excluding `targetNode`: // eslint-disable-next-line no-await-in-loop - const path = await OnionAPI.getOnionPath(targetNode); - const thisIdx = OnionAPI.assignOnionRequestNumber(); + const path = await OnionPaths.getInstance().getOnionPath(targetNode); + const thisIdx = OnionPaths.getInstance().assignOnionRequestNumber(); // At this point I only care about BAD_PATH @@ -541,7 +541,7 @@ export async function lokiOnionFetch( targetNode.port }` ); - OnionAPI.markPathAsBad(path); + OnionPaths.getInstance().markPathAsBad(path); return false; } else if (result === RequestError.OTHER) { // could mean, fail to parse results From 58cc6551e57b099f5cf3367cc9fc1c82c07e6f78 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 09:58:29 +1100 Subject: [PATCH 045/109] fix updates of message on message syncing --- ts/models/message.ts | 1 + .../outgoing/content/data/ChatMessage.ts | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ts/models/message.ts b/ts/models/message.ts index 75bc5ffa17..298f1cd4b7 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1055,6 +1055,7 @@ export class MessageModel extends Backbone.Model { throw new Error('Cannot trigger syncMessage with unknown convo.'); } const syncMessage = ChatMessage.buildSyncMessage( + this.id, dataMessage, conversation.id, sentTimestamp diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 4de97ab2ec..fcd5ad6f4a 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -91,19 +91,13 @@ export class ChatMessage extends DataMessage { } public static buildSyncMessage( - dataMessage: SignalService.IDataMessage, + identifier: string, + dataMessage: SignalService.DataMessage, syncTarget: string, sentTimestamp: number ) { - // the dataMessage.profileKey is of type ByteBuffer. We need to make it a Uint8Array - const lokiProfile: any = { - profileKey: new Uint8Array( - (dataMessage.profileKey as any).toArrayBuffer() - ), - }; - if ( - (dataMessage as any)?.$type?.name !== 'DataMessage' && + (dataMessage as any).constructor.name !== 'DataMessage' && !(dataMessage instanceof DataMessage) ) { throw new Error( @@ -114,6 +108,13 @@ export class ChatMessage extends DataMessage { if (!sentTimestamp || !isNumber(sentTimestamp)) { throw new Error('Tried to build a sync message without a sentTimestamp'); } + // the dataMessage.profileKey is of type ByteBuffer. We need to make it a Uint8Array + const lokiProfile: any = {}; + if (dataMessage.profileKey?.length) { + lokiProfile.profileKey = new Uint8Array( + (dataMessage.profileKey as any).toArrayBuffer() + ); + } if (dataMessage.profile) { if (dataMessage.profile?.displayName) { @@ -141,6 +142,7 @@ export class ChatMessage extends DataMessage { const preview = (dataMessage.preview as Array) || []; return new ChatMessage({ + identifier, timestamp, attachments, body, From e92632285b30a3af0ed604b59a7d1c561ba55c2e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 11:07:58 +1100 Subject: [PATCH 046/109] init messageQueue in the ActionsPanel This is to unsure that unsent messages in the pipeline are added to the pipeline right when we start the app again --- ts/components/session/ActionsPanel.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index c7a72cefa1..b834aa846e 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -20,6 +20,7 @@ import { import { DAYS } from '../../session/utils/Number'; import { removeItemById } from '../../data/data'; import { OnionPaths } from '../../session/onions'; +import { getMessageQueue } from '../../session/sending'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -63,6 +64,9 @@ class ActionsPanelPrivate extends React.Component { // Initialize paths for onion requests void OnionPaths.getInstance().buildNewOnionPaths(); } + // init the messageQueue. In the constructor, we had all not send messages + // this call does nothing except calling the constructor, which will continue sending message in the pipeline + getMessageQueue(); const theme = window.Events.getThemeSetting(); window.setTheme(theme); From ca22b4635f9bda92bc0baaffa8b84838a1019d40 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 12:00:08 +1100 Subject: [PATCH 047/109] fixup some building of sync message issues --- ts/components/session/ActionsPanel.tsx | 2 +- ts/models/message.ts | 2 +- ts/receiver/queuedJob.ts | 2 +- .../outgoing/content/data/ChatMessage.ts | 22 ++++++++++++++----- ts/session/sending/MessageQueue.ts | 6 ++++- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index b834aa846e..1292fcc0de 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -66,7 +66,7 @@ class ActionsPanelPrivate extends React.Component { } // init the messageQueue. In the constructor, we had all not send messages // this call does nothing except calling the constructor, which will continue sending message in the pipeline - getMessageQueue(); + void getMessageQueue().processAllPending(); const theme = window.Events.getThemeSetting(); window.setTheme(theme); diff --git a/ts/models/message.ts b/ts/models/message.ts index 298f1cd4b7..eac7138d6f 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1020,7 +1020,7 @@ export class MessageModel extends Backbone.Model { await this.commit(); } - public async sendSyncMessageOnly(dataMessage: any) { + public async sendSyncMessageOnly(dataMessage: DataMessage) { const now = Date.now(); this.set({ sent_to: [UserUtils.getOurPubKeyStrFromCache()], diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 3213002498..fc06d17ebf 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -211,7 +211,7 @@ async function handleExpireTimer( await conversation.updateExpirationTimer( expireTimer, source, - message.get('received_at'), + message.get('sent_at') || message.get('received_at'), { fromGroupUpdate: message.isGroupUpdate(), // WHAT DOES GROUP UPDATE HAVE TO DO WITH THIS??? } diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index fcd5ad6f4a..119aac9c83 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -127,15 +127,25 @@ export class ChatMessage extends DataMessage { const timestamp = toNumber(sentTimestamp); const body = dataMessage.body || undefined; + + const wrapToUInt8Array = (buffer: any) => { + if (!buffer) { + return undefined; + } + if (buffer instanceof Uint8Array) { + // Audio messages are already uint8Array + return buffer; + } + return new Uint8Array(buffer.toArrayBuffer()); + }; const attachments = (dataMessage.attachments || []).map(attachment => { + const key = wrapToUInt8Array(attachment.key); + const digest = wrapToUInt8Array(attachment.digest); + return { ...attachment, - key: attachment.key - ? new Uint8Array((attachment.key as any).toArrayBuffer()) - : undefined, - digest: attachment.digest - ? new Uint8Array((attachment.digest as any).toArrayBuffer()) - : undefined, + key, + digest, }; }) as Array; const quote = (dataMessage.quote as Quote) || undefined; diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 0d49140836..b7655d7e46 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -169,7 +169,11 @@ export class MessageQueue { }); } - private async processAllPending() { + /** + * This method should be called when the app is started and the user loggedin to fetch + * existing message waiting to be sent in the cache of message + */ + public async processAllPending() { const devices = await this.pendingMessageCache.getDevices(); const promises = devices.map(async device => this.processPending(device)); From 6ed4511c21876fcf64f44cb3cc099fa2c7ccef5e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 12:41:00 +1100 Subject: [PATCH 048/109] be sure to hide emjipanel when sending a message --- ts/components/session/conversation/SessionCompositionBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 08c38720b8..ae77ed217a 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -225,7 +225,7 @@ export class SessionCompositionBox extends React.Component { return; } - this.toggleEmojiPanel(); + this.hideEmojiPanel(); } private showEmojiPanel() { From 31e2341978b5f43502fa0dfc65e1540edfd48dc6 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 12:41:24 +1100 Subject: [PATCH 049/109] update last message on message delete otherwise, we might still see the last message on the leftpane if the message removed was the last one --- ts/models/conversation.ts | 11 +++++++++-- ts/receiver/attachments.ts | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 1d6d4e3233..5f9d30908e 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -622,12 +622,15 @@ export class ConversationModel extends Backbone.Model { embeddedContact && embeddedContact.length > 0 ? getName(embeddedContact[0]) : ''; - + const quotedAttachments = await this.getQuoteAttachment( + attachments, + preview + ); return { author: contact.id, id: quotedMessage.get('sent_at'), text: body || embeddedContactName, - attachments: await this.getQuoteAttachment(attachments, preview), + attachments: quotedAttachments, }; } @@ -1446,11 +1449,15 @@ export class ConversationModel extends Backbone.Model { }) ); + await this.updateLastMessage(); + return toDeleteLocally; } public async removeMessage(messageId: any) { await dataRemoveMessage(messageId); + this.updateLastMessage(); + window.Whisper.events.trigger('messageDeleted', { conversationKey: this.id, messageId, diff --git a/ts/receiver/attachments.ts b/ts/receiver/attachments.ts index 91e5e18189..43c821dc30 100644 --- a/ts/receiver/attachments.ts +++ b/ts/receiver/attachments.ts @@ -225,7 +225,10 @@ export async function queueAttachmentDownloads( ): Promise { let count = 0; - count += await processNormalAttachments(message, message.get('attachments')); + count += await processNormalAttachments( + message, + message.get('attachments') || [] + ); count += await processPreviews(message); From d5c4108ed64ca96a9e4a96b699fe5cea7be7e1c7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 13:29:33 +1100 Subject: [PATCH 050/109] fix tests --- test/models/messages_test.js | 37 +++++++++++++++---- .../session/unit/sending/MessageQueue_test.ts | 4 -- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/test/models/messages_test.js b/test/models/messages_test.js index 3add5067dd..49c43c0369 100644 --- a/test/models/messages_test.js +++ b/test/models/messages_test.js @@ -33,6 +33,7 @@ describe('MessageCollection', () => { const message = messages.add({ type: 'incoming', source, + conversationId: 'conversationId', }); message.getContact(); }); @@ -45,8 +46,8 @@ describe('MessageCollection', () => { tomorrow.setDate(today.getDate() + 1); // Add threads - messages.add({ received_at: today }); - messages.add({ received_at: tomorrow }); + messages.add({ received_at: today, conversationId: 'conversationId' }); + messages.add({ received_at: tomorrow, conversationId: 'conversationId' }); const { models } = messages; const firstTimestamp = models[0].get('received_at').getTime(); @@ -60,7 +61,10 @@ describe('MessageCollection', () => { const messages = new window.models.Message.MessageCollection(); let message = messages.add(attributes); assert.notOk(message.isIncoming()); - message = messages.add({ type: 'incoming' }); + message = messages.add({ + type: 'incoming', + conversationId: 'conversationId', + }); assert.ok(message.isIncoming()); }); @@ -68,7 +72,10 @@ describe('MessageCollection', () => { const messages = new window.models.Message.MessageCollection(); let message = messages.add(attributes); assert.ok(message.isOutgoing()); - message = messages.add({ type: 'incoming' }); + message = messages.add({ + type: 'incoming', + conversationId: 'conversationId', + }); assert.notOk(message.isOutgoing()); }); @@ -77,7 +84,10 @@ describe('MessageCollection', () => { let message = messages.add(attributes); assert.notOk(message.isGroupUpdate()); - message = messages.add({ group_update: true }); + message = messages.add({ + group_update: true, + conversationId: 'conversationId', + }); assert.ok(message.isGroupUpdate()); }); @@ -91,21 +101,30 @@ describe('MessageCollection', () => { 'If no group updates or end session flags, return message body.' ); - message = messages.add({ group_update: { left: 'Alice' } }); + message = messages.add({ + group_update: { left: 'Alice' }, + conversationId: 'conversationId', + }); assert.equal( message.getDescription(), 'Alice has left the group.', 'Notes one person leaving the group.' ); - message = messages.add({ group_update: { name: 'blerg' } }); + message = messages.add({ + group_update: { name: 'blerg' }, + conversationId: 'conversationId', + }); assert.equal( message.getDescription(), "Group name is now 'blerg'.", 'Returns a single notice if only group_updates.name changes.' ); - message = messages.add({ group_update: { joined: ['Bob'] } }); + message = messages.add({ + group_update: { joined: ['Bob'] }, + conversationId: 'conversationId', + }); assert.equal( message.getDescription(), 'Bob joined the group.', @@ -114,6 +133,7 @@ describe('MessageCollection', () => { message = messages.add({ group_update: { joined: ['Bob', 'Alice', 'Eve'] }, + conversationId: 'conversationId', }); assert.equal( message.getDescription(), @@ -123,6 +143,7 @@ describe('MessageCollection', () => { message = messages.add({ group_update: { joined: ['Bob'], name: 'blerg' }, + conversationId: 'conversationId', }); assert.equal( message.getDescription(), diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index 695616f77b..d018490e97 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -232,10 +232,6 @@ describe('MessageQueue', () => { const message = TestUtils.generateOpenGroupMessage(); await messageQueueStub.sendToOpenGroup(message); - expect(messageSentHandlerSuccessStub.callCount).to.equal(1); - expect( - messageSentHandlerSuccessStub.lastCall.args[0].identifier - ).to.equal(message.identifier); expect(messageSentPublicHandlerSuccessStub.callCount).to.equal(1); expect( messageSentPublicHandlerSuccessStub.lastCall.args[0].identifier From 4d6fcda668256d9a5a05e7ef6b06c8dc95d86565 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 13:51:11 +1100 Subject: [PATCH 051/109] fix typo SessionLastSeedIndicator => SessionLastSeenIndicator --- ...essionLastSeedIndicator.tsx => SessionLastSeenIndicator.tsx} | 0 ts/components/session/conversation/SessionMessagesList.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename ts/components/session/conversation/{SessionLastSeedIndicator.tsx => SessionLastSeenIndicator.tsx} (100%) diff --git a/ts/components/session/conversation/SessionLastSeedIndicator.tsx b/ts/components/session/conversation/SessionLastSeenIndicator.tsx similarity index 100% rename from ts/components/session/conversation/SessionLastSeedIndicator.tsx rename to ts/components/session/conversation/SessionLastSeenIndicator.tsx diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 9242eb301a..60b366e8bb 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -11,7 +11,7 @@ import { AttachmentType } from '../../../types/Attachment'; import { GroupNotification } from '../../conversation/GroupNotification'; import { GroupInvitation } from '../../conversation/GroupInvitation'; import { ConversationType } from '../../../state/ducks/conversations'; -import { SessionLastSeenIndicator } from './SessionLastSeedIndicator'; +import { SessionLastSeenIndicator } from './SessionLastSeenIndicator'; import { ToastUtils } from '../../../session/utils'; import { TypingBubble } from '../../conversation/TypingBubble'; import { ConversationController } from '../../../session/conversations'; From 4f7cb045524c4c52041477dc5d64b1b036e6f6d7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 14:10:26 +1100 Subject: [PATCH 052/109] remove completely SignalProtocolStore --- Gruntfile.js | 1 - background.html | 1 - background_test.html | 1 - js/signal_protocol_store.js | 33 ------------------------- libloki/test/index.html | 1 - libtextsecure/protocol_wrapper.js | 8 ------- libtextsecure/test/index.html | 1 - libtextsecure/test/protocol_test.js | 37 ----------------------------- test/backup_test.js | 10 +++++++- test/index.html | 1 - 10 files changed, 9 insertions(+), 85 deletions(-) delete mode 100644 js/signal_protocol_store.js delete mode 100644 libtextsecure/protocol_wrapper.js delete mode 100644 libtextsecure/test/protocol_test.js diff --git a/Gruntfile.js b/Gruntfile.js index d09285c67d..a3b1251af2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -75,7 +75,6 @@ module.exports = grunt => { src: [ 'libtextsecure/errors.js', 'libtextsecure/libsignal-protocol.js', - 'libtextsecure/protocol_wrapper.js', 'libtextsecure/crypto.js', 'libtextsecure/storage.js', 'libtextsecure/storage/user.js', diff --git a/background.html b/background.html index 4a873f3a50..d865a59f63 100644 --- a/background.html +++ b/background.html @@ -132,7 +132,6 @@ - diff --git a/background_test.html b/background_test.html index 599b6200cd..63c232333d 100644 --- a/background_test.html +++ b/background_test.html @@ -136,7 +136,6 @@ - diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js deleted file mode 100644 index ed05459270..0000000000 --- a/js/signal_protocol_store.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - global - Backbone, - _, - BlockedNumberController -*/ - -/* eslint-disable no-proto */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - function SignalProtocolStore() {} - - SignalProtocolStore.prototype = { - constructor: SignalProtocolStore, - async removeAllData() { - await window.Signal.Data.removeAll(); - - window.storage.reset(); - await window.storage.fetch(); - - window.getConversationController().reset(); - BlockedNumberController.reset(); - await window.getConversationController().load(); - await BlockedNumberController.load(); - }, - }; - _.extend(SignalProtocolStore.prototype, Backbone.Events); - - window.SignalProtocolStore = SignalProtocolStore; -})(); diff --git a/libloki/test/index.html b/libloki/test/index.html index f91e323a69..e1c72704c7 100644 --- a/libloki/test/index.html +++ b/libloki/test/index.html @@ -19,7 +19,6 @@ - diff --git a/libtextsecure/protocol_wrapper.js b/libtextsecure/protocol_wrapper.js deleted file mode 100644 index 73391ae4e9..0000000000 --- a/libtextsecure/protocol_wrapper.js +++ /dev/null @@ -1,8 +0,0 @@ -/* global window, textsecure, SignalProtocolStore */ - -// eslint-disable-next-line func-names -(function() { - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - textsecure.storage.protocol = new SignalProtocolStore(); -})(); diff --git a/libtextsecure/test/index.html b/libtextsecure/test/index.html index 3477b0328c..32b53b28ac 100644 --- a/libtextsecure/test/index.html +++ b/libtextsecure/test/index.html @@ -21,7 +21,6 @@ - diff --git a/libtextsecure/test/protocol_test.js b/libtextsecure/test/protocol_test.js deleted file mode 100644 index aad806caf4..0000000000 --- a/libtextsecure/test/protocol_test.js +++ /dev/null @@ -1,37 +0,0 @@ -/* global textsecure */ - -describe('Protocol', () => { - describe('Unencrypted PushMessageProto "decrypt"', () => { - // exclusive - it('works', done => { - localStorage.clear(); - - const textMessage = new textsecure.protobuf.DataMessage(); - textMessage.body = 'Hi Mom'; - const serverMessage = { - type: 4, // unencrypted - source: '+19999999999', - timestamp: 42, - message: textMessage.encode(), - }; - - return textsecure.protocol_wrapper - .handleEncryptedMessage( - serverMessage.source, - serverMessage.source_device, - serverMessage.type, - serverMessage.message - ) - .then(message => { - assert.equal(message.body, textMessage.body); - assert.equal( - message.attachments.length, - textMessage.attachments.length - ); - assert.equal(textMessage.attachments.length, 0); - }) - .then(done) - .catch(done); - }); - }); -}); diff --git a/test/backup_test.js b/test/backup_test.js index 8d15a14cc4..c0660a4e96 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -288,7 +288,15 @@ describe('Backup', () => { } async function clearAllData() { - await textsecure.storage.protocol.removeAllData(); + await window.Signal.Data.removeAll(); + + window.storage.reset(); + await window.storage.fetch(); + + window.getConversationController().reset(); + window.BlockedNumberController.reset(); + await window.getConversationController().load(); + await window.BlockedNumberController.load(); await fse.emptyDir(attachmentsPath); } diff --git a/test/index.html b/test/index.html index 455d9e8712..83d54ad1d3 100644 --- a/test/index.html +++ b/test/index.html @@ -175,7 +175,6 @@

- From 1fc672da285a3cc2ea223b9ae279569ba6a8f2fa Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 14:40:47 +1100 Subject: [PATCH 053/109] adress review --- preload.js | 1 + test/backup_test.js | 2 +- ts/models/messageType.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/preload.js b/preload.js index 78e302266c..6718aff28d 100644 --- a/preload.js +++ b/preload.js @@ -476,6 +476,7 @@ if ( tmp: require('tmp'), path: require('path'), basePath: __dirname, + attachmentsPath: window.Signal.Migrations.attachmentsPath, isWindows, }; /* eslint-enable global-require, import/no-extraneous-dependencies */ diff --git a/test/backup_test.js b/test/backup_test.js index c0660a4e96..f474f4bff2 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -1,4 +1,4 @@ -/* global Signal, assert, textsecure, _, libsignal */ +/* global Signal, assert, _, libsignal */ /* eslint-disable no-console */ diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index c7139479ae..7a7cbf589a 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -132,6 +132,7 @@ export const fillMessageAttributesWithDefaults = ( return _.defaults(optAttributes, { expireTimer: 0, // disabled id: uuidv4(), + schemaVersion: window.Signal.Types.Message.CURRENT_SCHEMA_VERSION, }); }; From 08004ad7b82b83dbc79636109440f202dbf82c98 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 15:08:06 +1100 Subject: [PATCH 054/109] WIP --- AUDRICTOCLEAN.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/AUDRICTOCLEAN.txt b/AUDRICTOCLEAN.txt index f1bf48c7d0..be9550220b 100644 --- a/AUDRICTOCLEAN.txt +++ b/AUDRICTOCLEAN.txt @@ -30,4 +30,13 @@ sendSyncMessageOnly to fix indexedDB initializeAttachmentMetadata=> schemaVersion for messages to put as what needs to be set -run_migration \ No newline at end of file +run_migration + + + +### Bug fixes on update of models +* quote of attachment does not share preview +* setting disappearing timer for isMe does not trigger the message +* expiration timer set from user1 second device is not synced to his other devices for a private chat +* add a way for users to know when the messageQueue is reconnected (loading bar or something) +* handled better reconnection \ No newline at end of file From c81e7b4547c93826af00bc77231645ae5f36b031 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 16:25:00 +1100 Subject: [PATCH 055/109] remove two unused files --- .vscode/launch.json | 58 --------------------------------------------- LOKI-NOTES.md | 6 ----- 2 files changed, 64 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 LOKI-NOTES.md diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 9a92b58d54..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Mocha Tests", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "--recursive", - "--exit", - "test/app", - "test/modules", - "ts/test", - "libloki/test/node" - ], - "internalConsoleOptions": "openOnSessionStart" - }, - { - "type": "node", - "request": "launch", - "name": "Launch node Program", - "program": "${file}" - }, - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${file}" - }, - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}" - }, - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "env": { - "NODE_APP_INSTANCE": "1" - }, - "cwd": "${workspaceRoot}", - "console": "integratedTerminal", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" - }, - "args": ["."], - "sourceMaps": true, - "outputCapture": "std" - } - ] -} diff --git a/LOKI-NOTES.md b/LOKI-NOTES.md deleted file mode 100644 index 18d4aefad7..0000000000 --- a/LOKI-NOTES.md +++ /dev/null @@ -1,6 +0,0 @@ -# Create fake contacts - -Run `window.getAccountManager().addMockContact()` in the debugger console. This will print the fake contact public key. -Behind the scenes, this also emulates that we're already received the contact's prekeys by generating them and saving them in our db. -Copy/paste that public key in the search bar to start chatting. -The messages should have a "v" tick to mark that the message was correctly sent (you need to run the httpserver from /mockup_servers) From 99cc5b448a7e6a1ee4d66415e9879f84f9b18890 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 22 Feb 2021 16:29:17 +1100 Subject: [PATCH 056/109] remove unused qrcode.js --- bower.json | 3 - components/qrcode/qrcode.js | 609 ----------------------------------- ts/util/lint/exceptions.json | 18 -- 3 files changed, 630 deletions(-) delete mode 100644 components/qrcode/qrcode.js diff --git a/bower.json b/bower.json index 199dd33bbe..3092a6bb13 100644 --- a/bower.json +++ b/bower.json @@ -20,9 +20,6 @@ ], "protobuf": [ "dist/ProtoBuf.js" - ], - "qrcode": [ - "qrcode.js" ] }, "concat": { diff --git a/components/qrcode/qrcode.js b/components/qrcode/qrcode.js deleted file mode 100644 index c1217a383b..0000000000 --- a/components/qrcode/qrcode.js +++ /dev/null @@ -1,609 +0,0 @@ -/** - * @fileoverview - * - Using the 'QRCode for Javascript library' - * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. - * - this library has no dependencies. - * - * @author davidshimjs - * @see http://www.d-project.com/ - * @see http://jeromeetienne.github.com/jquery-qrcode/ - */ -var QRCode; - -(function () { - //--------------------------------------------------------------------- - // QRCode for JavaScript - // - // Copyright (c) 2009 Kazuhiko Arase - // - // URL: http://www.d-project.com/ - // - // Licensed under the MIT license: - // http://www.opensource.org/licenses/mit-license.php - // - // The word "QR Code" is registered trademark of - // DENSO WAVE INCORPORATED - // http://www.denso-wave.com/qrcode/faqpatent-e.html - // - //--------------------------------------------------------------------- - function QR8bitByte(data) { - this.mode = QRMode.MODE_8BIT_BYTE; - this.data = data; - this.parsedData = []; - - // Added to support UTF-8 Characters - for (var i = 0, l = this.data.length; i < l; i++) { - var byteArray = []; - var code = this.data.charCodeAt(i); - - if (code > 0x10000) { - byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); - byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); - byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); - byteArray[3] = 0x80 | (code & 0x3F); - } else if (code > 0x800) { - byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); - byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); - byteArray[2] = 0x80 | (code & 0x3F); - } else if (code > 0x80) { - byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); - byteArray[1] = 0x80 | (code & 0x3F); - } else { - byteArray[0] = code; - } - - this.parsedData.push(byteArray); - } - - this.parsedData = Array.prototype.concat.apply([], this.parsedData); - - if (this.parsedData.length != this.data.length) { - this.parsedData.unshift(191); - this.parsedData.unshift(187); - this.parsedData.unshift(239); - } - } - - QR8bitByte.prototype = { - getLength: function (buffer) { - return this.parsedData.length; - }, - write: function (buffer) { - for (var i = 0, l = this.parsedData.length; i < l; i++) { - buffer.put(this.parsedData[i], 8); - } - } - }; - - function QRCodeModel(typeNumber, errorCorrectLevel) { - this.typeNumber = typeNumber; - this.errorCorrectLevel = errorCorrectLevel; - this.modules = null; - this.moduleCount = 0; - this.dataCache = null; - this.dataList = []; - } - - QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} - return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} - if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} - this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} - return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} - for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} - for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} - this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} - var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} - this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} - row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" - +buffer.getLengthInBits() - +">" - +totalDataCount*8 - +")");} - if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} - while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} - while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} - buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} - buffer.put(QRCodeModel.PAD1,8);} - return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} - var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} - return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} - return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} - return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} - for(var row=0;row=256){n-=255;} - return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} - if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} - this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; - - function _isSupportCanvas() { - return typeof CanvasRenderingContext2D != "undefined"; - } - - // android 2.x doesn't support Data-URI spec - function _getAndroid() { - var android = false; - var sAgent = navigator.userAgent; - - if (/android/i.test(sAgent)) { // android - android = true; - aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); - - if (aMat && aMat[1]) { - android = parseFloat(aMat[1]); - } - } - - return android; - } - - var svgDrawer = (function() { - - var Drawing = function (el, htOption) { - this._el = el; - this._htOption = htOption; - }; - - Drawing.prototype.draw = function (oQRCode) { - var _htOption = this._htOption; - var _el = this._el; - var nCount = oQRCode.getModuleCount(); - var nWidth = Math.floor(_htOption.width / nCount); - var nHeight = Math.floor(_htOption.height / nCount); - - this.clear(); - - function makeSVG(tag, attrs) { - var el = document.createElementNS('http://www.w3.org/2000/svg', tag); - for (var k in attrs) - if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); - return el; - } - - var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); - svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); - _el.appendChild(svg); - - svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); - - for (var row = 0; row < nCount; row++) { - for (var col = 0; col < nCount; col++) { - if (oQRCode.isDark(row, col)) { - var child = makeSVG("use", {"x": String(row), "y": String(col)}); - child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") - svg.appendChild(child); - } - } - } - }; - Drawing.prototype.clear = function () { - while (this._el.hasChildNodes()) - this._el.removeChild(this._el.lastChild); - }; - return Drawing; - })(); - - var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; - - // Drawing in DOM by using Table tag - var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { - var Drawing = function (el, htOption) { - this._el = el; - this._htOption = htOption; - }; - - /** - * Draw the QRCode - * - * @param {QRCode} oQRCode - */ - Drawing.prototype.draw = function (oQRCode) { - var _htOption = this._htOption; - var _el = this._el; - var nCount = oQRCode.getModuleCount(); - var nWidth = Math.floor(_htOption.width / nCount); - var nHeight = Math.floor(_htOption.height / nCount); - var aHTML = ['']; - - for (var row = 0; row < nCount; row++) { - aHTML.push(''); - - for (var col = 0; col < nCount; col++) { - aHTML.push(''); - } - - aHTML.push(''); - } - - aHTML.push('
'); - _el.innerHTML = aHTML.join(''); - - // Fix the margin values as real size. - var elTable = _el.childNodes[0]; - var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; - var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; - - if (nLeftMarginTable > 0 && nTopMarginTable > 0) { - elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; - } - }; - - /** - * Clear the QRCode - */ - Drawing.prototype.clear = function () { - this._el.innerHTML = ''; - }; - - return Drawing; - })() : (function () { // Drawing in Canvas - function _onMakeImage() { - this._elImage.src = this._elCanvas.toDataURL("image/png"); - this._elImage.style.display = "block"; - this._elCanvas.style.display = "none"; - } - - // Android 2.1 bug workaround - // http://code.google.com/p/android/issues/detail?id=5141 - if (this._android && this._android <= 2.1) { - var factor = 1 / window.devicePixelRatio; - var drawImage = CanvasRenderingContext2D.prototype.drawImage; - CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { - if (("nodeName" in image) && /img/i.test(image.nodeName)) { - for (var i = arguments.length - 1; i >= 1; i--) { - arguments[i] = arguments[i] * factor; - } - } else if (typeof dw == "undefined") { - arguments[1] *= factor; - arguments[2] *= factor; - arguments[3] *= factor; - arguments[4] *= factor; - } - - drawImage.apply(this, arguments); - }; - } - - /** - * Check whether the user's browser supports Data URI or not - * - * @private - * @param {Function} fSuccess Occurs if it supports Data URI - * @param {Function} fFail Occurs if it doesn't support Data URI - */ - function _safeSetDataURI(fSuccess, fFail) { - var self = this; - self._fFail = fFail; - self._fSuccess = fSuccess; - - // Check it just once - if (self._bSupportDataURI === null) { - var el = document.createElement("img"); - var fOnError = function() { - self._bSupportDataURI = false; - - if (self._fFail) { - _fFail.call(self); - } - }; - var fOnSuccess = function() { - self._bSupportDataURI = true; - - if (self._fSuccess) { - self._fSuccess.call(self); - } - }; - - el.onabort = fOnError; - el.onerror = fOnError; - el.onload = fOnSuccess; - el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. - return; - } else if (self._bSupportDataURI === true && self._fSuccess) { - self._fSuccess.call(self); - } else if (self._bSupportDataURI === false && self._fFail) { - self._fFail.call(self); - } - }; - - /** - * Drawing QRCode by using canvas - * - * @constructor - * @param {HTMLElement} el - * @param {Object} htOption QRCode Options - */ - var Drawing = function (el, htOption) { - this._bIsPainted = false; - this._android = _getAndroid(); - - this._htOption = htOption; - this._elCanvas = document.createElement("canvas"); - this._elCanvas.width = htOption.width; - this._elCanvas.height = htOption.height; - el.appendChild(this._elCanvas); - this._el = el; - this._oContext = this._elCanvas.getContext("2d"); - this._bIsPainted = false; - this._elImage = document.createElement("img"); - this._elImage.alt = "Scan me!"; - this._elImage.style.display = "none"; - this._el.appendChild(this._elImage); - this._bSupportDataURI = null; - }; - - /** - * Draw the QRCode - * - * @param {QRCode} oQRCode - */ - Drawing.prototype.draw = function (oQRCode) { - var _elImage = this._elImage; - var _oContext = this._oContext; - var _htOption = this._htOption; - - var nCount = oQRCode.getModuleCount(); - var nWidth = _htOption.width / nCount; - var nHeight = _htOption.height / nCount; - var nRoundedWidth = Math.round(nWidth); - var nRoundedHeight = Math.round(nHeight); - - _elImage.style.display = "none"; - this.clear(); - - for (var row = 0; row < nCount; row++) { - for (var col = 0; col < nCount; col++) { - var bIsDark = oQRCode.isDark(row, col); - var nLeft = col * nWidth; - var nTop = row * nHeight; - _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; - _oContext.lineWidth = 1; - _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; - _oContext.fillRect(nLeft, nTop, nWidth, nHeight); - - // 안티 앨리어싱 방지 처리 - _oContext.strokeRect( - Math.floor(nLeft) + 0.5, - Math.floor(nTop) + 0.5, - nRoundedWidth, - nRoundedHeight - ); - - _oContext.strokeRect( - Math.ceil(nLeft) - 0.5, - Math.ceil(nTop) - 0.5, - nRoundedWidth, - nRoundedHeight - ); - } - } - - this._bIsPainted = true; - }; - - /** - * Make the image from Canvas if the browser supports Data URI. - */ - Drawing.prototype.makeImage = function () { - if (this._bIsPainted) { - _safeSetDataURI.call(this, _onMakeImage); - } - }; - - /** - * Return whether the QRCode is painted or not - * - * @return {Boolean} - */ - Drawing.prototype.isPainted = function () { - return this._bIsPainted; - }; - - /** - * Clear the QRCode - */ - Drawing.prototype.clear = function () { - this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); - this._bIsPainted = false; - }; - - /** - * @private - * @param {Number} nNumber - */ - Drawing.prototype.round = function (nNumber) { - if (!nNumber) { - return nNumber; - } - - return Math.floor(nNumber * 1000) / 1000; - }; - - return Drawing; - })(); - - /** - * Get the type by string length - * - * @private - * @param {String} sText - * @param {Number} nCorrectLevel - * @return {Number} type - */ - function _getTypeNumber(sText, nCorrectLevel) { - var nType = 1; - var length = _getUTF8Length(sText); - - for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { - var nLimit = 0; - - switch (nCorrectLevel) { - case QRErrorCorrectLevel.L : - nLimit = QRCodeLimitLength[i][0]; - break; - case QRErrorCorrectLevel.M : - nLimit = QRCodeLimitLength[i][1]; - break; - case QRErrorCorrectLevel.Q : - nLimit = QRCodeLimitLength[i][2]; - break; - case QRErrorCorrectLevel.H : - nLimit = QRCodeLimitLength[i][3]; - break; - } - - if (length <= nLimit) { - break; - } else { - nType++; - } - } - - if (nType > QRCodeLimitLength.length) { - throw new Error("Too long data"); - } - - return nType; - } - - function _getUTF8Length(sText) { - var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); - return replacedText.length + (replacedText.length != sText ? 3 : 0); - } - - /** - * @class QRCode - * @constructor - * @example - * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); - * - * @example - * var oQRCode = new QRCode("test", { - * text : "http://naver.com", - * width : 128, - * height : 128 - * }); - * - * oQRCode.clear(); // Clear the QRCode. - * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. - * - * @param {HTMLElement|String} el target element or 'id' attribute of element. - * @param {Object|String} vOption - * @param {String} vOption.text QRCode link data - * @param {Number} [vOption.width=256] - * @param {Number} [vOption.height=256] - * @param {String} [vOption.colorDark="#000000"] - * @param {String} [vOption.colorLight="#ffffff"] - * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] - */ - QRCode = function (el, vOption) { - this._htOption = { - width : 256, - height : 256, - typeNumber : 4, - colorDark : "#000000", - colorLight : "#ffffff", - correctLevel : QRErrorCorrectLevel.H - }; - - if (typeof vOption === 'string') { - vOption = { - text : vOption - }; - } - - // Overwrites options - if (vOption) { - for (var i in vOption) { - this._htOption[i] = vOption[i]; - } - } - - if (typeof el == "string") { - el = document.getElementById(el); - } - - this._android = _getAndroid(); - this._el = el; - this._oQRCode = null; - this._oDrawing = new Drawing(this._el, this._htOption); - - if (this._htOption.text) { - this.makeCode(this._htOption.text); - } - }; - - /** - * Make the QRCode - * - * @param {String} sText link data - */ - QRCode.prototype.makeCode = function (sText) { - this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); - this._oQRCode.addData(sText); - this._oQRCode.make(); - this._el.title = sText; - this._oDrawing.draw(this._oQRCode); - this.makeImage(); - }; - - /** - * Make the Image from Canvas element - * - It occurs automatically - * - Android below 3 doesn't support Data-URI spec. - * - * @private - */ - QRCode.prototype.makeImage = function () { - if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { - this._oDrawing.makeImage(); - } - }; - - /** - * Clear the QRCode - */ - QRCode.prototype.clear = function () { - this._oDrawing.clear(); - }; - - /** - * @name QRCode.CorrectLevel - */ - QRCode.CorrectLevel = QRErrorCorrectLevel; -})(); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index a694bbb16d..b69a585242 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -21,24 +21,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-13T21:24:40.667Z" }, - { - "rule": "DOM-innerHTML", - "path": "components/qrcode/qrcode.js", - "line": "\t\t\t_el.innerHTML = aHTML.join('');", - "lineNumber": 254, - "reasonCategory": "usageTrusted", - "updated": "2018-09-18T19:19:27.699Z", - "reasonDetail": "Very limited in what HTML can be injected - dark/light options specify colors for the light/dark parts of QRCode" - }, - { - "rule": "DOM-innerHTML", - "path": "components/qrcode/qrcode.js", - "line": "\t\t\tthis._el.innerHTML = '';", - "lineNumber": 270, - "reasonCategory": "usageTrusted", - "updated": "2018-09-15T00:38:04.183Z", - "reasonDetail": "Hard-coded string" - }, { "rule": "jQuery-$(", "path": "js/about_start.js", From e466062f15b266d4904ffffb3863bc89c9587043 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 23 Feb 2021 14:22:01 +1100 Subject: [PATCH 057/109] WIP --- libtextsecure/storage/user.js | 12 ++++++++++++ preload.js | 3 +++ ts/components/session/RegistrationTabs.tsx | 16 ++++++++++++++-- ts/receiver/contentMessage.ts | 8 +++++--- ts/session/utils/User.ts | 15 +++++++++++++++ ts/session/utils/syncUtils.ts | 5 +++-- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/libtextsecure/storage/user.js b/libtextsecure/storage/user.js index 64e0cac7d8..091f1a805f 100644 --- a/libtextsecure/storage/user.js +++ b/libtextsecure/storage/user.js @@ -24,6 +24,18 @@ return textsecure.utils.unencodeNumber(numberId)[0]; }, + isRestoringFromSeed() { + const isRestoring = textsecure.storage.get('is_restoring_from_seed'); + if (isRestoring === undefined) { + return false; + } + return isRestoring; + }, + + setRestoringFromSeed(isRestoringFromSeed) { + textsecure.storage.put('is_restoring_from_seed', isRestoringFromSeed); + }, + getDeviceId() { const numberId = textsecure.storage.get('number_id'); if (numberId === undefined) { diff --git a/preload.js b/preload.js index 6718aff28d..88671d9dbf 100644 --- a/preload.js +++ b/preload.js @@ -517,9 +517,12 @@ window.deleteAccount = async reason => { try { window.log.info('DeleteAccount => Sending a last SyncConfiguration'); // be sure to wait for the message being effectively sent. Otherwise we won't be able to encrypt it for our devices ! + window.log.info('Sending one last configuration message.') await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded( true ); + window.log.info('Last configuration message sent!') + await syncedMessageSent(); } catch (error) { window.log.error( diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index cf739629e2..6b7a128e0c 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -10,7 +10,7 @@ import { import { trigger } from '../../shims/events'; import { SessionHtmlRenderer } from './SessionHTMLRenderer'; import { SessionIdEditable } from './SessionIdEditable'; -import { StringUtils, ToastUtils } from '../../session/utils'; +import { StringUtils, ToastUtils, UserUtils } from '../../session/utils'; import { lightTheme } from '../../state/ducks/SessionTheme'; import { ConversationController } from '../../session/conversations'; import { PasswordUtil } from '../../util'; @@ -702,12 +702,24 @@ export class RegistrationTabs extends React.Component { await this.resetRegistration(); await window.setPassword(password); + const isRestoringFromSeed = signInMode === SignInMode.UsingRecoveryPhrase; + UserUtils.setRestoringFromSeed(isRestoringFromSeed); + await this.accountManager.registerSingleDevice( seedToUse, language, trimName ); - trigger('openInbox'); + // if we are just creating a new account, no need to wait for a configuration message + if (!isRestoringFromSeed) { + trigger('openInbox'); + } else { + // We have to pull for all messages of the user of this menmonic + // We are looking for the most recent ConfigurationMessage he sent to himself. + // When we find it, we can just get the displayName, avatar and groups saved in it. + // If we do not find one, we will need to ask for a display name. + window.log.warn('isRestoringFromSeed'); + } } catch (e) { ToastUtils.pushToastError( 'registrationError', diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 8e621dd57b..6a2d2377c2 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -390,7 +390,7 @@ export async function innerHandleContentMessage( 'private' ); - if (content.dataMessage) { + if (content.dataMessage && !UserUtils.isRestoringFromSeed()) { if ( content.dataMessage.profileKey && content.dataMessage.profileKey.length === 0 @@ -401,15 +401,16 @@ export async function innerHandleContentMessage( return; } - if (content.receiptMessage) { + if (content.receiptMessage && !UserUtils.isRestoringFromSeed()) { await handleReceiptMessage(envelope, content.receiptMessage); return; } - if (content.typingMessage) { + if (content.typingMessage && !UserUtils.isRestoringFromSeed()) { await handleTypingMessage(envelope, content.typingMessage); return; } + // Be sure to check for the UserUtils.isRestoringFromSeed() if you add another if here if (content.configurationMessage) { await handleConfigurationMessage( envelope, @@ -417,6 +418,7 @@ export async function innerHandleContentMessage( ); return; } + // Be sure to check for the UserUtils.isRestoringFromSeed() if you add another if here } catch (e) { window.log.warn(e); } diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index d71a51d319..cf418e146e 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -69,3 +69,18 @@ export async function getUserED25519KeyPair(): Promise { } return undefined; } + +/** + * Returns the public key of this current device as a STRING, or throws an error + */ +export function isRestoringFromSeed(): boolean { + const ourNumber = window.textsecure.storage.user.isRestoringFromSeed(); + if (!ourNumber) { + throw new Error('ourNumber is not set'); + } + return ourNumber; +} + +export function setRestoringFromSeed(isRestoring: boolean) { + window.textsecure.storage.user.setRestoringFromSeed(isRestoring); +} diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index e9ab76191a..7d3cb17f7f 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -49,6 +49,8 @@ export const forceSyncConfigurationNowIfNeeded = async ( async function waitForMessageSentEvent(message: RawMessage) { return new Promise(resolve => { if (message.identifier === configMessage.identifier) { + // might have fail in fact + debugger; resolve(true); } }); @@ -61,10 +63,9 @@ export const forceSyncConfigurationNowIfNeeded = async ( configMessage, waitForMessageSentEvent as any ); - return Promise.resolve(); + return waitForMessageSentEvent; } else { await getMessageQueue().sendSyncMessage(configMessage); - return waitForMessageSentEvent; } } catch (e) { window.log.warn( From 01085244bdab7dd46277b22641db048fce510fe5 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 26 Feb 2021 11:27:29 +1100 Subject: [PATCH 058/109] split up registration signup tab logic to sub components --- _locales/de/messages.json | 5 +- _locales/en/messages.json | 6 +- _locales/es/messages.json | 5 +- _locales/fr/messages.json | 5 +- _locales/id/messages.json | 5 +- _locales/it/messages.json | 5 +- _locales/ja/messages.json | 5 +- _locales/pl/messages.json | 5 +- _locales/pt_BR/messages.json | 5 +- _locales/ru/messages.json | 5 +- _locales/vi/messages.json | 5 +- ts/components/EditProfileDialog.tsx | 2 +- .../session/SessionRegistrationView.tsx | 2 +- .../{ => registration}/RegistrationTabs.tsx | 476 +++++------------- .../registration/RegistrationUserDetails.tsx | 137 +++++ .../session/registration/SignInTab.tsx | 13 + .../session/registration/SignUpTab.tsx | 139 +++++ .../session/registration/TabLabel.tsx | 42 ++ .../registration/TermsAndConditions.tsx | 10 + ts/session/utils/User.ts | 9 +- 20 files changed, 482 insertions(+), 404 deletions(-) rename ts/components/session/{ => registration}/RegistrationTabs.tsx (50%) create mode 100644 ts/components/session/registration/RegistrationUserDetails.tsx create mode 100644 ts/components/session/registration/SignInTab.tsx create mode 100644 ts/components/session/registration/SignUpTab.tsx create mode 100644 ts/components/session/registration/TabLabel.tsx create mode 100644 ts/components/session/registration/TermsAndConditions.tsx diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 1774c694fa..04982e3156 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -508,7 +508,7 @@ "welcomeToYourSession": { "message": "Willkommen bei Session" }, - "generateSessionID": { + "createSessionID": { "message": "Session ID erstellen" }, "yourUniqueSessionID": { @@ -1410,9 +1410,6 @@ "enterDisplayName": { "message": "Geben Sie einen Anzeigenamen ein" }, - "enterSessionIDHere": { - "message": "Geben Sie Ihre Session ID ein." - }, "continueYourSession": { "message": "Ihre Session fortsetzen" }, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d52d18610e..37eec6bf81 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2008,7 +2008,7 @@ "getStarted": { "message": "Get started" }, - "generateSessionID": { + "createSessionID": { "message": "Create Session ID", "androidKey": "activity_landing_register_button_title" }, @@ -2050,10 +2050,6 @@ "devicePairingHeader_Step4": { "message": "Enter your Session ID below to link this device to your Session ID." }, - "enterSessionIDHere": { - "message": "Enter your Session ID", - "androidKey": "fragment_enter_session_id_edit_text_hint" - }, "continueYourSession": { "message": "Continue Your Session", "androidKey": "activity_landing_restore_button_title" diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 07022ba9c6..bf24c35015 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "Tu ID de Session es la dirección única que las personas pueden usar para contactarte en Session. Por diseño, tu ID de Session es totalmente anónima y privada, sin vínculo con tu identidad real." }, - "generateSessionID": { + "createSessionID": { "message": "Crear ID de Session" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "Ingresa un nombre para mostrar" }, - "enterSessionIDHere": { - "message": "Ingresa tu ID de Session" - }, "continueYourSession": { "message": "Continúa tu Session" }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 1fad04b32f..f12bb793db 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "Votre Session ID est l'identifiant unique que les gens utilisent pour vous contacter dans Session. Sans lien avec votre identité réelle, votre Session ID est complètement anonyme et privé." }, - "generateSessionID": { + "createSessionID": { "message": "Créer un Session ID" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "Saisissez un nom d'utilisateur" }, - "enterSessionIDHere": { - "message": "Saisissez votre Session ID" - }, "continueYourSession": { "message": "Continuez votre Session" }, diff --git a/_locales/id/messages.json b/_locales/id/messages.json index b1fae34659..ad190ae564 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -1302,7 +1302,7 @@ "allUsersAreRandomly...": { "message": "Session ID adalah alamat unik yang bisa digunakan untuk mengontak anda. Tanpa koneksi dengan identitas asli, Session ID anda didesain bersifat anonim dan rahasia." }, - "generateSessionID": { + "createSessionID": { "message": "Buat Session ID" }, "recoveryPhrase": { @@ -1314,9 +1314,6 @@ "enterDisplayName": { "message": "Masukkan nama" }, - "enterSessionIDHere": { - "message": "Masuk ke Session ID" - }, "continueYourSession": { "message": "Lanjutkan Session" }, diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 16aec837d3..79d4176ed5 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "La Sessione ID è l'indirizzo univoco che le persone possono utilizzare per contattarti su una Sessione. Senza alcuna connessione con la tua vera identità, la Sessione ID è totalmente anonimo e privato fin dal incezione." }, - "generateSessionID": { + "createSessionID": { "message": "Crea Sessione ID" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "Inserisci il nome da visualizzare" }, - "enterSessionIDHere": { - "message": "Inserisci la Sessione ID" - }, "continueYourSession": { "message": "Continua la Sessione" }, diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 0aa0ce8be2..9510b283e3 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "Session ID は、Session で連絡を取るために使用できる一意のアドレスです。本当のアイデンティティに関係なく、あなたの Session ID は設計上完全に匿名でプライベートです。" }, - "generateSessionID": { + "createSessionID": { "message": "Session ID を作成する" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "表示名を入力してください" }, - "enterSessionIDHere": { - "message": "Session ID を入力してください" - }, "continueYourSession": { "message": "Session を続ける" }, diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 81da95a5d2..760cb7d73b 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "Twój identyfikator Session to unikalny adres, za pomocą którego można się z Tobą kontaktować w Sesji. Bez połączenia z twoją prawdziwą tożsamością, identyfikator Session jest z założenia całkowicie anonimowy i prywatny." }, - "generateSessionID": { + "createSessionID": { "message": "Utwórz identyfikator Session" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "Wprowadź wyświetlaną nazwe" }, - "enterSessionIDHere": { - "message": "Wpisz swój identyfikator Session" - }, "continueYourSession": { "message": "Kontynuuj swoją sesję" }, diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index 47619cea5b..3cbce99ab6 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "Seu ID Session é o endereço exclusivo que as pessoas podem usar para entrar em contato com você no Session. Sem conexão com sua identidade real, seu ID Session é totalmente anônimo e privado por definição." }, - "generateSessionID": { + "createSessionID": { "message": "Criar ID Session" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "Digite um nome de exibição" }, - "enterSessionIDHere": { - "message": "Digite seu ID Session" - }, "continueYourSession": { "message": "Continuar com seu Session" }, diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index a2bd8c18a9..be2be9403b 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1308,7 +1308,7 @@ "allUsersAreRandomly...": { "message": "Ваш Session ID - это уникальный адрес, который другие пользователи могут использовать для связи с вами при помощи Session. Поскольку ваш Session ID никак не связан с вашей настоящей личностью, он по определению является полностью анонимным и конфиденциальным." }, - "generateSessionID": { + "createSessionID": { "message": "Создать Session ID" }, "recoveryPhrase": { @@ -1320,9 +1320,6 @@ "enterDisplayName": { "message": "Введите отображаемое имя" }, - "enterSessionIDHere": { - "message": "Введите свой Session ID" - }, "continueYourSession": { "message": "Восстановить Session ID" }, diff --git a/_locales/vi/messages.json b/_locales/vi/messages.json index 007b471640..b51f3bdea7 100644 --- a/_locales/vi/messages.json +++ b/_locales/vi/messages.json @@ -1272,7 +1272,7 @@ "allUsersAreRandomly...": { "message": "Session ID của bạn là địa chỉ duy nhất mà mọi người có thể dùng để liên lạc với bạn trên ứng dụng Session. Session ID của bạn được thiết kế đảm bảo tuyệt đối ẩn danh và riêng tư vì nó không liên kết với danh tính thật của bạn." }, - "generateSessionID": { + "createSessionID": { "message": "Tạo Session ID" }, "recoveryPhrase": { @@ -1284,9 +1284,6 @@ "enterDisplayName": { "message": "Nhập một tên hiển thị" }, - "enterSessionIDHere": { - "message": "Nhập Session ID của bạn" - }, "continueYourSession": { "message": "Tiếp tục Session của bạn" }, diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index 6dc69c7a20..328d267927 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -19,7 +19,7 @@ import { SessionModal } from './session/SessionModal'; import { PillDivider } from './session/PillDivider'; import { ToastUtils, UserUtils } from '../session/utils'; import { DefaultTheme } from 'styled-components'; -import { MAX_USERNAME_LENGTH } from './session/RegistrationTabs'; +import { MAX_USERNAME_LENGTH } from './session/registration/RegistrationTabs'; interface Props { i18n: any; diff --git a/ts/components/session/SessionRegistrationView.tsx b/ts/components/session/SessionRegistrationView.tsx index f1bfe47a36..186061762d 100644 --- a/ts/components/session/SessionRegistrationView.tsx +++ b/ts/components/session/SessionRegistrationView.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AccentText } from './AccentText'; -import { RegistrationTabs } from './RegistrationTabs'; +import { RegistrationTabs } from './registration/RegistrationTabs'; import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; import { SessionToastContainer } from './SessionToastContainer'; import { lightTheme, SessionTheme } from '../../state/ducks/SessionTheme'; diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/registration/RegistrationTabs.tsx similarity index 50% rename from ts/components/session/RegistrationTabs.tsx rename to ts/components/session/registration/RegistrationTabs.tsx index 6b7a128e0c..6238a508e9 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/registration/RegistrationTabs.tsx @@ -1,39 +1,23 @@ import React from 'react'; -import classNames from 'classnames'; -import { SessionInput } from './SessionInput'; import { SessionButton, SessionButtonColor, SessionButtonType, -} from './SessionButton'; -import { trigger } from '../../shims/events'; -import { SessionHtmlRenderer } from './SessionHTMLRenderer'; -import { SessionIdEditable } from './SessionIdEditable'; -import { StringUtils, ToastUtils, UserUtils } from '../../session/utils'; -import { lightTheme } from '../../state/ducks/SessionTheme'; -import { ConversationController } from '../../session/conversations'; -import { PasswordUtil } from '../../util'; -import { removeAll } from '../../data/data'; +} from '../SessionButton'; +import { trigger } from '../../../shims/events'; +import { StringUtils, ToastUtils, UserUtils } from '../../../session/utils'; +import { ConversationController } from '../../../session/conversations'; +import { PasswordUtil } from '../../../util'; +import { removeAll } from '../../../data/data'; +import { SignUpMode, SignUpTab } from './SignUpTab'; +import { SignInMode } from './SignInTab'; +import { RegistrationUserDetails } from './RegistrationUserDetails'; +import { TermsAndConditions } from './TermsAndConditions'; +import { TabLabel, TabType } from './TabLabel'; export const MAX_USERNAME_LENGTH = 20; -enum SignInMode { - Default, - UsingRecoveryPhrase, -} - -enum SignUpMode { - Default, - SessionIDShown, - EnterDetails, -} - -enum TabType { - Create, - SignIn, -} - interface State { selectedTab: TabType; signInMode: SignInMode; @@ -47,46 +31,11 @@ interface State { recoveryPhrase: string; generatedRecoveryPhrase: string; hexGeneratedPubKey: string; - primaryDevicePubKey: string; mnemonicError: string | undefined; displayNameError: string | undefined; - loading: boolean; } -const Tab = ({ - isSelected, - label, - onSelect, - type, -}: { - isSelected: boolean; - label: string; - onSelect?: (event: TabType) => void; - type: TabType; -}) => { - const handleClick = onSelect - ? () => { - onSelect(type); - } - : undefined; - - return ( -
- {label} -
- ); -}; - export class RegistrationTabs extends React.Component { - private readonly accountManager: any; - constructor(props: any) { super(props); @@ -94,21 +43,14 @@ export class RegistrationTabs extends React.Component { this.onDisplayNameChanged = this.onDisplayNameChanged.bind(this); this.onPasswordChanged = this.onPasswordChanged.bind(this); this.onPasswordVerifyChanged = this.onPasswordVerifyChanged.bind(this); - this.onSignUpGenerateSessionIDClick = this.onSignUpGenerateSessionIDClick.bind( - this - ); - this.onSignUpGetStartedClick = this.onSignUpGetStartedClick.bind(this); - this.onSecondDeviceSessionIDChanged = this.onSecondDeviceSessionIDChanged.bind( - this - ); - this.onCompleteSignUpClick = this.onCompleteSignUpClick.bind(this); this.handlePressEnter = this.handlePressEnter.bind(this); this.handleContinueYourSessionClick = this.handleContinueYourSessionClick.bind( this ); + this.onCompleteSignUpClick = this.onCompleteSignUpClick.bind(this); this.state = { - selectedTab: TabType.Create, + selectedTab: TabType.SignUp, signInMode: SignInMode.Default, signUpMode: SignUpMode.Default, secretWords: undefined, @@ -120,14 +62,9 @@ export class RegistrationTabs extends React.Component { recoveryPhrase: '', generatedRecoveryPhrase: '', hexGeneratedPubKey: '', - primaryDevicePubKey: '', mnemonicError: undefined, displayNameError: undefined, - loading: false, }; - - this.accountManager = window.getAccountManager(); - // Clean status in case the app closed unexpectedly } public componentDidMount() { @@ -137,25 +74,19 @@ export class RegistrationTabs extends React.Component { public render() { const { selectedTab } = this.state; - - const createAccount = window.i18n('createAccount'); - const signIn = window.i18n('signIn'); - const isCreateSelected = selectedTab === TabType.Create; - const isSignInSelected = selectedTab === TabType.SignIn; + // tslint:disable: use-simple-attributes return (
- -
@@ -167,7 +98,9 @@ export class RegistrationTabs extends React.Component { private async generateMnemonicAndKeyPair() { if (this.state.generatedRecoveryPhrase === '') { const language = 'english'; - const mnemonic = await this.accountManager.generateMnemonic(language); + const mnemonic = await window + .getAccountManager() + .generateMnemonic(language); let seedHex = window.mnemonic.mn_decode(mnemonic, language); // handle shorter than 32 bytes seeds @@ -201,7 +134,6 @@ export class RegistrationTabs extends React.Component { passwordErrorString: '', passwordFieldsMatch: false, recoveryPhrase: '', - primaryDevicePubKey: '', mnemonicError: undefined, displayNameError: undefined, }); @@ -239,143 +171,54 @@ export class RegistrationTabs extends React.Component { private renderSections() { const { selectedTab } = this.state; - if (selectedTab === TabType.Create) { - return this.renderSignUp(); + if (selectedTab === TabType.SignUp) { + return ( + { + this.setState({ + signUpMode: SignUpMode.EnterDetails, + }); + }} + createSessionID={() => { + this.setState( + { + signUpMode: SignUpMode.SessionIDShown, + }, + () => { + window.Session.setNewSessionID(this.state.hexGeneratedPubKey); + } + ); + }} + onCompleteSignUpClick={this.onCompleteSignUpClick} + displayName={this.state.displayName} + password={this.state.password} + passwordErrorString={this.state.passwordErrorString} + passwordFieldsMatch={this.state.passwordFieldsMatch} + displayNameError={this.state.displayNameError} + recoveryPhrase={this.state.recoveryPhrase} + onPasswordVerifyChanged={this.onPasswordVerifyChanged} + handlePressEnter={this.handlePressEnter} + onPasswordChanged={this.onPasswordChanged} + onDisplayNameChanged={this.onDisplayNameChanged} + onSeedChanged={this.onSeedChanged} + /> + ); } return this.renderSignIn(); } - private renderSignUp() { - const { signUpMode } = this.state; - switch (signUpMode) { - case SignUpMode.Default: - return ( -
- {this.renderSignUpHeader()} - {this.renderSignUpButton()} -
- ); - case SignUpMode.SessionIDShown: - return ( -
- {this.renderSignUpHeader()} -
- {window.i18n('yourUniqueSessionID')} -
- {this.renderEnterSessionID(false)} - {this.renderSignUpButton()} - {this.getRenderTermsConditionAgreement()} -
- ); - - default: - const { - passwordErrorString, - passwordFieldsMatch, - displayNameError, - displayName, - password, - } = this.state; - - let enableCompleteSignUp = true; - const displayNameOK = !displayNameError && !!displayName; //display name required - const passwordsOK = - !password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching - - enableCompleteSignUp = displayNameOK && passwordsOK; - - return ( -
-
- {window.i18n('welcomeToYourSession')} -
- - {this.renderRegistrationContent()} - -
- ); - } - } - private getRenderTermsConditionAgreement() { - const { selectedTab, signInMode, signUpMode } = this.state; - if (selectedTab === TabType.Create) { - return signUpMode !== SignUpMode.Default - ? this.renderTermsConditionAgreement() - : null; - } else { - return signInMode !== SignInMode.Default - ? this.renderTermsConditionAgreement() - : null; + const { selectedTab, signInMode } = this.state; + if (selectedTab !== TabType.SignUp) { + return signInMode !== SignInMode.Default ? : null; } - } - - private renderSignUpHeader() { - const allUsersAreRandomly = window.i18n('allUsersAreRandomly...'); - - return ( -
{allUsersAreRandomly}
- ); - } - - private renderSignUpButton() { - const { signUpMode } = this.state; - - let buttonType: SessionButtonType; - let buttonColor: SessionButtonColor; - let buttonText: string; - if (signUpMode !== SignUpMode.Default) { - buttonType = SessionButtonType.Brand; - buttonColor = SessionButtonColor.Green; - buttonText = window.i18n('continue'); - } else { - buttonType = SessionButtonType.BrandOutline; - buttonColor = SessionButtonColor.Green; - buttonText = window.i18n('generateSessionID'); - } - - return ( - { - if (signUpMode === SignUpMode.Default) { - this.onSignUpGenerateSessionIDClick(); - } else { - this.onSignUpGetStartedClick(); - } - }} - buttonType={buttonType} - buttonColor={buttonColor} - text={buttonText} - /> - ); - } - - private onSignUpGenerateSessionIDClick() { - this.setState( - { - signUpMode: SignUpMode.SessionIDShown, - }, - () => { - window.Session.setNewSessionID(this.state.hexGeneratedPubKey); - } - ); - } - - private onSignUpGetStartedClick() { - this.setState({ - signUpMode: SignUpMode.EnterDetails, - }); + return <>; } private onCompleteSignUpClick() { - void this.register('english'); + void this.register(); } private renderSignIn() { @@ -390,133 +233,60 @@ export class RegistrationTabs extends React.Component { } private renderRegistrationContent() { - const { signInMode, signUpMode } = this.state; + const { signInMode } = this.state; - if (signInMode === SignInMode.UsingRecoveryPhrase) { - return ( -
- { - this.onSeedChanged(val); - }} - onEnterPressed={() => { - this.handlePressEnter(); - }} - theme={lightTheme} + const isSignInNotDefault = signInMode !== SignInMode.Default; + + if (isSignInNotDefault) { + const sharedProps = { + displayName: this.state.displayName, + handlePressEnter: this.handlePressEnter, + onDisplayNameChanged: this.onDisplayNameChanged, + onPasswordChanged: this.onPasswordChanged, + onPasswordVerifyChanged: this.onPasswordVerifyChanged, + onSeedChanged: this.onSeedChanged, + password: this.state.password, + passwordErrorString: this.state.passwordErrorString, + passwordFieldsMatch: this.state.passwordFieldsMatch, + recoveryPhrase: this.state.recoveryPhrase, + stealAutoFocus: true, + }; + + if (signInMode === SignInMode.UsingRecoveryPhrase) { + return ( + - {this.renderNamePasswordAndVerifyPasswordFields(false)} -
- ); - } + ); + } - if (signUpMode === SignUpMode.EnterDetails) { - return ( -
- {this.renderNamePasswordAndVerifyPasswordFields(true)} -
- ); + if (signInMode === SignInMode.LinkDevice) { + return ( + + ); + } } return null; } - private renderNamePasswordAndVerifyPasswordFields( - stealAutoFocus: boolean = false - ) { - const { password, passwordFieldsMatch } = this.state; - const passwordsDoNotMatch = - !passwordFieldsMatch && this.state.password - ? window.i18n('passwordsDoNotMatch') - : undefined; - - return ( -
- { - this.onDisplayNameChanged(val); - }} - onEnterPressed={() => { - this.handlePressEnter(); - }} - theme={lightTheme} - /> - - { - this.onPasswordChanged(val); - }} - onEnterPressed={() => { - this.handlePressEnter(); - }} - theme={lightTheme} - /> - - {!!password && ( - { - this.onPasswordVerifyChanged(val); - }} - onEnterPressed={() => { - this.handlePressEnter(); - }} - theme={lightTheme} - /> - )} -
- ); - } - - private renderEnterSessionID(contentEditable: boolean) { - const enterSessionIDHere = window.i18n('enterSessionIDHere'); - - return ( - { - this.onSecondDeviceSessionIDChanged(value); - }} - /> - ); - } - - private onSecondDeviceSessionIDChanged(value: string) { - this.setState({ - primaryDevicePubKey: value, - }); - } - private renderSignInButtons() { const { signInMode } = this.state; - // const or = window.i18n('or'); + const or = window.i18n('or'); if (signInMode === SignInMode.Default) { return (
- {this.renderRestoreUsingRecoveryPhraseButton( - SessionButtonType.BrandOutline, - SessionButtonColor.Green - )} + {this.renderRestoreUsingRecoveryPhraseButton()} +
{or}
+ {this.renderLinkDeviceButton()}
); } @@ -532,41 +302,51 @@ export class RegistrationTabs extends React.Component { } private renderTermsConditionAgreement() { - return ( -
- -
- ); + return ; } private handleContinueYourSessionClick() { if (this.state.signInMode === SignInMode.UsingRecoveryPhrase) { - void this.register('english'); + void this.register(); } } - private renderRestoreUsingRecoveryPhraseButton( - buttonType: SessionButtonType, - buttonColor: SessionButtonColor - ) { + private renderRestoreUsingRecoveryPhraseButton() { return ( { this.setState({ signInMode: SignInMode.UsingRecoveryPhrase, - primaryDevicePubKey: '', recoveryPhrase: '', displayName: '', signUpMode: SignUpMode.Default, }); }} - buttonType={buttonType} - buttonColor={buttonColor} + buttonType={SessionButtonType.BrandOutline} + buttonColor={SessionButtonColor.Green} text={window.i18n('restoreUsingRecoveryPhrase')} /> ); } + private renderLinkDeviceButton() { + return ( + { + this.setState({ + signInMode: SignInMode.LinkDevice, + recoveryPhrase: '', + displayName: '', + signUpMode: SignUpMode.Default, + }); + }} + buttonType={SessionButtonType.BrandOutline} + buttonColor={SessionButtonColor.Green} + text={window.i18n('linkDevice')} + /> + ); + } + private handlePressEnter() { const { signInMode, signUpMode } = this.state; if (signUpMode === SignUpMode.EnterDetails) { @@ -631,17 +411,17 @@ export class RegistrationTabs extends React.Component { private async resetRegistration() { await removeAll(); + await window.storage.reset(); await window.storage.fetch(); ConversationController.getInstance().reset(); await ConversationController.getInstance().load(); this.setState({ - loading: false, secretWords: undefined, }); } - private async register(language: string) { + private async register() { const { password, recoveryPhrase, @@ -705,11 +485,9 @@ export class RegistrationTabs extends React.Component { const isRestoringFromSeed = signInMode === SignInMode.UsingRecoveryPhrase; UserUtils.setRestoringFromSeed(isRestoringFromSeed); - await this.accountManager.registerSingleDevice( - seedToUse, - language, - trimName - ); + await window + .getAccountManager() + .registerSingleDevice(seedToUse, 'english', trimName); // if we are just creating a new account, no need to wait for a configuration message if (!isRestoringFromSeed) { trigger('openInbox'); diff --git a/ts/components/session/registration/RegistrationUserDetails.tsx b/ts/components/session/registration/RegistrationUserDetails.tsx new file mode 100644 index 0000000000..9b90475c3c --- /dev/null +++ b/ts/components/session/registration/RegistrationUserDetails.tsx @@ -0,0 +1,137 @@ +import classNames from 'classnames'; +import React from 'react'; +import { lightTheme } from '../../../state/ducks/SessionTheme'; +import { SessionInput } from '../SessionInput'; +import { MAX_USERNAME_LENGTH } from './RegistrationTabs'; + +const DisplayNameInput = (props: { + stealAutoFocus?: boolean; + displayName: string; + onDisplayNameChanged: (val: string) => any; + handlePressEnter: () => any; +}) => { + return ( + // tslint:disable-next-line: use-simple-attributes + + ); +}; + +const RecoveryPhraseInput = (props: { + recoveryPhrase: string; + onSeedChanged: (val: string) => any; + handlePressEnter: () => any; +}) => { + return ( + + ); +}; + +const PasswordAndVerifyPasswordFields = (props: { + password: string; + passwordFieldsMatch: boolean; + passwordErrorString: string; + onPasswordChanged: (val: string) => any; + onPasswordVerifyChanged: (val: string) => any; + handlePressEnter: () => any; +}) => { + const { password, passwordFieldsMatch, passwordErrorString } = props; + const passwordsDoNotMatch = + !passwordFieldsMatch && password + ? window.i18n('passwordsDoNotMatch') + : undefined; + + return ( + <> + + + {!!password && ( + + )} + + ); +}; + +export interface Props { + // tslint:disable: react-unused-props-and-state + showDisplayNameField: boolean; + showSeedField: boolean; + stealAutoFocus?: boolean; + recoveryPhrase: string; + displayName: string; + password: string; + passwordErrorString: string; + passwordFieldsMatch: boolean; + handlePressEnter: () => any; + onSeedChanged: (val: string) => any; + onDisplayNameChanged: (val: string) => any; + onPasswordChanged: (val: string) => any; + onPasswordVerifyChanged: (val: string) => any; +} + +export const RegistrationUserDetails = (props: Props) => { + return ( +
+ {props.showSeedField && ( + + )} +
+ {props.showDisplayNameField && ( + + )} + +
+
+ ); +}; diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx new file mode 100644 index 0000000000..939d129fcc --- /dev/null +++ b/ts/components/session/registration/SignInTab.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export enum SignInMode { + Default, + UsingRecoveryPhrase, + LinkDevice, +} + +export interface Props { + signInMode: SignInMode; +} + +export const SignInTab = (props: Props) => {}; diff --git a/ts/components/session/registration/SignUpTab.tsx b/ts/components/session/registration/SignUpTab.tsx new file mode 100644 index 0000000000..cd80a572bb --- /dev/null +++ b/ts/components/session/registration/SignUpTab.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { + SessionButton, + SessionButtonColor, + SessionButtonType, +} from '../SessionButton'; +import { SessionIdEditable } from '../SessionIdEditable'; +import { RegistrationUserDetails } from './RegistrationUserDetails'; +import { TermsAndConditions } from './TermsAndConditions'; + +export enum SignUpMode { + Default, + SessionIDShown, + EnterDetails, +} + +export interface Props { + // tslint:disable: react-unused-props-and-state + signUpMode: SignUpMode; + continueSignUp: () => any; + createSessionID: () => any; + onCompleteSignUpClick: () => any; + passwordErrorString: string; + passwordFieldsMatch: boolean; + displayNameError?: string; + displayName: string; + password: string; + recoveryPhrase: string; + stealAutoFocus?: boolean; + handlePressEnter: () => any; + onSeedChanged: (val: string) => any; + onDisplayNameChanged: (val: string) => any; + onPasswordChanged: (val: string) => any; + onPasswordVerifyChanged: (val: string) => any; +} + +const CreateSessionIdButton = ({ + createSessionID, +}: { + createSessionID: any; +}) => { + return ( + + ); +}; + +const ContinueSignUpButton = ({ continueSignUp }: { continueSignUp: any }) => { + return ( + + ); +}; + +export const SignUpTab = (props: Props) => { + const { signUpMode, continueSignUp, createSessionID } = props; + + switch (signUpMode) { + case SignUpMode.Default: + const allUsersAreRandomly = window.i18n('allUsersAreRandomly...'); + + return ( +
+
{allUsersAreRandomly}
+ +
+ ); + + case SignUpMode.SessionIDShown: + return ( +
+
+ {window.i18n('yourUniqueSessionID')} +
+ + + + +
+ ); + + // can only be the EnterDetails step + default: + const { + passwordErrorString, + passwordFieldsMatch, + displayNameError, + displayName, + password, + } = props; + + let enableCompleteSignUp = true; + const displayNameOK = !displayNameError && !!displayName; //display name required + const passwordsOK = + !password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching + + enableCompleteSignUp = displayNameOK && passwordsOK; + + return ( +
+
+ {window.i18n('welcomeToYourSession')} +
+ + + + +
+ ); + } +}; diff --git a/ts/components/session/registration/TabLabel.tsx b/ts/components/session/registration/TabLabel.tsx new file mode 100644 index 0000000000..49855a0e1a --- /dev/null +++ b/ts/components/session/registration/TabLabel.tsx @@ -0,0 +1,42 @@ +import classNames from 'classnames'; +import React from 'react'; +import { SignUpMode, SignUpTab } from './SignUpTab'; + +export enum TabType { + SignUp, + SignIn, +} + +export const TabLabel = ({ + isSelected, + onSelect, + type, +}: { + isSelected: boolean; + onSelect?: (event: TabType) => void; + type: TabType; +}) => { + const handleClick = onSelect + ? () => { + onSelect(type); + } + : undefined; + + const label = + type === TabType.SignUp + ? window.i18n('createAccount') + : window.i18n('signIn'); + + return ( +
+ {label} +
+ ); +}; diff --git a/ts/components/session/registration/TermsAndConditions.tsx b/ts/components/session/registration/TermsAndConditions.tsx new file mode 100644 index 0000000000..2a65dd3bf3 --- /dev/null +++ b/ts/components/session/registration/TermsAndConditions.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { SessionHtmlRenderer } from '../SessionHTMLRenderer'; + +export const TermsAndConditions = () => { + return ( +
+ +
+ ); +}; diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index cf418e146e..4d0a2e8f1b 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -70,15 +70,8 @@ export async function getUserED25519KeyPair(): Promise { return undefined; } -/** - * Returns the public key of this current device as a STRING, or throws an error - */ export function isRestoringFromSeed(): boolean { - const ourNumber = window.textsecure.storage.user.isRestoringFromSeed(); - if (!ourNumber) { - throw new Error('ourNumber is not set'); - } - return ourNumber; + return window.textsecure.storage.user.isRestoringFromSeed(); } export function setRestoringFromSeed(isRestoring: boolean) { From fc24df00fb4c232ae7a443f29e677bde1fc1f383 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 26 Feb 2021 11:48:08 +1100 Subject: [PATCH 059/109] always share our profileKey on outgoing messages --- test/backup_test.js | 1 - .../session/registration/TabLabel.tsx | 1 - ts/models/conversation.ts | 53 +------------------ ts/models/message.ts | 7 +-- ts/receiver/dataMessage.ts | 10 +--- ts/receiver/queuedJob.ts | 6 +-- .../data/ExpirationTimerUpdateMessage.ts | 6 --- ts/session/utils/User.ts | 34 ++++++++++++ ts/test/test-utils/utils/message.ts | 1 - 9 files changed, 39 insertions(+), 80 deletions(-) diff --git a/test/backup_test.js b/test/backup_test.js index f474f4bff2..f9a6748564 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -512,7 +512,6 @@ describe('Backup', () => { }, profileKey: 'BASE64KEY', profileName: 'Someone! 🤔', - profileSharing: true, timestamp: 1524185933350, type: 'private', unreadCount: 0, diff --git a/ts/components/session/registration/TabLabel.tsx b/ts/components/session/registration/TabLabel.tsx index 49855a0e1a..bb2548f433 100644 --- a/ts/components/session/registration/TabLabel.tsx +++ b/ts/components/session/registration/TabLabel.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames'; import React from 'react'; -import { SignUpMode, SignUpTab } from './SignUpTab'; export enum TabType { SignUp, diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index f745d7c539..a5edbd13dd 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -35,12 +35,6 @@ import { fromBase64ToArrayBuffer, } from '../session/utils/String'; -export interface OurLokiProfile { - displayName: string; - avatarPointer: string; - profileKey: Uint8Array | null; -} - export interface ConversationAttributes { profileName?: string; id: string; @@ -48,7 +42,6 @@ export interface ConversationAttributes { members: Array; left: boolean; expireTimer: number; - profileSharing: boolean; mentionedUs: boolean; unreadCount: number; lastMessageStatus: string | null; @@ -83,7 +76,6 @@ export interface ConversationAttributesOptionals { members?: Array; left?: boolean; expireTimer?: number; - profileSharing?: boolean; mentionedUs?: boolean; unreadCount?: number; lastMessageStatus?: string | null; @@ -122,7 +114,6 @@ export const fillConvoAttributesWithDefaults = ( return _.defaults(optAttributes, { members: [], left: false, - profileSharing: false, unreadCount: 0, lastMessageStatus: null, lastJoinedTimestamp: new Date('1970-01-01Z00:00:00:000').getTime(), @@ -179,11 +170,6 @@ export class ConversationModel extends Backbone.Model { this.updateAvatarOnPublicChat(avatar) ); - // Always share profile pics with public chats - if (this.isPublic()) { - this.set('profileSharing', true); - } - this.typingRefreshTimer = null; this.typingPauseTimer = null; @@ -687,7 +673,7 @@ export class ConversationModel extends Backbone.Model { expireTimer, preview: uploads.preview, quote: uploads.quote, - lokiProfile: this.getOurProfile(), + lokiProfile: UserUtils.getOurProfile(true), }; const destinationPubkey = new PubKey(destination); @@ -834,9 +820,7 @@ export class ConversationModel extends Backbone.Model { if (!this.isPublic()) { return; } - if (!this.get('profileSharing')) { - return; - } + // Always share avatars on PublicChat if (profileKey && typeof profileKey !== 'string') { // eslint-disable-next-line no-param-reassign @@ -953,16 +937,10 @@ export class ConversationModel extends Backbone.Model { return message; } - let profileKey; - if (this.get('profileSharing')) { - profileKey = window.storage.get('profileKey'); - } - const expireUpdate = { identifier: message.id, timestamp, expireTimer, - profileKey, }; if (!expireUpdate.expireTimer) { @@ -1565,33 +1543,6 @@ export class ConversationModel extends Backbone.Model { return null; } - /** - * Returns - * displayName: string; - * avatarPointer: string; - * profileKey: Uint8Array; - */ - public getOurProfile(): OurLokiProfile | undefined { - try { - // Secondary devices have their profile stored - // in their primary device's conversation - const ourNumber = window.storage.get('primaryDevicePubKey'); - const ourConversation = ConversationController.getInstance().get( - ourNumber - ); - let profileKey = null; - if (this.get('profileSharing')) { - profileKey = new Uint8Array(window.storage.get('profileKey')); - } - const avatarPointer = ourConversation.get('avatarPointer'); - const { displayName } = ourConversation.getLokiProfile(); - return { displayName, avatarPointer, profileKey }; - } catch (e) { - window.log.error(`Failed to get our profile: ${e}`); - return undefined; - } - } - public getNumber() { if (!this.isPrivate()) { return ''; diff --git a/ts/models/message.ts b/ts/models/message.ts index eac7138d6f..b15679dc5b 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -878,10 +878,6 @@ export class MessageModel extends Backbone.Model { } const { body, attachments, preview, quote } = await this.uploadData(); - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - const ourConversation = ConversationController.getInstance().get( - ourNumber - ); const chatParams = { identifier: this.id, @@ -891,8 +887,7 @@ export class MessageModel extends Backbone.Model { attachments, preview, quote, - lokiProfile: - (ourConversation && ourConversation.getOurProfile()) || undefined, + lokiProfile: UserUtils.getOurProfile(true), }; if (!chatParams.lokiProfile) { delete chatParams.lokiProfile; diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index f91cabfb41..5a8c46ba1d 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -400,15 +400,7 @@ async function handleProfileUpdate( const profileKey = StringUtils.decode(profileKeyBuffer, 'base64'); if (!isIncoming) { - const receiver = await ConversationController.getInstance().getOrCreateAndWait( - convoId, - convoType - ); - // First set profileSharing = true for the conversation we sent to - receiver.set({ profileSharing: true }); - await receiver.commit(); - - // Then we update our own profileKey if it's different from what we have + // We update our own profileKey if it's different from what we have const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const me = await ConversationController.getInstance().getOrCreate( ourNumber, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index fc06d17ebf..d9505f1156 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -254,12 +254,8 @@ async function processProfileKey( sendingDeviceConversation: ConversationModel, profileKeyBuffer: Uint8Array ) { - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - const profileKey = StringUtils.decode(profileKeyBuffer, 'base64'); - if (source === ourNumber) { - conversation.set({ profileSharing: true }); - } else if (conversation.isPrivate()) { + if (conversation.isPrivate()) { await conversation.setProfileKey(profileKey); } else { await sendingDeviceConversation.setProfileKey(profileKey); diff --git a/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts b/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts index 180c6553da..4d68d13bfe 100644 --- a/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts +++ b/ts/session/messages/outgoing/content/data/ExpirationTimerUpdateMessage.ts @@ -8,18 +8,15 @@ import { Constants } from '../../../..'; interface ExpirationTimerUpdateMessageParams extends MessageParams { groupId?: string | PubKey; expireTimer: number | null; - profileKey?: Uint8Array; } export class ExpirationTimerUpdateMessage extends DataMessage { public readonly groupId?: PubKey; public readonly expireTimer: number | null; - public readonly profileKey?: Uint8Array; constructor(params: ExpirationTimerUpdateMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); this.expireTimer = params.expireTimer; - this.profileKey = params.profileKey; const { groupId } = params; this.groupId = groupId ? PubKey.cast(groupId) : undefined; @@ -52,9 +49,6 @@ export class ExpirationTimerUpdateMessage extends DataMessage { if (this.expireTimer) { data.expireTimer = this.expireTimer; } - if (this.profileKey && this.profileKey.length) { - data.profileKey = this.profileKey; - } return data; } diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index 4d0a2e8f1b..69443f6be5 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -4,6 +4,7 @@ import { getItemById } from '../../../ts/data/data'; import { KeyPair } from '../../../libtextsecure/libsignal-protocol'; import { PubKey } from '../types'; import { toHex } from './String'; +import { ConversationController } from '../conversations'; export type HexKeyPair = { pubKey: string; @@ -77,3 +78,36 @@ export function isRestoringFromSeed(): boolean { export function setRestoringFromSeed(isRestoring: boolean) { window.textsecure.storage.user.setRestoringFromSeed(isRestoring); } + +export interface OurLokiProfile { + displayName: string; + avatarPointer: string; + profileKey: Uint8Array | null; +} + +/** + * Returns + * displayName: string; + * avatarPointer: string; + * profileKey: Uint8Array; + */ +export function getOurProfile( + shareAvatar: boolean +): OurLokiProfile | undefined { + try { + // Secondary devices have their profile stored + // in their primary device's conversation + const ourNumber = window.storage.get('primaryDevicePubKey'); + const ourConversation = ConversationController.getInstance().get(ourNumber); + let profileKey = null; + if (shareAvatar) { + profileKey = new Uint8Array(window.storage.get('profileKey')); + } + const avatarPointer = ourConversation.get('avatarPointer'); + const { displayName } = ourConversation.getLokiProfile(); + return { displayName, avatarPointer, profileKey }; + } catch (e) { + window.log.error(`Failed to get our profile: ${e}`); + return undefined; + } +} diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index f7116c8b53..3a4486fd9b 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -76,7 +76,6 @@ export class MockConversation { members, left: false, expireTimer: 0, - profileSharing: true, mentionedUs: false, unreadCount: 5, isKickedFromGroup: false, From 43e2ca00ff152c7442501d1e7fa72dd4ff956185 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 26 Feb 2021 12:08:07 +1100 Subject: [PATCH 060/109] do not sync profileKey on sync => only on ConfigurationMessage --- .../outgoing/content/data/ChatMessage.ts | 19 +----- ts/session/utils/String.ts | 2 + ts/session/utils/syncUtils.ts | 63 ++++++++++++------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/ts/session/messages/outgoing/content/data/ChatMessage.ts b/ts/session/messages/outgoing/content/data/ChatMessage.ts index 119aac9c83..ccd6061eec 100644 --- a/ts/session/messages/outgoing/content/data/ChatMessage.ts +++ b/ts/session/messages/outgoing/content/data/ChatMessage.ts @@ -108,23 +108,7 @@ export class ChatMessage extends DataMessage { if (!sentTimestamp || !isNumber(sentTimestamp)) { throw new Error('Tried to build a sync message without a sentTimestamp'); } - // the dataMessage.profileKey is of type ByteBuffer. We need to make it a Uint8Array - const lokiProfile: any = {}; - if (dataMessage.profileKey?.length) { - lokiProfile.profileKey = new Uint8Array( - (dataMessage.profileKey as any).toArrayBuffer() - ); - } - - if (dataMessage.profile) { - if (dataMessage.profile?.displayName) { - lokiProfile.displayName = dataMessage.profile.displayName; - } - if (dataMessage.profile?.profilePicture) { - lokiProfile.avatarPointer = dataMessage.profile.profilePicture; - } - } - + // don't include our profileKey on syncing message. This is to be done by a ConfigurationMessage now const timestamp = toNumber(sentTimestamp); const body = dataMessage.body || undefined; @@ -157,7 +141,6 @@ export class ChatMessage extends DataMessage { attachments, body, quote, - lokiProfile, preview, syncTarget, }); diff --git a/ts/session/utils/String.ts b/ts/session/utils/String.ts index b96d5fdac8..ba76d83df0 100644 --- a/ts/session/utils/String.ts +++ b/ts/session/utils/String.ts @@ -36,5 +36,7 @@ export const fromHex = (d: string) => encode(d, 'hex'); export const fromHexToArray = (d: string) => new Uint8Array(encode(d, 'hex')); export const fromBase64ToArrayBuffer = (d: string) => encode(d, 'base64'); +export const fromBase64ToArray = (d: string) => + new Uint8Array(encode(d, 'base64')); export const fromArrayBufferToBase64 = (d: BufferType) => decode(d, 'base64'); diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index a781d9b239..d64da5b7bb 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -15,7 +15,12 @@ import { ConfigurationMessageContact, } from '../messages/outgoing/content/ConfigurationMessage'; import { ConversationModel } from '../../models/conversation'; -import { fromHexToArray } from './String'; +import { + fromBase64ToArray, + fromBase64ToArrayBuffer, + fromHexToArray, +} from './String'; +import { fromBase64 } from 'bytebuffer'; const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp'; @@ -57,33 +62,43 @@ export const forceSyncConfigurationNowIfNeeded = async ( ) => new Promise(resolve => { const allConvos = ConversationController.getInstance().getConversations(); - - void getCurrentConfigurationMessage(allConvos).then(configMessage => { - // console.warn('forceSyncConfigurationNowIfNeeded with', configMessage); - - try { - // this just adds the message to the sending queue. - // if waitForMessageSent is set, we need to effectively wait until then - // tslint:disable-next-line: no-void-expression - const callback = waitForMessageSent - ? () => { - resolve(true); - } - : undefined; - void getMessageQueue().sendSyncMessage(configMessage, callback as any); - // either we resolve from the callback if we need to wait for it, - // or we don't want to wait, we resolve it here. - if (!waitForMessageSent) { - resolve(true); + void getCurrentConfigurationMessage(allConvos) + .then(configMessage => { + // console.warn('forceSyncConfigurationNowIfNeeded with', configMessage); + + try { + // this just adds the message to the sending queue. + // if waitForMessageSent is set, we need to effectively wait until then + // tslint:disable-next-line: no-void-expression + const callback = waitForMessageSent + ? () => { + resolve(true); + } + : undefined; + void getMessageQueue().sendSyncMessage( + configMessage, + callback as any + ); + // either we resolve from the callback if we need to wait for it, + // or we don't want to wait, we resolve it here. + if (!waitForMessageSent) { + resolve(true); + } + } catch (e) { + window.log.warn( + 'Caught an error while sending our ConfigurationMessage:', + e + ); + resolve(false); } - } catch (e) { + }) + .catch(e => { window.log.warn( - 'Caught an error while sending our ConfigurationMessage:', + 'Caught an error while building our ConfigurationMessage:', e ); resolve(false); - } - }); + }); }); export const getCurrentConfigurationMessage = async ( @@ -146,7 +161,7 @@ export const getCurrentConfigurationMessage = async ( const contacts = contactsModels.map(c => { const profileKeyForContact = c.get('profileKey') - ? fromHexToArray(c.get('profileKey') as string) + ? fromBase64ToArray(c.get('profileKey') as string) : undefined; return new ConfigurationMessageContact({ From 683fa84970f56c8b4e85bc820c9547b168220ec5 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 26 Feb 2021 15:29:14 +1100 Subject: [PATCH 061/109] remove Nickname dialog for now --- background.html | 15 +++-- background_test.html | 15 +++-- js/background.js | 6 -- js/views/app_view.js | 14 ---- js/views/nickname_dialog_view.js | 108 ------------------------------- test/index.html | 7 +- ts/models/conversation.ts | 6 +- 7 files changed, 21 insertions(+), 150 deletions(-) delete mode 100644 js/views/nickname_dialog_view.js diff --git a/background.html b/background.html index d865a59f63..8312a1ba0b 100644 --- a/background.html +++ b/background.html @@ -1,13 +1,14 @@ + - + - + style-src 'self' 'unsafe-inline';"> Session @@ -166,7 +166,6 @@ - @@ -174,6 +173,7 @@ +