Support reporting token on envelope

This commit is contained in:
Fedor Indutny 2023-02-07 16:55:12 -08:00 committed by GitHub
parent dc8d8e529d
commit 486cbe0471
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 26 deletions

View file

@ -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 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 string updated_pni = 15;
optional bool story = 16; // indicates that the content is a story. optional bool story = 16; // indicates that the content is a story.
// next: 17 optional bytes reporting_token = 17;
// next: 18
} }
message Content { message Content {

View file

@ -2853,7 +2853,7 @@ export async function startApp(): Promise<void> {
}: EnvelopeEvent): Promise<void> { }: EnvelopeEvent): Promise<void> {
const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) { if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) {
const { mergePromises } = const { mergePromises, conversation } =
window.ConversationController.maybeMergeContacts({ window.ConversationController.maybeMergeContacts({
e164: envelope.source, e164: envelope.source,
aci: envelope.sourceUuid, aci: envelope.sourceUuid,
@ -2863,6 +2863,10 @@ export async function startApp(): Promise<void> {
if (mergePromises.length > 0) { if (mergePromises.length > 0) {
await Promise.all(mergePromises); await Promise.all(mergePromises);
} }
if (envelope.reportingToken) {
await conversation.updateReportingToken(envelope.reportingToken);
}
} }
} }

View file

@ -2,8 +2,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { assertDev } from '../../util/assert'; import { assertDev } from '../../util/assert';
import { isDirectConversation } from '../../util/whatTypeOfConversation';
import * as log from '../../logging/log'; 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'; import type { reportSpamJobQueue } from '../reportSpamJobQueue';
export async function addReportSpamJob({ export async function addReportSpamJob({
@ -11,14 +12,16 @@ export async function addReportSpamJob({
getMessageServerGuidsForSpam, getMessageServerGuidsForSpam,
jobQueue, jobQueue,
}: Readonly<{ }: Readonly<{
conversation: Readonly<ConversationType>; conversation: Readonly<
Pick<ConversationAttributesType, 'id' | 'type' | 'uuid' | 'reportingToken'>
>;
getMessageServerGuidsForSpam: ( getMessageServerGuidsForSpam: (
conversationId: string conversationId: string
) => Promise<Array<string>>; ) => Promise<Array<string>>;
jobQueue: Pick<typeof reportSpamJobQueue, 'add'>; jobQueue: Pick<typeof reportSpamJobQueue, 'add'>;
}>): Promise<void> { }>): Promise<void> {
assertDev( assertDev(
conversation.type === 'direct', isDirectConversation(conversation),
'addReportSpamJob: cannot report spam for non-direct conversations' 'addReportSpamJob: cannot report spam for non-direct conversations'
); );
@ -41,5 +44,5 @@ export async function addReportSpamJob({
return; return;
} }
await jobQueue.add({ uuid, serverGuids }); await jobQueue.add({ uuid, serverGuids, token: conversation.reportingToken });
} }

View file

@ -28,6 +28,7 @@ const isRetriable4xxStatus = (code: number): boolean =>
const reportSpamJobDataSchema = z.object({ const reportSpamJobDataSchema = z.object({
uuid: z.string().min(1), uuid: z.string().min(1),
token: z.string().optional(),
serverGuids: z.string().array().min(1).max(1000), serverGuids: z.string().array().min(1).max(1000),
}); });
@ -48,7 +49,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
{ data }: Readonly<{ data: ReportSpamJobData }>, { data }: Readonly<{ data: ReportSpamJobData }>,
{ log }: Readonly<{ log: LoggerType }> { log }: Readonly<{ log: LoggerType }>
): Promise<void> { ): Promise<void> {
const { uuid, serverGuids } = data; const { uuid: senderUuid, token, serverGuids } = data;
await new Promise<void>(resolve => { await new Promise<void>(resolve => {
window.storage.onready(resolve); window.storage.onready(resolve);
@ -66,7 +67,9 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
try { try {
await Promise.all( await Promise.all(
map(serverGuids, serverGuid => server.reportMessage(uuid, serverGuid)) map(serverGuids, serverGuid =>
server.reportMessage({ senderUuid, serverGuid, token })
)
); );
} catch (err: unknown) { } catch (err: unknown) {
if (!(err instanceof HTTPError)) { if (!(err instanceof HTTPError)) {

1
ts/model-types.d.ts vendored
View file

@ -353,6 +353,7 @@ export type ConversationAttributesType = {
username?: string; username?: string;
shareMyPhoneNumber?: boolean; shareMyPhoneNumber?: boolean;
previousIdentityKey?: string; previousIdentityKey?: string;
reportingToken?: string;
// Group-only // Group-only
groupId?: string; groupId?: string;

View file

@ -2090,6 +2090,18 @@ export class ConversationModel extends window.Backbone
} }
} }
async updateReportingToken(token?: Uint8Array): Promise<void> {
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 { incrementMessageCount(): void {
this.set({ this.set({
messageCount: (this.get('messageCount') || 0) + 1, messageCount: (this.get('messageCount') || 0) + 1,

View file

@ -264,6 +264,7 @@ export type UnprocessedType = {
decrypted?: string; decrypted?: string;
urgent?: boolean; urgent?: boolean;
story?: boolean; story?: boolean;
reportingToken?: string;
}; };
export type UnprocessedUpdateType = { export type UnprocessedUpdateType = {

View file

@ -2870,7 +2870,7 @@ function blockAndReportSpam(
await Promise.all([ await Promise.all([
conversation.syncMessageRequestResponse(messageRequestEnum.BLOCK), conversation.syncMessageRequestResponse(messageRequestEnum.BLOCK),
addReportSpamJob({ addReportSpamJob({
conversation: conversation.format(), conversation: conversation.attributes,
getMessageServerGuidsForSpam: getMessageServerGuidsForSpam:
window.Signal.Data.getMessageServerGuidsForSpam, window.Signal.Data.getMessageServerGuidsForSpam,
jobQueue: reportSpamJobQueue, jobQueue: reportSpamJobQueue,

View file

@ -2,8 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import { Job } from '../../../jobs/Job'; import { Job } from '../../../jobs/Job';
import { UUID } from '../../../types/UUID';
import { addReportSpamJob } from '../../../jobs/helpers/addReportSpamJob'; import { addReportSpamJob } from '../../../jobs/helpers/addReportSpamJob';
@ -11,6 +11,12 @@ describe('addReportSpamJob', () => {
let getMessageServerGuidsForSpam: sinon.SinonStub; let getMessageServerGuidsForSpam: sinon.SinonStub;
let jobQueue: { add: sinon.SinonStub }; let jobQueue: { add: sinon.SinonStub };
const conversation = {
id: 'convo',
type: 'private' as const,
uuid: UUID.generate().toString(),
};
beforeEach(() => { beforeEach(() => {
getMessageServerGuidsForSpam = sinon.stub().resolves(['abc', 'xyz']); getMessageServerGuidsForSpam = sinon.stub().resolves(['abc', 'xyz']);
jobQueue = { jobQueue = {
@ -31,7 +37,10 @@ describe('addReportSpamJob', () => {
it('does nothing if the conversation lacks a UUID', async () => { it('does nothing if the conversation lacks a UUID', async () => {
await addReportSpamJob({ await addReportSpamJob({
conversation: getDefaultConversation({ uuid: undefined }), conversation: {
...conversation,
uuid: undefined,
},
getMessageServerGuidsForSpam, getMessageServerGuidsForSpam,
jobQueue, jobQueue,
}); });
@ -44,7 +53,7 @@ describe('addReportSpamJob', () => {
getMessageServerGuidsForSpam.resolves([]); getMessageServerGuidsForSpam.resolves([]);
await addReportSpamJob({ await addReportSpamJob({
conversation: getDefaultConversation(), conversation,
getMessageServerGuidsForSpam, getMessageServerGuidsForSpam,
jobQueue, jobQueue,
}); });
@ -52,9 +61,7 @@ describe('addReportSpamJob', () => {
sinon.assert.notCalled(jobQueue.add); sinon.assert.notCalled(jobQueue.add);
}); });
it('enqueues a job', async () => { it('enqueues a job without a token', async () => {
const conversation = getDefaultConversation();
await addReportSpamJob({ await addReportSpamJob({
conversation, conversation,
getMessageServerGuidsForSpam, getMessageServerGuidsForSpam,
@ -68,6 +75,28 @@ describe('addReportSpamJob', () => {
sinon.assert.calledWith(jobQueue.add, { sinon.assert.calledWith(jobQueue.add, {
uuid: conversation.uuid, uuid: conversation.uuid,
serverGuids: ['abc', 'xyz'], 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',
}); });
}); });
}); });

View file

@ -412,6 +412,9 @@ export default class MessageReceiver
serverTimestamp, serverTimestamp,
urgent: isBoolean(decoded.urgent) ? decoded.urgent : true, urgent: isBoolean(decoded.urgent) ? decoded.urgent : true,
story: decoded.story, story: decoded.story,
reportingToken: decoded.reportingToken?.length
? decoded.reportingToken
: undefined,
}; };
// After this point, decoding errors are not the server's // After this point, decoding errors are not the server's
@ -848,6 +851,9 @@ export default class MessageReceiver
item.serverTimestamp || decoded.serverTimestamp?.toNumber(), item.serverTimestamp || decoded.serverTimestamp?.toNumber(),
urgent: isBoolean(item.urgent) ? item.urgent : true, urgent: isBoolean(item.urgent) ? item.urgent : true,
story: Boolean(item.story), story: Boolean(item.story),
reportingToken: item.reportingToken
? Bytes.fromBase64(item.reportingToken)
: undefined,
}; };
const { decrypted } = item; const { decrypted } = item;
@ -1123,6 +1129,9 @@ export default class MessageReceiver
receivedAtCounter: envelope.receivedAtCounter, receivedAtCounter: envelope.receivedAtCounter,
urgent: envelope.urgent, urgent: envelope.urgent,
story: envelope.story, story: envelope.story,
reportingToken: envelope.reportingToken
? Bytes.toBase64(envelope.reportingToken)
: undefined,
}; };
this.decryptAndCacheBatcher.add({ this.decryptAndCacheBatcher.add({
request, request,
@ -1262,14 +1271,12 @@ export default class MessageReceiver
return; return;
} }
if (envelope.content) { if (!envelope.content) {
await this.innerHandleContentMessage(envelope, plaintext); this.removeFromCache(envelope);
throw new Error('Received message with no content');
return;
} }
this.removeFromCache(envelope); await this.innerHandleContentMessage(envelope, plaintext);
throw new Error('Received message with no content');
} }
private async unsealEnvelope( private async unsealEnvelope(

View file

@ -97,6 +97,7 @@ export type ProcessedEnvelope = Readonly<{
groupId?: string; groupId?: string;
urgent?: boolean; urgent?: boolean;
story?: boolean; story?: boolean;
reportingToken?: Uint8Array;
}>; }>;
export type ProcessedAttachment = { export type ProcessedAttachment = {

View file

@ -812,6 +812,12 @@ export type ConfirmCodeOptionsType = Readonly<{
accessKey?: Uint8Array; accessKey?: Uint8Array;
}>; }>;
export type ReportMessageOptionsType = Readonly<{
senderUuid: string;
serverGuid: string;
token?: string;
}>;
export type WebAPIType = { export type WebAPIType = {
startRegistration(): unknown; startRegistration(): unknown;
finishRegistration(baton: unknown): void; finishRegistration(baton: unknown): void;
@ -931,7 +937,7 @@ export type WebAPIType = {
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>; registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>; registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<void>; registerSupportForUnauthenticatedDelivery: () => Promise<void>;
reportMessage: (senderUuid: string, serverGuid: string) => Promise<void>; reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
requestVerificationSMS: (number: string, token: string) => Promise<void>; requestVerificationSMS: (number: string, token: string) => Promise<void>;
requestVerificationVoice: (number: string, token: string) => Promise<void>; requestVerificationVoice: (number: string, token: string) => Promise<void>;
checkAccountExistence: (uuid: UUID) => Promise<boolean>; checkAccountExistence: (uuid: UUID) => Promise<boolean>;
@ -1800,15 +1806,19 @@ export function initialize({
}); });
} }
async function reportMessage( async function reportMessage({
senderUuid: string, senderUuid,
serverGuid: string serverGuid,
): Promise<void> { token,
}: ReportMessageOptionsType): Promise<void> {
const jsonData = { token };
await _ajax({ await _ajax({
call: 'reportMessage', call: 'reportMessage',
httpType: 'POST', httpType: 'POST',
urlParameters: urlPathFromComponents([senderUuid, serverGuid]), urlParameters: urlPathFromComponents([senderUuid, serverGuid]),
responseType: 'bytes', responseType: 'bytes',
jsonData,
}); });
} }