diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 51232e089015..82daffc90def 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -36,7 +36,8 @@ message Envelope { optional bool urgent = 14 [default=true]; // indicates that the content is considered timely by the sender; defaults to true so senders have to opt-out to say something isn't time critical optional string updated_pni = 15; optional bool story = 16; // indicates that the content is a story. - // next: 17 + optional bytes reporting_token = 17; + // next: 18 } message Content { diff --git a/ts/background.ts b/ts/background.ts index 9130004dae5e..308fcbb5c4c4 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -2853,7 +2853,7 @@ export async function startApp(): Promise { }: EnvelopeEvent): Promise { const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) { - const { mergePromises } = + const { mergePromises, conversation } = window.ConversationController.maybeMergeContacts({ e164: envelope.source, aci: envelope.sourceUuid, @@ -2863,6 +2863,10 @@ export async function startApp(): Promise { if (mergePromises.length > 0) { await Promise.all(mergePromises); } + + if (envelope.reportingToken) { + await conversation.updateReportingToken(envelope.reportingToken); + } } } diff --git a/ts/jobs/helpers/addReportSpamJob.ts b/ts/jobs/helpers/addReportSpamJob.ts index d3b7ca5c8dad..836522634d4f 100644 --- a/ts/jobs/helpers/addReportSpamJob.ts +++ b/ts/jobs/helpers/addReportSpamJob.ts @@ -2,8 +2,9 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assertDev } from '../../util/assert'; +import { isDirectConversation } from '../../util/whatTypeOfConversation'; import * as log from '../../logging/log'; -import type { ConversationType } from '../../state/ducks/conversations'; +import type { ConversationAttributesType } from '../../model-types.d'; import type { reportSpamJobQueue } from '../reportSpamJobQueue'; export async function addReportSpamJob({ @@ -11,14 +12,16 @@ export async function addReportSpamJob({ getMessageServerGuidsForSpam, jobQueue, }: Readonly<{ - conversation: Readonly; + conversation: Readonly< + Pick + >; getMessageServerGuidsForSpam: ( conversationId: string ) => Promise>; jobQueue: Pick; }>): Promise { assertDev( - conversation.type === 'direct', + isDirectConversation(conversation), 'addReportSpamJob: cannot report spam for non-direct conversations' ); @@ -41,5 +44,5 @@ export async function addReportSpamJob({ return; } - await jobQueue.add({ uuid, serverGuids }); + await jobQueue.add({ uuid, serverGuids, token: conversation.reportingToken }); } diff --git a/ts/jobs/reportSpamJobQueue.ts b/ts/jobs/reportSpamJobQueue.ts index cd4e1428abf0..f31851b37642 100644 --- a/ts/jobs/reportSpamJobQueue.ts +++ b/ts/jobs/reportSpamJobQueue.ts @@ -28,6 +28,7 @@ const isRetriable4xxStatus = (code: number): boolean => const reportSpamJobDataSchema = z.object({ uuid: z.string().min(1), + token: z.string().optional(), serverGuids: z.string().array().min(1).max(1000), }); @@ -48,7 +49,7 @@ export class ReportSpamJobQueue extends JobQueue { { data }: Readonly<{ data: ReportSpamJobData }>, { log }: Readonly<{ log: LoggerType }> ): Promise { - const { uuid, serverGuids } = data; + const { uuid: senderUuid, token, serverGuids } = data; await new Promise(resolve => { window.storage.onready(resolve); @@ -66,7 +67,9 @@ export class ReportSpamJobQueue extends JobQueue { try { await Promise.all( - map(serverGuids, serverGuid => server.reportMessage(uuid, serverGuid)) + map(serverGuids, serverGuid => + server.reportMessage({ senderUuid, serverGuid, token }) + ) ); } catch (err: unknown) { if (!(err instanceof HTTPError)) { diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 664c47c9aa6e..40387f5c42c7 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -353,6 +353,7 @@ export type ConversationAttributesType = { username?: string; shareMyPhoneNumber?: boolean; previousIdentityKey?: string; + reportingToken?: string; // Group-only groupId?: string; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 9f0ef684fa66..4291cf710bbf 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -2090,6 +2090,18 @@ export class ConversationModel extends window.Backbone } } + async updateReportingToken(token?: Uint8Array): Promise { + const oldValue = this.get('reportingToken'); + const newValue = token ? Bytes.toBase64(token) : undefined; + + if (oldValue === newValue) { + return; + } + + this.set('reportingToken', newValue); + await window.Signal.Data.updateConversation(this.attributes); + } + incrementMessageCount(): void { this.set({ messageCount: (this.get('messageCount') || 0) + 1, diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 50a490faceec..ea56d9faf9e0 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -264,6 +264,7 @@ export type UnprocessedType = { decrypted?: string; urgent?: boolean; story?: boolean; + reportingToken?: string; }; export type UnprocessedUpdateType = { diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 66b865b05778..c71ada211141 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -2870,7 +2870,7 @@ function blockAndReportSpam( await Promise.all([ conversation.syncMessageRequestResponse(messageRequestEnum.BLOCK), addReportSpamJob({ - conversation: conversation.format(), + conversation: conversation.attributes, getMessageServerGuidsForSpam: window.Signal.Data.getMessageServerGuidsForSpam, jobQueue: reportSpamJobQueue, diff --git a/ts/test-node/jobs/helpers/addReportSpamJob_test.ts b/ts/test-node/jobs/helpers/addReportSpamJob_test.ts index 342e76f3125c..2e6b528ff5de 100644 --- a/ts/test-node/jobs/helpers/addReportSpamJob_test.ts +++ b/ts/test-node/jobs/helpers/addReportSpamJob_test.ts @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as sinon from 'sinon'; -import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; import { Job } from '../../../jobs/Job'; +import { UUID } from '../../../types/UUID'; import { addReportSpamJob } from '../../../jobs/helpers/addReportSpamJob'; @@ -11,6 +11,12 @@ describe('addReportSpamJob', () => { let getMessageServerGuidsForSpam: sinon.SinonStub; let jobQueue: { add: sinon.SinonStub }; + const conversation = { + id: 'convo', + type: 'private' as const, + uuid: UUID.generate().toString(), + }; + beforeEach(() => { getMessageServerGuidsForSpam = sinon.stub().resolves(['abc', 'xyz']); jobQueue = { @@ -31,7 +37,10 @@ describe('addReportSpamJob', () => { it('does nothing if the conversation lacks a UUID', async () => { await addReportSpamJob({ - conversation: getDefaultConversation({ uuid: undefined }), + conversation: { + ...conversation, + uuid: undefined, + }, getMessageServerGuidsForSpam, jobQueue, }); @@ -44,7 +53,7 @@ describe('addReportSpamJob', () => { getMessageServerGuidsForSpam.resolves([]); await addReportSpamJob({ - conversation: getDefaultConversation(), + conversation, getMessageServerGuidsForSpam, jobQueue, }); @@ -52,9 +61,7 @@ describe('addReportSpamJob', () => { sinon.assert.notCalled(jobQueue.add); }); - it('enqueues a job', async () => { - const conversation = getDefaultConversation(); - + it('enqueues a job without a token', async () => { await addReportSpamJob({ conversation, getMessageServerGuidsForSpam, @@ -68,6 +75,28 @@ describe('addReportSpamJob', () => { sinon.assert.calledWith(jobQueue.add, { uuid: conversation.uuid, serverGuids: ['abc', 'xyz'], + token: undefined, + }); + }); + + it('enqueues a job with a token', async () => { + await addReportSpamJob({ + conversation: { + ...conversation, + reportingToken: 'uvw', + }, + getMessageServerGuidsForSpam, + jobQueue, + }); + + sinon.assert.calledOnce(getMessageServerGuidsForSpam); + sinon.assert.calledWith(getMessageServerGuidsForSpam, conversation.id); + + sinon.assert.calledOnce(jobQueue.add); + sinon.assert.calledWith(jobQueue.add, { + uuid: conversation.uuid, + serverGuids: ['abc', 'xyz'], + token: 'uvw', }); }); }); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index e89613309e2e..21c963f9459d 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -412,6 +412,9 @@ export default class MessageReceiver serverTimestamp, urgent: isBoolean(decoded.urgent) ? decoded.urgent : true, story: decoded.story, + reportingToken: decoded.reportingToken?.length + ? decoded.reportingToken + : undefined, }; // After this point, decoding errors are not the server's @@ -848,6 +851,9 @@ export default class MessageReceiver item.serverTimestamp || decoded.serverTimestamp?.toNumber(), urgent: isBoolean(item.urgent) ? item.urgent : true, story: Boolean(item.story), + reportingToken: item.reportingToken + ? Bytes.fromBase64(item.reportingToken) + : undefined, }; const { decrypted } = item; @@ -1123,6 +1129,9 @@ export default class MessageReceiver receivedAtCounter: envelope.receivedAtCounter, urgent: envelope.urgent, story: envelope.story, + reportingToken: envelope.reportingToken + ? Bytes.toBase64(envelope.reportingToken) + : undefined, }; this.decryptAndCacheBatcher.add({ request, @@ -1262,14 +1271,12 @@ export default class MessageReceiver return; } - if (envelope.content) { - await this.innerHandleContentMessage(envelope, plaintext); - - return; + if (!envelope.content) { + this.removeFromCache(envelope); + throw new Error('Received message with no content'); } - this.removeFromCache(envelope); - throw new Error('Received message with no content'); + await this.innerHandleContentMessage(envelope, plaintext); } private async unsealEnvelope( diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 536b4859475a..fae60799be81 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -97,6 +97,7 @@ export type ProcessedEnvelope = Readonly<{ groupId?: string; urgent?: boolean; story?: boolean; + reportingToken?: Uint8Array; }>; export type ProcessedAttachment = { diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 610c19aace38..43224fb5de20 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -812,6 +812,12 @@ export type ConfirmCodeOptionsType = Readonly<{ accessKey?: Uint8Array; }>; +export type ReportMessageOptionsType = Readonly<{ + senderUuid: string; + serverGuid: string; + token?: string; +}>; + export type WebAPIType = { startRegistration(): unknown; finishRegistration(baton: unknown): void; @@ -931,7 +937,7 @@ export type WebAPIType = { registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise; registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise; registerSupportForUnauthenticatedDelivery: () => Promise; - reportMessage: (senderUuid: string, serverGuid: string) => Promise; + reportMessage: (options: ReportMessageOptionsType) => Promise; requestVerificationSMS: (number: string, token: string) => Promise; requestVerificationVoice: (number: string, token: string) => Promise; checkAccountExistence: (uuid: UUID) => Promise; @@ -1800,15 +1806,19 @@ export function initialize({ }); } - async function reportMessage( - senderUuid: string, - serverGuid: string - ): Promise { + async function reportMessage({ + senderUuid, + serverGuid, + token, + }: ReportMessageOptionsType): Promise { + const jsonData = { token }; + await _ajax({ call: 'reportMessage', httpType: 'POST', urlParameters: urlPathFromComponents([senderUuid, serverGuid]), responseType: 'bytes', + jsonData, }); }