signal-desktop/ts/services/writeProfile.ts

155 lines
4.5 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2021 Signal Messenger, LLC
2021-07-19 19:26:06 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
2024-07-22 18:16:33 +00:00
import { DataWriter } from '../sql/Client';
import type { ConversationType } from '../state/ducks/conversations';
2022-01-26 21:58:00 +00:00
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { computeHash } from '../Crypto';
2021-07-19 19:26:06 +00:00
import { encryptProfileData } from '../util/encryptProfileData';
import { getProfile } from '../util/getProfile';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
2022-01-26 21:58:00 +00:00
import { strictAssert } from '../util/assert';
import { isWhitespace } from '../util/whitespaceStringUtil';
import { imagePathToBytes } from '../util/imagePathToBytes';
2024-07-11 19:44:09 +00:00
import { getLocalAvatarUrl } from '../util/avatarUtils';
import type {
AvatarUpdateOptionsType,
AvatarUpdateType,
} from '../types/Avatar';
import MessageSender from '../textsecure/SendMessage';
2021-07-19 19:26:06 +00:00
export async function writeProfile(
conversation: ConversationType,
options: AvatarUpdateOptionsType
2021-07-19 19:26:06 +00:00
): Promise<void> {
const { server } = window.textsecure;
if (!server) {
throw new Error('server is not available!');
}
2021-07-19 19:26:06 +00:00
// Before we write anything we request the user's profile so that we can
// have an up-to-date paymentAddress to be able to include it when we write
const model = window.ConversationController.get(conversation.id);
if (!model) {
return;
}
await getProfile({
serviceId: model.getServiceId() ?? null,
e164: model.get('e164') ?? null,
groupId: null,
});
2021-07-19 19:26:06 +00:00
// Encrypt the profile data, update profile, and if needed upload the avatar
const {
aboutEmoji,
aboutText,
avatarHash,
2024-07-11 19:44:09 +00:00
rawAvatarPath,
2021-07-19 19:26:06 +00:00
familyName,
firstName,
} = conversation;
2022-01-26 21:58:00 +00:00
strictAssert(
!isWhitespace(String(conversation.firstName)),
'writeProfile: Cannot set an empty profile name'
);
let avatarUpdate: AvatarUpdateType;
if (options.keepAvatar) {
2024-07-11 19:44:09 +00:00
const profileAvatarUrl = getLocalAvatarUrl(model.attributes);
let avatarBuffer: Uint8Array | undefined;
2024-07-11 19:44:09 +00:00
if (profileAvatarUrl) {
try {
2024-07-11 19:44:09 +00:00
avatarBuffer = await imagePathToBytes(profileAvatarUrl);
} catch (error) {
log.warn('writeProfile: local avatar not found, dropping remote');
}
}
avatarUpdate = {
oldAvatar: avatarBuffer,
newAvatar: avatarBuffer,
};
} else {
avatarUpdate = options.avatarUpdate;
}
2021-07-19 19:26:06 +00:00
const [profileData, encryptedAvatarData] = await encryptProfileData(
conversation,
avatarUpdate
2021-07-19 19:26:06 +00:00
);
const avatarRequestHeaders = await server.putProfile(profileData);
2021-07-19 19:26:06 +00:00
// Upload the avatar if provided
// delete existing files on disk if avatar has been removed
// update the account's avatar path and hash if it's a new avatar
const { newAvatar } = avatarUpdate;
let maybeProfileAvatarUpdate: {
profileAvatar?:
| {
hash: string;
path: string;
}
| undefined;
} = {};
if (profileData.sameAvatar) {
log.info('writeProfile: not updating avatar');
} else if (avatarRequestHeaders && encryptedAvatarData && newAvatar) {
log.info('writeProfile: uploading new avatar');
const avatarUrl = await server.uploadAvatar(
2021-07-19 19:26:06 +00:00
avatarRequestHeaders,
encryptedAvatarData
);
const hash = await computeHash(newAvatar);
2021-07-19 19:26:06 +00:00
if (hash !== avatarHash) {
log.info('writeProfile: removing old avatar and saving the new one');
2024-07-11 19:44:09 +00:00
const [local] = await Promise.all([
window.Signal.Migrations.writeNewAttachmentData(newAvatar),
2024-07-11 19:44:09 +00:00
rawAvatarPath
? window.Signal.Migrations.deleteAttachmentData(rawAvatarPath)
2021-07-19 19:26:06 +00:00
: undefined,
]);
maybeProfileAvatarUpdate = {
2024-07-11 19:44:09 +00:00
profileAvatar: { hash, ...local },
2021-07-19 19:26:06 +00:00
};
}
await window.storage.put('avatarUrl', avatarUrl);
2024-07-11 19:44:09 +00:00
} else if (rawAvatarPath) {
log.info('writeProfile: removing avatar');
await Promise.all([
2024-07-11 19:44:09 +00:00
window.Signal.Migrations.deleteAttachmentData(rawAvatarPath),
window.storage.put('avatarUrl', undefined),
]);
2021-07-19 19:26:06 +00:00
maybeProfileAvatarUpdate = { profileAvatar: undefined };
}
2021-07-19 19:26:06 +00:00
// Update backbone, update DB, run storage service upload
model.set({
about: aboutText,
aboutEmoji,
profileName: firstName,
profileFamilyName: familyName,
...maybeProfileAvatarUpdate,
2021-07-19 19:26:06 +00:00
});
2024-07-22 18:16:33 +00:00
await DataWriter.updateConversation(model.attributes);
2021-07-19 19:26:06 +00:00
model.captureChange('writeProfile');
try {
await singleProtoJobQueue.add(
MessageSender.getFetchLocalProfileSyncMessage()
);
} catch (error) {
log.error(
'writeProfile: Failed to queue sync message',
Errors.toLogFormat(error)
);
}
2021-07-19 19:26:06 +00:00
}