signal-desktop/ts/jobs/reportSpamJobQueue.ts

125 lines
3.7 KiB
TypeScript
Raw Normal View History

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as z from 'zod';
import * as durations from '../util/durations';
import { strictAssert } from '../util/assert';
import { waitForOnline } from '../util/waitForOnline';
import { isDone as isDeviceLinked } from '../util/registration';
import type { LoggerType } from '../types/Logging';
2023-08-16 20:54:39 +00:00
import { aciSchema } from '../types/ServiceId';
import { map } from '../util/iterables';
import type { JOB_STATUS } from './JobQueue';
import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { parseIntWithFallback } from '../util/parseIntWithFallback';
import type { WebAPIType } from '../textsecure/WebAPI';
2021-09-22 00:58:03 +00:00
import { HTTPError } from '../textsecure/Errors';
2023-02-24 19:03:17 +00:00
import { sleeper } from '../util/sleeper';
const RETRY_WAIT_TIME = durations.MINUTE;
const RETRYABLE_4XX_FAILURE_STATUSES = new Set([
2021-11-11 22:43:05 +00:00
404, 408, 410, 412, 413, 414, 417, 423, 424, 425, 426, 428, 429, 431, 449,
]);
const is4xxStatus = (code: number): boolean => code >= 400 && code <= 499;
const is5xxStatus = (code: number): boolean => code >= 500 && code <= 599;
const isRetriable4xxStatus = (code: number): boolean =>
RETRYABLE_4XX_FAILURE_STATUSES.has(code);
const reportSpamJobDataSchema = z.object({
2023-08-16 20:54:39 +00:00
aci: aciSchema,
2023-02-08 00:55:12 +00:00
token: z.string().optional(),
serverGuids: z.string().array().min(1).max(1000),
});
export type ReportSpamJobData = z.infer<typeof reportSpamJobDataSchema>;
2021-07-21 21:10:08 +00:00
export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
private server?: WebAPIType;
public initialize({ server }: { server: WebAPIType }): void {
this.server = server;
}
2021-07-21 21:10:08 +00:00
protected parseData(data: unknown): ReportSpamJobData {
return reportSpamJobDataSchema.parse(data);
2021-07-21 21:10:08 +00:00
}
protected async run(
{ data }: Readonly<{ data: ReportSpamJobData }>,
{ log }: Readonly<{ log: LoggerType }>
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
2023-08-16 20:54:39 +00:00
const { aci: senderAci, token, serverGuids } = data;
await new Promise<void>(resolve => {
window.storage.onready(resolve);
});
if (!isDeviceLinked()) {
log.info("reportSpamJobQueue: skipping this job because we're unlinked");
return undefined;
}
await waitForOnline();
const { server } = this;
strictAssert(server !== undefined, 'ReportSpamJobQueue not initialized');
try {
await Promise.all(
2023-02-08 00:55:12 +00:00
map(serverGuids, serverGuid =>
2023-08-16 20:54:39 +00:00
server.reportMessage({ senderAci, serverGuid, token })
2023-02-08 00:55:12 +00:00
)
);
return undefined;
} catch (err: unknown) {
2021-09-22 00:58:03 +00:00
if (!(err instanceof HTTPError)) {
throw err;
}
const code = parseIntWithFallback(err.code, -1);
// This is an unexpected case, except for -1, which can happen for network failures.
if (code < 400) {
throw err;
}
if (code === 508) {
log.info(
'reportSpamJobQueue: server responded with 508. Giving up on this job'
);
return undefined;
}
if (isRetriable4xxStatus(code) || is5xxStatus(code)) {
log.info(
`reportSpamJobQueue: server responded with ${code} status code. Sleeping before our next attempt`
);
2023-02-24 19:03:17 +00:00
await sleeper.sleep(
RETRY_WAIT_TIME,
`reportSpamJobQueue: server responded with ${code} status code`
);
throw err;
}
if (is4xxStatus(code)) {
log.error(
`reportSpamJobQueue: server responded with ${code} status code. Giving up on this job`
);
return undefined;
}
throw err;
}
2021-07-21 21:10:08 +00:00
}
}
export const reportSpamJobQueue = new ReportSpamJobQueue({
store: jobQueueDatabaseStore,
queueType: 'report spam',
maxAttempts: 25,
});