Support device name change sync message

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2024-12-09 18:02:53 -06:00 committed by GitHub
parent 227d80e441
commit f5b6852f39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 190 additions and 12 deletions

View file

@ -700,6 +700,11 @@ message SyncMessage {
repeated LocalOnlyConversationDelete localOnlyConversationDeletes = 3;
repeated AttachmentDelete attachmentDeletes = 4;
}
message DeviceNameChange {
reserved /*name*/ 1;
optional uint32 deviceId = 2;
}
optional Sent sent = 1;
optional Contacts contacts = 2;
@ -723,6 +728,7 @@ message SyncMessage {
optional CallLinkUpdate callLinkUpdate = 20;
optional CallLogEvent callLogEvent = 21;
optional DeleteForMe deleteForMe = 22;
optional DeviceNameChange deviceNameChange = 23;
}
message AttachmentPointer {

View file

@ -199,6 +199,10 @@ import { getParametersForRedux, loadAll } from './services/allLoaders';
import { checkFirstEnvelope } from './util/checkFirstEnvelope';
import { BLOCKED_UUIDS_ID } from './textsecure/storage/Blocked';
import { ReleaseNotesFetcher } from './services/releaseNotesFetcher';
import {
maybeQueueDeviceNameFetch,
onDeviceNameChangeSync,
} from './util/onDeviceNameChangeSync';
export function isOverHourIntoPast(timestamp: number): boolean {
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
@ -697,6 +701,10 @@ export async function startApp(): Promise<void> {
'deleteForMeSync',
queuedEventListener(onDeleteForMeSync, false)
);
messageReceiver.addEventListener(
'deviceNameChangeSync',
queuedEventListener(onDeviceNameChangeSync, false)
);
if (!window.storage.get('defaultConversationColor')) {
drop(
@ -1924,6 +1932,12 @@ export async function startApp(): Promise<void> {
Errors.toLogFormat(error)
);
}
// Ensure we have the correct device name locally (allowing us to get eventually
// consistent with primary, in case we failed to process a deviceNameChangeSync
// for some reason). We do this after calling `maybeUpdateDeviceName` to ensure
// that the device name on server is encrypted.
drop(maybeQueueDeviceNameFetch());
}
if (firstRun === true && !areWePrimaryDevice) {

View file

@ -315,6 +315,7 @@ export default class AccountManager extends EventTarget {
if (base64) {
await this.server.updateDeviceName(base64);
await window.textsecure.storage.user.setDeviceNameEncrypted();
}
}

View file

@ -113,6 +113,7 @@ import {
DecryptionErrorEvent,
DeleteForMeSyncEvent,
DeliveryEvent,
DeviceNameChangeSyncEvent,
EmptyEvent,
EnvelopeQueuedEvent,
EnvelopeUnsealedEvent,
@ -701,6 +702,11 @@ export default class MessageReceiver
handler: (ev: DeleteForMeSyncEvent) => void
): void;
public override addEventListener(
name: 'deviceNameChangeSync',
handler: (ev: DeviceNameChangeSyncEvent) => void
): void;
public override addEventListener(name: string, handler: EventHandler): void {
return super.addEventListener(name, handler);
}
@ -3191,6 +3197,12 @@ export default class MessageReceiver
if (syncMessage.deleteForMe) {
return this.handleDeleteForMeSync(envelope, syncMessage.deleteForMe);
}
if (syncMessage.deviceNameChange) {
return this.handleDeviceNameChangeSync(
envelope,
syncMessage.deviceNameChange
);
}
this.removeFromCache(envelope);
const envelopeId = getEnvelopeId(envelope);
@ -3817,6 +3829,39 @@ export default class MessageReceiver
log.info('handleDeleteForMeSync: finished');
}
private async handleDeviceNameChangeSync(
envelope: ProcessedEnvelope,
deviceNameChange: Proto.SyncMessage.IDeviceNameChange
): Promise<void> {
const logId = `MessageReceiver.handleDeviceNameChangeSync: ${getEnvelopeId(envelope)}`;
log.info(logId);
logUnexpectedUrgentValue(envelope, 'deviceNameChangeSync');
const { deviceId } = deviceNameChange;
const localDeviceId = parseIntOrThrow(
this.storage.user.getDeviceId(),
'MessageReceiver.handleDeviceNameChangeSync: localDeviceId'
);
if (deviceId == null) {
log.warn(logId, 'deviceId was falsey');
this.removeFromCache(envelope);
return;
}
if (deviceId !== localDeviceId) {
log.info(logId, 'meant for other device:', deviceId);
this.removeFromCache(envelope);
return;
}
const deviceNameChangeEvent = new DeviceNameChangeSyncEvent(
this.removeFromCache.bind(this, envelope)
);
await this.dispatchAndWait(logId, deviceNameChangeEvent);
}
private async handleContacts(
envelope: ProcessedEnvelope,
contactSyncProto: Proto.SyncMessage.IContacts

View file

@ -862,6 +862,19 @@ export type GetAccountForUsernameResultType = z.infer<
typeof getAccountForUsernameResultZod
>;
const getDevicesResultZod = z.object({
devices: z.array(
z.object({
id: z.number(),
name: z.string().nullish(), // primary devices may not have a name
lastSeen: z.number().nullish(),
created: z.number().nullish(),
})
),
});
export type GetDevicesResultType = z.infer<typeof getDevicesResultZod>;
export type GetIceServersResultType = Readonly<{
relays?: ReadonlyArray<IceServerGroupType>;
}>;
@ -874,15 +887,6 @@ export type IceServerGroupType = Readonly<{
hostname?: string;
}>;
export type GetDevicesResultType = ReadonlyArray<
Readonly<{
id: number;
name: string;
lastSeen: number;
created: number;
}>
>;
export type GetSenderCertificateResultType = Readonly<{ certificate: string }>;
export type MakeProxiedRequestResultType =
@ -1376,6 +1380,7 @@ export type WebAPIType = {
getAccountForUsername: (
options: GetAccountForUsernameOptionsType
) => Promise<GetAccountForUsernameResultType>;
getDevices: () => Promise<GetDevicesResultType>;
getProfileUnauth: (
serviceId: ServiceIdString,
options: ProfileFetchUnauthRequestOptions
@ -1847,6 +1852,7 @@ export function initialize({
getBackupUploadForm,
getBadgeImageFile,
getConfig,
getDevices,
getGroup,
getGroupAvatar,
getGroupCredentials,
@ -2935,6 +2941,15 @@ export function initialize({
});
}
async function getDevices() {
const result = await _ajax({
call: 'devices',
httpType: 'GET',
responseType: 'json',
});
return parseUnknown(getDevicesResultZod, result);
}
async function updateDeviceName(deviceName: string) {
await _ajax({
call: 'updateDeviceName',

View file

@ -493,6 +493,12 @@ export class CallLinkUpdateSyncEvent extends ConfirmableEvent {
}
}
export class DeviceNameChangeSyncEvent extends ConfirmableEvent {
constructor(confirm: ConfirmCallback) {
super('deviceNameChangeSync', confirm);
}
}
const messageToDeleteSchema = z.union([
z.object({
type: z.literal('aci').readonly(),

View file

@ -153,6 +153,10 @@ export class User {
return this.storage.get('device_name');
}
public async setDeviceName(name: string): Promise<void> {
return this.storage.put('device_name', name);
}
public async setDeviceNameEncrypted(): Promise<void> {
return this.storage.put('deviceNameEncrypted', true);
}
@ -175,9 +179,7 @@ export class User {
this.storage.put('uuid_id', `${aci}.${deviceId}`),
this.storage.put('password', password),
this.setPni(pni),
deviceName
? this.storage.put('device_name', deviceName)
: Promise.resolve(),
deviceName ? this.setDeviceName(deviceName) : Promise.resolve(),
]);
}

View file

@ -69,6 +69,7 @@ export const sendTypesEnum = z.enum([
'callEventSync',
'callLinkUpdateSync',
'callLogEventSync',
'deviceNameChangeSync',
// No longer used, all non-urgent
'legacyGroupChange',

View file

@ -0,0 +1,88 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import PQueue from 'p-queue';
import type { DeviceNameChangeSyncEvent } from '../textsecure/messageReceiverEvents';
import { MINUTE } from './durations';
import { strictAssert } from './assert';
import { parseIntOrThrow } from './parseIntOrThrow';
import * as log from '../logging/log';
import { toLogFormat } from '../types/errors';
import { drop } from './drop';
const deviceNameFetchQueue = new PQueue({
concurrency: 1,
timeout: 5 * MINUTE,
throwOnTimeout: true,
});
export async function onDeviceNameChangeSync(
event: DeviceNameChangeSyncEvent
): Promise<void> {
const { confirm } = event;
const maybeQueueAndThenConfirm = async () => {
await maybeQueueDeviceNameFetch();
confirm();
};
drop(maybeQueueAndThenConfirm());
}
export async function maybeQueueDeviceNameFetch(): Promise<void> {
if (deviceNameFetchQueue.size >= 1) {
log.info('maybeQueueDeviceNameFetch: skipping; fetch already queued');
}
try {
await deviceNameFetchQueue.add(fetchAndUpdateDeviceName);
} catch (e) {
log.error(
'maybeQueueDeviceNameFetch: error when fetching device name',
toLogFormat(e)
);
}
}
async function fetchAndUpdateDeviceName() {
strictAssert(window.textsecure.server, 'WebAPI must be initialized');
const { devices } = await window.textsecure.server.getDevices();
const localDeviceId = parseIntOrThrow(
window.textsecure.storage.user.getDeviceId(),
'fetchAndUpdateDeviceName: localDeviceId'
);
const ourDevice = devices.find(device => device.id === localDeviceId);
strictAssert(ourDevice, 'ourDevice must be returned from devices endpoint');
const newNameEncrypted = ourDevice.name;
if (!newNameEncrypted) {
log.error('fetchAndUpdateDeviceName: device had empty name');
return;
}
let newName: string;
try {
newName = await window
.getAccountManager()
.decryptDeviceName(newNameEncrypted);
} catch (e) {
const deviceNameWasEncrypted =
window.textsecure.storage.user.getDeviceNameEncrypted();
log.error(
`fetchAndUpdateDeviceName: failed to decrypt device name. Was encrypted local state: ${deviceNameWasEncrypted}`
);
return;
}
const existingName = window.storage.user.getDeviceName();
if (newName === existingName) {
log.info('fetchAndUpdateDeviceName: new name matches existing name');
return;
}
await window.storage.user.setDeviceName(newName);
log.info(
'fetchAndUpdateDeviceName: successfully updated new device name locally'
);
}