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 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 {

View file

@ -2853,7 +2853,7 @@ export async function startApp(): Promise<void> {
}: EnvelopeEvent): Promise<void> {
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<void> {
if (mergePromises.length > 0) {
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
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<ConversationType>;
conversation: Readonly<
Pick<ConversationAttributesType, 'id' | 'type' | 'uuid' | 'reportingToken'>
>;
getMessageServerGuidsForSpam: (
conversationId: string
) => Promise<Array<string>>;
jobQueue: Pick<typeof reportSpamJobQueue, 'add'>;
}>): Promise<void> {
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 });
}

View file

@ -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<ReportSpamJobData> {
{ data }: Readonly<{ data: ReportSpamJobData }>,
{ log }: Readonly<{ log: LoggerType }>
): Promise<void> {
const { uuid, serverGuids } = data;
const { uuid: senderUuid, token, serverGuids } = data;
await new Promise<void>(resolve => {
window.storage.onready(resolve);
@ -66,7 +67,9 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
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)) {

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

@ -353,6 +353,7 @@ export type ConversationAttributesType = {
username?: string;
shareMyPhoneNumber?: boolean;
previousIdentityKey?: string;
reportingToken?: string;
// Group-only
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 {
this.set({
messageCount: (this.get('messageCount') || 0) + 1,

View file

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

View file

@ -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,

View file

@ -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',
});
});
});

View file

@ -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(

View file

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

View file

@ -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<void>;
registerKeys: (genKeys: KeysType, uuidKind: UUIDKind) => Promise<void>;
registerSupportForUnauthenticatedDelivery: () => Promise<void>;
reportMessage: (senderUuid: string, serverGuid: string) => Promise<void>;
reportMessage: (options: ReportMessageOptionsType) => Promise<void>;
requestVerificationSMS: (number: string, token: string) => Promise<void>;
requestVerificationVoice: (number: string, token: string) => Promise<void>;
checkAccountExistence: (uuid: UUID) => Promise<boolean>;
@ -1800,15 +1806,19 @@ export function initialize({
});
}
async function reportMessage(
senderUuid: string,
serverGuid: string
): Promise<void> {
async function reportMessage({
senderUuid,
serverGuid,
token,
}: ReportMessageOptionsType): Promise<void> {
const jsonData = { token };
await _ajax({
call: 'reportMessage',
httpType: 'POST',
urlParameters: urlPathFromComponents([senderUuid, serverGuid]),
responseType: 'bytes',
jsonData,
});
}