signal-desktop/ts/services/username.ts
2022-10-18 10:12:02 -07:00

172 lines
4.7 KiB
TypeScript

// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import dataInterface from '../sql/Client';
import { updateOurUsernameAndPni } from '../util/updateOurUsernameAndPni';
import { sleep } from '../util/sleep';
import type { UsernameReservationType } from '../types/Username';
import { ReserveUsernameError } from '../types/Username';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import MessageSender from '../textsecure/SendMessage';
import { HTTPError } from '../textsecure/Errors';
import { findRetryAfterTimeFromError } from '../jobs/helpers/findRetryAfterTimeFromError';
export type WriteUsernameOptionsType = Readonly<
| {
reservation: UsernameReservationType;
}
| {
username: undefined;
previousUsername: string | undefined;
reservation?: undefined;
}
>;
export type ReserveUsernameOptionsType = Readonly<{
nickname: string;
previousUsername: string | undefined;
abortSignal?: AbortSignal;
}>;
export type ReserveUsernameResultType = Readonly<
| {
ok: true;
reservation: UsernameReservationType;
error?: void;
}
| {
ok: false;
reservation?: void;
error: ReserveUsernameError;
}
>;
export async function reserveUsername(
options: ReserveUsernameOptionsType
): Promise<ReserveUsernameResultType> {
const { server } = window.textsecure;
if (!server) {
throw new Error('server interface is not available!');
}
const { nickname, previousUsername, abortSignal } = options;
const me = window.ConversationController.getOurConversationOrThrow();
await updateOurUsernameAndPni();
if (me.get('username') !== previousUsername) {
throw new Error('reserveUsername: Username has changed on another device');
}
try {
const { username, reservationToken } = await server.reserveUsername({
nickname,
abortSignal,
});
return {
ok: true,
reservation: { previousUsername, username, reservationToken },
};
} catch (error) {
if (error instanceof HTTPError) {
if (error.code === 422) {
return { ok: false, error: ReserveUsernameError.Unprocessable };
}
if (error.code === 409) {
return { ok: false, error: ReserveUsernameError.Conflict };
}
if (error.code === 413) {
const time = findRetryAfterTimeFromError(error);
log.warn(`reserveUsername: got 413, waiting ${time}ms`);
await sleep(time, abortSignal);
return reserveUsername(options);
}
}
throw error;
}
}
async function updateUsernameAndSyncProfile(
username: string | undefined
): Promise<void> {
const me = window.ConversationController.getOurConversationOrThrow();
// Update backbone, update DB, then tell linked devices about profile update
me.set({ username });
dataInterface.updateConversation(me.attributes);
try {
await singleProtoJobQueue.add(
MessageSender.getFetchLocalProfileSyncMessage()
);
} catch (error) {
log.error(
'updateUsernameAndSyncProfile: Failed to queue sync message',
Errors.toLogFormat(error)
);
}
}
export async function confirmUsername(
reservation: UsernameReservationType,
abortSignal?: AbortSignal
): Promise<void> {
const { server } = window.textsecure;
if (!server) {
throw new Error('server interface is not available!');
}
const { previousUsername, username, reservationToken } = reservation;
const me = window.ConversationController.getOurConversationOrThrow();
await updateOurUsernameAndPni();
if (me.get('username') !== previousUsername) {
throw new Error('Username has changed on another device');
}
try {
await server.confirmUsername({
usernameToConfirm: username,
reservationToken,
abortSignal,
});
await updateUsernameAndSyncProfile(username);
} catch (error) {
if (error instanceof HTTPError) {
if (error.code === 413) {
const time = findRetryAfterTimeFromError(error);
log.warn(`confirmUsername: got 413, waiting ${time}ms`);
await sleep(time, abortSignal);
return confirmUsername(reservation, abortSignal);
}
}
throw error;
}
}
export async function deleteUsername(
previousUsername: string,
abortSignal?: AbortSignal
): Promise<void> {
const { server } = window.textsecure;
if (!server) {
throw new Error('server interface is not available!');
}
const me = window.ConversationController.getOurConversationOrThrow();
await updateOurUsernameAndPni();
if (me.get('username') !== previousUsername) {
throw new Error('Username has changed on another device');
}
await server.deleteUsername(abortSignal);
await updateUsernameAndSyncProfile(undefined);
}