Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"fs-extra": "11.3.0",
"glob": "10.4.5",
"image-type": "^4.1.0",
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.5.8/libsession_util_nodejs-v0.5.8.tar.gz",
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.5.9/libsession_util_nodejs-v0.5.9.tar.gz",
"libsodium-wrappers-sumo": "^0.7.15",
"linkify-it": "^5.0.0",
"lodash": "^4.17.21",
Expand Down
1 change: 1 addition & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ window.sessionFeatureFlags = {
replaceLocalizedStringsWithKeys: false,
// Hooks
useClosedGroupV2QAButtons: false, // TODO DO NOT MERGE
useDeterministicEncryption: !isEmpty(process.env.SESSION_ATTACH_DETERMINISTIC_ENCRYPTION),
useOnionRequests: true,
useTestNet: isTestNet() || isTestIntegration(),
useLocalDevNet: !isEmpty(process.env.LOCAL_DEVNET_SEED_URL)
Expand Down
36 changes: 18 additions & 18 deletions protos/SignalService.proto
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,11 @@ message DataMessage {
}

message Quote {

message QuotedAttachment {
optional string contentType = 1;
optional string fileName = 2;
optional AttachmentPointer thumbnail = 3;
}
reserved 3, 4;
reserved "text", "attachments";

required uint64 id = 1;
required string author = 2;
optional string text = 3;
repeated QuotedAttachment attachments = 4;
}

message Preview {
Expand Down Expand Up @@ -268,16 +262,22 @@ message AttachmentPointer {

// @required
required fixed64 deprecated_id = 1;
optional string contentType = 2;
optional bytes key = 3;
optional uint32 size = 4;
optional bytes digest = 6;
optional string fileName = 7;
optional uint32 flags = 8;
optional uint32 width = 9;
optional uint32 height = 10;
optional string caption = 11;
optional string url = 101;
optional string contentType = 2;
optional bytes key = 3;
optional uint32 size = 4;
optional bytes digest = 6;
optional string fileName = 7;
optional uint32 flags = 8;
optional uint32 width = 9;
optional uint32 height = 10;
optional string caption = 11;
/**
* This field can be just an url to the file, or have a fragment appended to it that can contain:
* - `p=<server_pubkey_hex>` // hex encoded pubkey of the file server
* - `d=` // if the file is deterministically encrypted, this field is present, otherwise it is not
* If needed, those fields are a &, and can be parsed/built with the usual URLSearchParams logic
*/
optional string url = 101;
}


Expand Down
4 changes: 2 additions & 2 deletions ts/components/conversation/composition/CompositionBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ export interface StagedLinkPreviewData {
scaledDown: ProcessedLinkPreviewThumbnailType | null;
}

export interface StagedAttachmentType extends AttachmentType {
export type StagedAttachmentType = AttachmentType & {
file: File;
path?: string; // a bit hacky, but this is the only way to make our sending audio message be playable, this must be used only for those message
}
};

export type SendMessageType = {
conversationId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ function formatAttachmentUrl(attachment: PropsForAttachment) {
return tr('attachmentsNa');
}

const fileId = attachment.url.split('/').pop() || '';
const fileUrl = URL.canParse(attachment.url) && new URL(attachment.url);
const fileId = fileUrl ? fileUrl?.pathname.split('/').pop() || '' : '';

if (!fileId) {
return tr('attachmentsNa');
Expand Down
32 changes: 3 additions & 29 deletions ts/components/leftpane/ActionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { DecryptedAttachmentsManager } from '../../session/crypto/DecryptedAttac

import { DURATION } from '../../session/constants';

import { reuploadCurrentAvatarUs } from '../../interactions/avatar-interactions/nts-avatar-interactions';
import {
onionPathModal,
updateDebugMenuModal,
Expand Down Expand Up @@ -52,14 +51,13 @@ import { useDebugMode } from '../../state/selectors/debug';
import { networkDataActions } from '../../state/ducks/networkData';
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
import { AvatarMigrate } from '../../session/utils/job_runners/jobs/AvatarMigrateJob';
import { NetworkTime } from '../../util/NetworkTime';
import { Storage } from '../../util/storage';
import { getFileInfoFromFileServer } from '../../session/apis/file_server_api/FileServerApi';
import { themesArray } from '../../themes/constants/colors';
import { isDebugMode, isDevProd } from '../../shared/env_vars';
import { GearAvatarButton } from '../buttons/avatar/GearAvatarButton';
import { useZoomShortcuts } from '../../hooks/useZoomingShortcut';
import { OnionStatusLight } from '../dialog/OnionStatusPathDialog';
import { AvatarReupload } from '../../session/utils/job_runners/jobs/AvatarReuploadJob';

const StyledContainerAvatar = styled.div`
padding: var(--margins-lg);
Expand Down Expand Up @@ -98,17 +96,6 @@ const triggerSyncIfNeeded = async () => {
}
};

const triggerAvatarReUploadIfNeeded = async () => {
const lastAvatarUploadExpiryMs =
(await Data.getItemById(SettingsKey.ntsAvatarExpiryMs))?.value || Number.MAX_SAFE_INTEGER;

if (NetworkTime.now() > lastAvatarUploadExpiryMs) {
window.log.info('Reuploading avatar...');
// reupload the avatar
await reuploadCurrentAvatarUs();
}
};

/**
* This function is called only once: on app startup with a logged in user
*/
Expand All @@ -127,9 +114,8 @@ const doAppStartUp = async () => {
}); // refresh our swarm on start to speed up the first message fetching event
void Data.cleanupOrphanedAttachments();

// TODOLATER make this a job of the JobRunner
// Note: do not make this a debounce call (as for some reason it doesn't work with promises)
void triggerAvatarReUploadIfNeeded();
await AvatarReupload.addAvatarReuploadJob();

/* Postpone a little bit of the polling of sogs messages to let the swarm messages come in first. */
global.setTimeout(() => {
Expand All @@ -147,17 +133,6 @@ const doAppStartUp = async () => {
// Schedule a confSyncJob in some time to let anything incoming from the network be applied and see if there is a push needed
// Note: this also starts periodic jobs, so we don't need to keep doing it
await UserSync.queueNewJobIfNeeded();

// on app startup, check that the avatar expiry on the file server
const avatarPointer = ConvoHub.use()
.get(UserUtils.getOurPubKeyStrFromCache())
.getAvatarPointer();
if (avatarPointer) {
const details = await getFileInfoFromFileServer(avatarPointer);
if (details?.expiryMs) {
await Storage.put(SettingsKey.ntsAvatarExpiryMs, details.expiryMs);
}
}
}, 20000);

global.setTimeout(() => {
Expand Down Expand Up @@ -283,8 +258,7 @@ export const ActionsPanel = () => {
if (!ourPrimaryConversation) {
return;
}
// this won't be run every days, but if the app stays open for more than 10 days
void triggerAvatarReUploadIfNeeded();
void AvatarReupload.addAvatarReuploadJob();
},
window.sessionFeatureFlags.fsTTL30s ? DURATION.SECONDS * 1 : DURATION.DAYS * 1
);
Expand Down
2 changes: 0 additions & 2 deletions ts/data/settings-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const settingsOpengroupPruning = 'prune-setting';
const settingsNotification = 'notification-setting';
const settingsAudioNotification = 'audio-notification-setting';
const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
const ntsAvatarExpiryMs = 'ntsAvatarExpiryMs';
const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed';
const hasFollowSystemThemeEnabled = 'hasFollowSystemThemeEnabled';
const hideRecoveryPassword = 'hideRecoveryPassword';
Expand Down Expand Up @@ -44,7 +43,6 @@ export const SettingsKey = {
settingsNotification,
settingsAudioNotification,
hasSyncedInitialConfigurationItem,
ntsAvatarExpiryMs,
hasLinkPreviewPopupBeenDisplayed,
latestUserProfileEnvelopeTimestamp,
latestUserGroupEnvelopeTimestamp,
Expand Down
116 changes: 42 additions & 74 deletions ts/interactions/avatar-interactions/nts-avatar-interactions.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,68 @@
import { isEmpty } from 'lodash';
import { SettingsKey } from '../../data/settings-key';
import { randombytes_buf } from 'libsodium-wrappers-sumo';

import { uploadFileToFsWithOnionV4 } from '../../session/apis/file_server_api/FileServerApi';
import { ConvoHub } from '../../session/conversations';
import { DecryptedAttachmentsManager } from '../../session/crypto/DecryptedAttachmentsManager';
import { UserUtils } from '../../session/utils';
import { fromHexToArray } from '../../session/utils/String';
import { urlToBlob } from '../../types/attachments/VisualAttachment';
import { processNewAttachment } from '../../types/MessageAttachment';
import { IMAGE_JPEG } from '../../types/MIME';
import { encryptProfile } from '../../util/crypto/profileEncrypter';
import { Storage } from '../../util/storage';
import type { ConversationModel } from '../../models/conversation';
import { processAvatarData } from '../../util/avatar/processAvatarData';
import { UserConfigWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';

/**
* This function can be used for reupload our avatar to the file server.
* It will reuse the same profileKey and avatarContent if we have some, or do nothing if one of those is missing.
*/
export async function reuploadCurrentAvatarUs() {
const ourConvo = ConvoHub.use().get(UserUtils.getOurPubKeyStrFromCache());
if (!ourConvo) {
window.log.warn('ourConvo not found... This is not a valid case');
return null;
}

// this is a reupload. no need to generate a new profileKey
const ourConvoProfileKey =
ConvoHub.use().get(UserUtils.getOurPubKeyStrFromCache())?.getProfileKey() || null;

const profileKey = ourConvoProfileKey ? fromHexToArray(ourConvoProfileKey) : null;
if (!profileKey || isEmpty(profileKey)) {
window.log.info('reuploadCurrentAvatarUs: our profileKey empty');

return null;
}
// Note: we do want to grab the current non-static avatar path here
// to reupload it, no matter if we are a pro user or not.
const currentNonStaticAvatarPath = ourConvo.getAvatarInProfilePath();

if (!currentNonStaticAvatarPath) {
window.log.info('No attachment currently set for our convo.. Nothing to do.');
return null;
}

const decryptedAvatarUrl = await DecryptedAttachmentsManager.getDecryptedMediaUrl(
currentNonStaticAvatarPath,
IMAGE_JPEG,
true
);

if (!decryptedAvatarUrl) {
window.log.warn('Could not decrypt avatar stored locally..');
return null;
}
const blob = await urlToBlob(decryptedAvatarUrl);

const decryptedAvatarData = await blob.arrayBuffer();

return uploadAndSetOurAvatarShared({
decryptedAvatarData,
ourConvo,
profileKey,
context: 'reuploadAvatar',
});
}
import {
MultiEncryptWrapperActions,
UserConfigWrapperActions,
} from '../../webworker/workers/browser/libsession_worker_interface';
import { UserUtils } from '../../session/utils';
import { fromBase64ToArray } from '../../session/utils/String';

export async function uploadAndSetOurAvatarShared({
decryptedAvatarData,
ourConvo,
profileKey,
context,
}: {
ourConvo: ConversationModel;
decryptedAvatarData: ArrayBuffer;
profileKey: Uint8Array;
context: 'uploadNewAvatar' | 'reuploadAvatar';
}) {
if (!decryptedAvatarData?.byteLength) {
window.log.warn('uploadAndSetOurAvatarShared: avatar content is empty');
return null;
}
// Note: we want to encrypt & upload the **processed** avatar
// below (resized & converted), not the original one.
const { avatarFallback, mainAvatarDetails } = await processAvatarData(decryptedAvatarData, true);

let encryptedData: ArrayBuffer;
let encryptionKey: Uint8Array;
const deterministicEncryption = window.sessionFeatureFlags?.useDeterministicEncryption;
if (deterministicEncryption) {
const encryptedContent = await MultiEncryptWrapperActions.attachmentEncrypt({
allowLarge: false,
seed: await UserUtils.getUserEd25519Seed(),
data: new Uint8Array(mainAvatarDetails.outputBuffer),
domain: 'profilePic',
});
encryptedData = encryptedContent.encryptedData;
encryptionKey = encryptedContent.encryptionKey;
} else {
// if this is a reupload, reuse the current profile key. Otherwise generate a new one
const existingProfileKey = ourConvo.getProfileKey();
const profileKey =
context === 'reuploadAvatar' && existingProfileKey
? fromBase64ToArray(existingProfileKey)
: randombytes_buf(32);
encryptedData = await encryptProfile(mainAvatarDetails.outputBuffer, profileKey);
encryptionKey = profileKey;
}

const encryptedData = await encryptProfile(decryptedAvatarData, profileKey);

const avatarPointer = await uploadFileToFsWithOnionV4(encryptedData);
const avatarPointer = await uploadFileToFsWithOnionV4(encryptedData, deterministicEncryption);
if (!avatarPointer) {
window.log.warn('failed to upload avatar to file server');
return null;
}
const { fileUrl, expiresMs } = avatarPointer;
// Note: we don't care about the expiry of the file anymore.
// This is because we renew the expiry of the file itself, and only when that fails we reupload the avatar content.
const { fileUrl } = avatarPointer;

// Note: processing the avatar here doesn't change the buffer (unless the first one was uploaded as an image too big for an avatar.)
// so, once we have deterministic encryption of avatars, the uploaded should always have the same hash
const { avatarFallback, mainAvatarDetails } = await processAvatarData(decryptedAvatarData);

// this encrypts and save the new avatar and returns a new attachment path
const savedMainAvatar = await processNewAttachment({
Expand All @@ -106,7 +75,7 @@ export async function uploadAndSetOurAvatarShared({
? await processNewAttachment({
isRaw: true,
data: avatarFallback.outputBuffer,
contentType: avatarFallback.contentType, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case.
contentType: avatarFallback.contentType,
})
: null;

Expand All @@ -118,17 +87,16 @@ export async function uploadAndSetOurAvatarShared({
displayName: null,
avatarPointer: fileUrl,
type: 'setAvatarDownloadedPrivate',
profileKey,
profileKey: encryptionKey,
});
await Storage.put(SettingsKey.ntsAvatarExpiryMs, expiresMs);
if (context === 'uploadNewAvatar') {
await UserConfigWrapperActions.setNewProfilePic({
key: profileKey,
key: encryptionKey,
url: fileUrl,
});
} else if (context === 'reuploadAvatar') {
await UserConfigWrapperActions.setReuploadProfilePic({
key: profileKey,
key: encryptionKey,
url: fileUrl,
});
}
Expand Down
3 changes: 1 addition & 2 deletions ts/models/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1772,9 +1772,8 @@ export class ConversationModel extends Model<ConversationAttributes> {
const updatedAtSeconds = this.getProfileUpdatedSeconds();

return new OutgoingUserProfile({
avatarPointer,
profilePic: { url: avatarPointer, key: profileKey ? from_hex(profileKey) : null },
displayName,
profileKey,
updatedAtSeconds,
});
}
Expand Down
Loading
Loading