diff --git a/js/background.js b/js/background.js index b86834aa0..2210cfea9 100644 --- a/js/background.js +++ b/js/background.js @@ -2288,6 +2288,7 @@ if (data.message.reaction) { const { reaction } = data.message; + window.log.info('Queuing reaction for', reaction.targetTimestamp); const reactionModel = Whisper.Reactions.add({ emoji: reaction.emoji, remove: reaction.remove, @@ -2305,6 +2306,7 @@ if (data.message.delete) { const { delete: del } = data.message; + window.log.info('Queuing DOE for', del.targetSentTimestamp); const deleteModel = Whisper.Deletes.add({ targetSentTimestamp: del.targetSentTimestamp, serverTimestamp: data.serverTimestamp, diff --git a/js/deletes.js b/js/deletes.js index 95c06b073..4ae40c3c8 100644 --- a/js/deletes.js +++ b/js/deletes.js @@ -11,8 +11,6 @@ (function() { 'use strict'; - const ONE_DAY = 24 * 60 * 60 * 1000; - window.Whisper = window.Whisper || {}; Whisper.Deletes = new (Backbone.Collection.extend({ forMessage(message) { @@ -46,6 +44,8 @@ // Do not await, since this can deadlock the queue fromContact.queueJob(async () => { + window.log.info('Handling DOE for', del.get('targetSentTimestamp')); + const messages = await window.Signal.Data.getMessagesBySentAt( del.get('targetSentTimestamp'), { @@ -74,29 +74,12 @@ return; } - // Make sure the server timestamps for the DOE and the matching message - // are less than one day apart - const delta = Math.abs( - del.get('serverTimestamp') - targetMessage.get('serverTimestamp') - ); - if (delta > ONE_DAY) { - window.log.info('Received late DOE. Dropping.', { - fromId: del.get('fromId'), - targetSentTimestamp: del.get('targetSentTimestamp'), - messageServerTimestamp: message.get('serverTimestamp'), - deleteServerTimestamp: del.get('serverTimestamp'), - }); - this.remove(del); - - return; - } - const message = MessageController.register( targetMessage.id, targetMessage ); - await message.handleDeleteForEveryone(del); + await window.Signal.Util.deleteForEveryone(message, del); this.remove(del); }); diff --git a/js/models/messages.js b/js/models/messages.js index 40ae2cd30..cd4b27fb3 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1111,7 +1111,7 @@ isErased() { return Boolean(this.get('isErased')); }, - async eraseContents(additionalProperties = {}) { + async eraseContents(additionalProperties = {}, shouldPersist = true) { if (this.get('isErased')) { return; } @@ -1139,9 +1139,11 @@ }); this.trigger('content-changed'); - await window.Signal.Data.saveMessage(this.attributes, { - Message: Whisper.Message, - }); + if (shouldPersist) { + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); + } }, unload() { if (this.quotedMessage) { @@ -2583,7 +2585,9 @@ // Does this message have any pending, previously-received associated // delete for everyone messages? const deletes = Whisper.Deletes.forMessage(message); - deletes.forEach(del => Whisper.Deletes.onDelete(del, false)); + deletes.forEach(del => { + window.Signal.Util.deleteForEveryone(message, del, false); + }); await window.Signal.Data.saveMessage(message.attributes, { Message: Whisper.Message, @@ -2658,7 +2662,7 @@ } }, - async handleDeleteForEveryone(del) { + async handleDeleteForEveryone(del, shouldPersist = true) { window.log.info('Handling DOE.', { fromId: del.get('fromId'), targetSentTimestamp: del.get('targetSentTimestamp'), @@ -2673,7 +2677,10 @@ Whisper.Notifications.remove(notificationForMessage); // Erase the contents of this message - await this.eraseContents({ deletedForEveryone: true, reactions: [] }); + await this.eraseContents( + { deletedForEveryone: true, reactions: [] }, + shouldPersist + ); // Update the conversation's last message in case this was the last message this.getConversation().updateLastMessage(); diff --git a/js/reactions.js b/js/reactions.js index c6da4d3cb..254a744bb 100644 --- a/js/reactions.js +++ b/js/reactions.js @@ -53,11 +53,22 @@ reaction.get('targetTimestamp') ); if (!targetConversation) { + window.log.info( + 'No contact for reaction', + reaction.get('targetAuthorE164'), + reaction.get('targetAuthorUuid'), + reaction.get('targetTimestamp') + ); return; } // awaiting is safe since `onReaction` is never called from inside the queue await targetConversation.queueJob(async () => { + window.log.info( + 'Handling reaction for', + reaction.get('targetTimestamp') + ); + const messages = await window.Signal.Data.getMessagesBySentAt( reaction.get('targetTimestamp'), { diff --git a/package.json b/package.json index d83329014..003a88ed5 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "@storybook/addons": "5.1.11", "@storybook/react": "5.1.11", "@types/agent-base": "4.2.0", + "@types/backbone": "1.4.3", "@types/chai": "4.1.2", "@types/classnames": "2.2.3", "@types/config": "0.0.34", diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts new file mode 100644 index 000000000..fe3d7a9b4 --- /dev/null +++ b/ts/model-types.d.ts @@ -0,0 +1,114 @@ +import * as Backbone from 'backbone'; +import { ColorType, LocalizerType } from './types/Util'; +import { SendOptionsType } from './textsecure/SendMessage'; +import { ConversationType } from './state/ducks/conversations'; +import { SyncMessageClass } from './textsecure.d'; + +interface ModelAttributesInterface { + [key: string]: any; +} + +type DeletesAttributesType = { + fromId: string; + serverTimestamp: number; + targetSentTimestamp: number; +}; + +declare class DeletesModelType extends Backbone.Model { + forMessage(message: MessageModelType): Array; + onDelete(doe: DeletesAttributesType): Promise; +} + +type TaskResultType = any; + +type MessageAttributesType = { + id: string; + serverTimestamp: number; +}; + +declare class MessageModelType extends Backbone.Model { + id: string; + + static updateTimers(): void; + + getContact(): ConversationModelType | undefined | null; + getConversation(): ConversationModelType | undefined | null; + getPropsForSearchResult(): any; + getPropsForBubble(): any; + cleanup(): Promise; + handleDeleteForEveryone( + doe: DeletesModelType, + shouldPersist: boolean + ): Promise; +} + +type ConversationTypeType = 'private' | 'group'; + +type ConversationAttributesType = { + id: string; + uuid?: string; + e164?: string; + + active_at?: number | null; + draft?: string; + groupId?: string; + isArchived?: boolean; + lastMessage?: string; + members?: Array; + needsVerification?: boolean; + profileFamilyName?: string | null; + profileKey?: string | null; + profileName?: string | null; + profileSharing: boolean; + storageID?: string; + type: ConversationTypeType; + unreadCount?: number; + verified?: number; + version: number; +}; + +declare class ConversationModelType extends Backbone.Model< + ConversationAttributesType +> { + id: string; + cachedProps: ConversationType; + initialPromise: Promise; + + applyMessageRequestResponse( + response: number, + options?: { fromSync: boolean } + ): void; + cleanup(): Promise; + disableProfileSharing(): void; + getAccepted(): boolean; + getAvatarPath(): string | undefined; + getColor(): ColorType | undefined; + getIsAddedByContact(): boolean; + getName(): string | undefined; + getNumber(): string; + getProfileName(): string | undefined; + getProfiles(): Promise>>; + getRecipients: () => Array; + getSendOptions(options?: any): SendOptionsType | undefined; + getTitle(): string; + idForLogging(): string; + isVerified(): boolean; + safeGetVerified(): Promise; + setProfileKey(profileKey?: string | null): Promise; + toggleVerified(): Promise; + unblock(): boolean | undefined; + updateE164: (e164?: string) => void; + updateLastMessage: () => Promise; + updateUuid: (uuid?: string) => void; + wrapSend: (sendPromise: Promise) => Promise; +} + +declare class ConversationModelCollectionType extends Backbone.Collection< + ConversationModelType +> { + resetLookups(): void; +} + +declare class MessageModelCollectionType extends Backbone.Collection< + MessageModelType +> {} diff --git a/ts/util/deleteForEveryone.ts b/ts/util/deleteForEveryone.ts new file mode 100644 index 000000000..f77a928b9 --- /dev/null +++ b/ts/util/deleteForEveryone.ts @@ -0,0 +1,26 @@ +import { DeletesModelType, MessageModelType } from '../model-types.d'; + +const ONE_DAY = 24 * 60 * 60 * 1000; + +export async function deleteForEveryone( + message: MessageModelType, + doe: DeletesModelType, + shouldPersist: boolean = true +): Promise { + // Make sure the server timestamps for the DOE and the matching message + // are less than one day apart + const delta = Math.abs( + doe.get('serverTimestamp') - message.get('serverTimestamp') + ); + if (delta > ONE_DAY) { + window.log.info('Received late DOE. Dropping.', { + fromId: doe.get('fromId'), + targetSentTimestamp: doe.get('targetSentTimestamp'), + messageServerTimestamp: message.get('serverTimestamp'), + deleteServerTimestamp: doe.get('serverTimestamp'), + }); + return; + } + + await message.handleDeleteForEveryone(doe, shouldPersist); +} diff --git a/ts/util/index.ts b/ts/util/index.ts index 8600b428e..9fe3fea28 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -4,6 +4,7 @@ import { arrayBufferToObjectURL } from './arrayBufferToObjectURL'; import { combineNames } from './combineNames'; import { createBatcher } from './batcher'; import { createWaitBatcher } from './waitBatcher'; +import { deleteForEveryone } from './deleteForEveryone'; import { downloadAttachment } from './downloadAttachment'; import { hasExpired } from './hasExpired'; import { isFileDangerous } from './isFileDangerous'; @@ -17,6 +18,7 @@ export { combineNames, createBatcher, createWaitBatcher, + deleteForEveryone, downloadAttachment, GoogleChrome, hasExpired, diff --git a/tslint.json b/tslint.json index 30b420679..dc8ccdfa3 100644 --- a/tslint.json +++ b/tslint.json @@ -21,6 +21,7 @@ "prefer-for-of": false, "no-this-assignment": false, "binary-expression-operand-order": false, + "no-backbone-get-set-outside-model": false, // Allows us to write inline `style`s. Revisit when we have a more sophisticated // CSS-in-JS solution: diff --git a/yarn.lock b/yarn.lock index 7dfe847d1..028c8fcd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2044,6 +2044,14 @@ resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== +"@types/backbone@1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/backbone/-/backbone-1.4.3.tgz#75dc6e55382e226788db8d796de346891d6b2256" + integrity sha512-PZVw2FckEbEJ+qh2hvtgpI/4p8yD3sRbA8FEO72k01/90SSH73GcLW3CqcYP5epwDpLl3cKrgK0yypQY4qiuEw== + dependencies: + "@types/jquery" "*" + "@types/underscore" "*" + "@types/body-parser@*": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897" @@ -2217,6 +2225,13 @@ dependencies: "@types/node" "*" +"@types/jquery@*": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68" + integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw== + dependencies: + "@types/sizzle" "*" + "@types/jquery@3.3.29": version "3.3.29" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd" @@ -2496,6 +2511,11 @@ dependencies: source-map "^0.6.1" +"@types/underscore@*": + version "1.10.14" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.14.tgz#a2a831c72a12deddaef26028d16a5aa48aadbee0" + integrity sha512-VE20ZYf38nmOU1lU0wpQBWcGPlskfKK8uU8AN1UIz5PjxT2YM7HTF0iUA85iGJnbQ3tZweqIfQqmLgLMtP27YQ== + "@types/uuid@3.4.4": version "3.4.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"