mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
Implement backup support for chat folders
This commit is contained in:
@@ -58,6 +58,10 @@ import type { SmartPreferencesEditChatFolderPageProps } from '../state/smart/Pre
|
|||||||
import { CurrentChatFolders } from '../types/CurrentChatFolders.std.js';
|
import { CurrentChatFolders } from '../types/CurrentChatFolders.std.js';
|
||||||
import type { ExternalProps as SmartNotificationProfilesProps } from '../state/smart/PreferencesNotificationProfiles.preload.js';
|
import type { ExternalProps as SmartNotificationProfilesProps } from '../state/smart/PreferencesNotificationProfiles.preload.js';
|
||||||
import type { NotificationProfileIdString } from '../types/NotificationProfile.std.js';
|
import type { NotificationProfileIdString } from '../types/NotificationProfile.std.js';
|
||||||
|
import type {
|
||||||
|
ExportResultType,
|
||||||
|
LocalBackupExportResultType,
|
||||||
|
} from '../services/backups/types.std.js';
|
||||||
import { BackupLevel } from '../services/backups/types.std.js';
|
import { BackupLevel } from '../services/backups/types.std.js';
|
||||||
|
|
||||||
const { shuffle } = lodash;
|
const { shuffle } = lodash;
|
||||||
@@ -112,13 +116,14 @@ const availableSpeakers = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const validateBackupResult = {
|
const validateBackupResult: ExportResultType = {
|
||||||
totalBytes: 100,
|
totalBytes: 100,
|
||||||
duration: 10000,
|
duration: 10000,
|
||||||
stats: {
|
stats: {
|
||||||
adHocCalls: 1,
|
adHocCalls: 1,
|
||||||
callLinks: 2,
|
callLinks: 2,
|
||||||
conversations: 3,
|
conversations: 3,
|
||||||
|
chatFolders: 3,
|
||||||
chats: 4,
|
chats: 4,
|
||||||
distributionLists: 5,
|
distributionLists: 5,
|
||||||
messages: 6,
|
messages: 6,
|
||||||
@@ -129,7 +134,7 @@ const validateBackupResult = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportLocalBackupResult = {
|
const exportLocalBackupResult: LocalBackupExportResultType = {
|
||||||
...validateBackupResult,
|
...validateBackupResult,
|
||||||
snapshotDir: '/home/signaluser/SignalBackups/signal-backup-1745618069169',
|
snapshotDir: '/home/signaluser/SignalBackups/signal-backup-1745618069169',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ import type {
|
|||||||
AboutMe,
|
AboutMe,
|
||||||
BackupExportOptions,
|
BackupExportOptions,
|
||||||
LocalChatStyle,
|
LocalChatStyle,
|
||||||
|
StatsType,
|
||||||
} from './types.std.js';
|
} from './types.std.js';
|
||||||
import { messageHasPaymentEvent } from '../../messages/payments.std.js';
|
import { messageHasPaymentEvent } from '../../messages/payments.std.js';
|
||||||
import {
|
import {
|
||||||
@@ -177,6 +178,7 @@ import {
|
|||||||
} from '../../util/Settings.preload.js';
|
} from '../../util/Settings.preload.js';
|
||||||
import { KIBIBYTE } from '../../types/AttachmentSize.std.js';
|
import { KIBIBYTE } from '../../types/AttachmentSize.std.js';
|
||||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||||
|
import { ChatFolderType } from '../../types/ChatFolder.std.js';
|
||||||
|
|
||||||
const { isNumber } = lodash;
|
const { isNumber } = lodash;
|
||||||
|
|
||||||
@@ -242,19 +244,6 @@ type NonBubbleResultType = Readonly<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type StatsType = {
|
|
||||||
adHocCalls: number;
|
|
||||||
callLinks: number;
|
|
||||||
conversations: number;
|
|
||||||
chats: number;
|
|
||||||
distributionLists: number;
|
|
||||||
messages: number;
|
|
||||||
notificationProfiles: number;
|
|
||||||
skippedMessages: number;
|
|
||||||
stickerPacks: number;
|
|
||||||
fixedDirectMessages: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class BackupExportStream extends Readable {
|
export class BackupExportStream extends Readable {
|
||||||
// Shared between all methods for consistency.
|
// Shared between all methods for consistency.
|
||||||
#now = Date.now();
|
#now = Date.now();
|
||||||
@@ -269,6 +258,7 @@ export class BackupExportStream extends Readable {
|
|||||||
adHocCalls: 0,
|
adHocCalls: 0,
|
||||||
callLinks: 0,
|
callLinks: 0,
|
||||||
conversations: 0,
|
conversations: 0,
|
||||||
|
chatFolders: 0,
|
||||||
chats: 0,
|
chats: 0,
|
||||||
distributionLists: 0,
|
distributionLists: 0,
|
||||||
messages: 0,
|
messages: 0,
|
||||||
@@ -728,6 +718,42 @@ export class BackupExportStream extends Readable {
|
|||||||
this.#stats.notificationProfiles += 1;
|
this.#stats.notificationProfiles += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentChatFolders = await DataReader.getCurrentChatFolders();
|
||||||
|
|
||||||
|
for (const chatFolder of currentChatFolders) {
|
||||||
|
let folderType: Backups.ChatFolder.FolderType;
|
||||||
|
if (chatFolder.folderType === ChatFolderType.ALL) {
|
||||||
|
folderType = Backups.ChatFolder.FolderType.ALL;
|
||||||
|
} else if (chatFolder.folderType === ChatFolderType.CUSTOM) {
|
||||||
|
folderType = Backups.ChatFolder.FolderType.CUSTOM;
|
||||||
|
} else {
|
||||||
|
log.warn('backups: Dropping chat folder; unknown folder type');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#pushFrame({
|
||||||
|
chatFolder: {
|
||||||
|
id: uuidToBytes(chatFolder.id),
|
||||||
|
name: chatFolder.name,
|
||||||
|
folderType,
|
||||||
|
showOnlyUnread: chatFolder.showOnlyUnread,
|
||||||
|
showMutedChats: chatFolder.showMutedChats,
|
||||||
|
includeAllIndividualChats: chatFolder.includeAllIndividualChats,
|
||||||
|
includeAllGroupChats: chatFolder.includeAllGroupChats,
|
||||||
|
includedRecipientIds: chatFolder.includedConversationIds.map(id => {
|
||||||
|
return this.#getOrPushPrivateRecipient({ id });
|
||||||
|
}),
|
||||||
|
excludedRecipientIds: chatFolder.excludedConversationIds.map(id => {
|
||||||
|
return this.#getOrPushPrivateRecipient({ id });
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.#flush();
|
||||||
|
this.#stats.chatFolders += 1;
|
||||||
|
}
|
||||||
|
|
||||||
let cursor: PageMessagesCursorType | undefined;
|
let cursor: PageMessagesCursorType | undefined;
|
||||||
|
|
||||||
const callHistory = await DataReader.getAllCallHistory();
|
const callHistory = await DataReader.getAllCallHistory();
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ import {
|
|||||||
import { normalizeNotificationProfileId } from '../../types/NotificationProfile-node.node.js';
|
import { normalizeNotificationProfileId } from '../../types/NotificationProfile-node.node.js';
|
||||||
import { updateBackupMediaDownloadProgress } from '../../util/updateBackupMediaDownloadProgress.preload.js';
|
import { updateBackupMediaDownloadProgress } from '../../util/updateBackupMediaDownloadProgress.preload.js';
|
||||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||||
|
import { ChatFolderType } from '../../types/ChatFolder.std.js';
|
||||||
|
import type { ChatFolderId, ChatFolder } from '../../types/ChatFolder.std.js';
|
||||||
|
|
||||||
const { isNumber } = lodash;
|
const { isNumber } = lodash;
|
||||||
|
|
||||||
@@ -543,9 +545,7 @@ export class BackupImportStream extends Writable {
|
|||||||
} else if (frame.notificationProfile) {
|
} else if (frame.notificationProfile) {
|
||||||
await this.#fromNotificationProfile(frame.notificationProfile);
|
await this.#fromNotificationProfile(frame.notificationProfile);
|
||||||
} else if (frame.chatFolder) {
|
} else if (frame.chatFolder) {
|
||||||
log.warn(
|
await this.#fromChatFolder(frame.chatFolder);
|
||||||
`${this.#logId}: Received currently unsupported feature: chat folder. Dropping.`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn(
|
||||||
`${this.#logId}: unknown unsupported frame item ${frame.item}`
|
`${this.#logId}: unknown unsupported frame item ${frame.item}`
|
||||||
@@ -3634,6 +3634,68 @@ export class BackupImportStream extends Writable {
|
|||||||
await DataWriter.createNotificationProfile(profile);
|
await DataWriter.createNotificationProfile(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chatFolderPositionCursor = 0;
|
||||||
|
|
||||||
|
async #fromChatFolder(proto: Backups.IChatFolder): Promise<void> {
|
||||||
|
if (proto.id == null || proto.id.length === 0) {
|
||||||
|
log.warn('Dropping chat folder; it was missing an id');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = bytesToUuid(proto.id) as ChatFolderId | undefined;
|
||||||
|
if (id == null) {
|
||||||
|
log.warn('Dropping chat folder; invalid uuid bytes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let folderType: ChatFolderType;
|
||||||
|
if (proto.folderType === Backups.ChatFolder.FolderType.ALL) {
|
||||||
|
folderType = ChatFolderType.ALL;
|
||||||
|
} else if (proto.folderType === Backups.ChatFolder.FolderType.CUSTOM) {
|
||||||
|
folderType = ChatFolderType.CUSTOM;
|
||||||
|
} else {
|
||||||
|
log.warn('Dropping chat folder; unknown folder type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = this.#chatFolderPositionCursor;
|
||||||
|
this.#chatFolderPositionCursor += 1;
|
||||||
|
|
||||||
|
const includedRecipientIds = proto.includedRecipientIds ?? [];
|
||||||
|
const excludedRecipientIds = proto.excludedRecipientIds ?? [];
|
||||||
|
|
||||||
|
const chatFolder: ChatFolder = {
|
||||||
|
id,
|
||||||
|
name: proto.name ?? '',
|
||||||
|
folderType,
|
||||||
|
showOnlyUnread: proto.showOnlyUnread ?? false,
|
||||||
|
showMutedChats: proto.showMutedChats ?? false,
|
||||||
|
includeAllIndividualChats: proto.includeAllIndividualChats ?? false,
|
||||||
|
includeAllGroupChats: proto.includeAllGroupChats ?? false,
|
||||||
|
includedConversationIds: includedRecipientIds.map(recipientId => {
|
||||||
|
const convo = this.#recipientIdToConvo.get(recipientId.toNumber());
|
||||||
|
strictAssert(convo != null, 'Missing chat folder included recipient');
|
||||||
|
return convo.id;
|
||||||
|
}),
|
||||||
|
excludedConversationIds: excludedRecipientIds.map(recipientId => {
|
||||||
|
const convo = this.#recipientIdToConvo.get(recipientId.toNumber());
|
||||||
|
strictAssert(convo != null, 'Missing chat folder included recipient');
|
||||||
|
return convo.id;
|
||||||
|
}),
|
||||||
|
position,
|
||||||
|
deletedAtTimestampMs: 0,
|
||||||
|
storageID: null,
|
||||||
|
storageVersion: null,
|
||||||
|
storageUnknownFields: null,
|
||||||
|
storageNeedsSync: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (folderType === ChatFolderType.ALL) {
|
||||||
|
await DataWriter.upsertAllChatsChatFolderFromSync(chatFolder);
|
||||||
|
} else {
|
||||||
|
await DataWriter.createChatFolder(chatFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async #fromCustomChatColors(
|
async #fromCustomChatColors(
|
||||||
customChatColors:
|
customChatColors:
|
||||||
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
|
| ReadonlyArray<Backups.ChatStyle.ICustomChatColor>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { measureSize } from '../../AttachmentCrypto.node.js';
|
|||||||
import { signalProtocolStore } from '../../SignalProtocolStore.preload.js';
|
import { signalProtocolStore } from '../../SignalProtocolStore.preload.js';
|
||||||
import { isTestOrMockEnvironment } from '../../environment.std.js';
|
import { isTestOrMockEnvironment } from '../../environment.std.js';
|
||||||
import { runStorageServiceSyncJob } from '../storage.preload.js';
|
import { runStorageServiceSyncJob } from '../storage.preload.js';
|
||||||
import { BackupExportStream, type StatsType } from './export.preload.js';
|
import { BackupExportStream } from './export.preload.js';
|
||||||
import { BackupImportStream } from './import.preload.js';
|
import { BackupImportStream } from './import.preload.js';
|
||||||
import {
|
import {
|
||||||
getBackupId,
|
getBackupId,
|
||||||
@@ -69,7 +69,12 @@ import {
|
|||||||
validateBackupStream,
|
validateBackupStream,
|
||||||
ValidationType,
|
ValidationType,
|
||||||
} from './validator.preload.js';
|
} from './validator.preload.js';
|
||||||
import type { BackupExportOptions, BackupImportOptions } from './types.std.js';
|
import type {
|
||||||
|
BackupExportOptions,
|
||||||
|
BackupImportOptions,
|
||||||
|
ExportResultType,
|
||||||
|
LocalBackupExportResultType,
|
||||||
|
} from './types.std.js';
|
||||||
import {
|
import {
|
||||||
BackupInstallerError,
|
BackupInstallerError,
|
||||||
BackupDownloadFailedError,
|
BackupDownloadFailedError,
|
||||||
@@ -126,16 +131,6 @@ type DoDownloadOptionsType = Readonly<{
|
|||||||
) => void;
|
) => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ExportResultType = Readonly<{
|
|
||||||
totalBytes: number;
|
|
||||||
duration: number;
|
|
||||||
stats: Readonly<StatsType>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type LocalBackupExportResultType = ExportResultType & {
|
|
||||||
snapshotDir: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ValidationResultType = Readonly<
|
export type ValidationResultType = Readonly<
|
||||||
| {
|
| {
|
||||||
result: ExportResultType | LocalBackupExportResultType;
|
result: ExportResultType | LocalBackupExportResultType;
|
||||||
|
|||||||
@@ -50,3 +50,27 @@ export type LocalChatStyle = Readonly<{
|
|||||||
dimWallpaperInDarkMode: boolean | undefined;
|
dimWallpaperInDarkMode: boolean | undefined;
|
||||||
autoBubbleColor: boolean | undefined;
|
autoBubbleColor: boolean | undefined;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type StatsType = {
|
||||||
|
adHocCalls: number;
|
||||||
|
callLinks: number;
|
||||||
|
conversations: number;
|
||||||
|
chatFolders: number;
|
||||||
|
chats: number;
|
||||||
|
distributionLists: number;
|
||||||
|
messages: number;
|
||||||
|
notificationProfiles: number;
|
||||||
|
skippedMessages: number;
|
||||||
|
stickerPacks: number;
|
||||||
|
fixedDirectMessages: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExportResultType = Readonly<{
|
||||||
|
totalBytes: number;
|
||||||
|
duration: number;
|
||||||
|
stats: Readonly<StatsType>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type LocalBackupExportResultType = ExportResultType & {
|
||||||
|
snapshotDir: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -80,8 +80,6 @@ describe('backup/integration', () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
expectedString.includes('ReleaseChannelDonationRequest') ||
|
expectedString.includes('ReleaseChannelDonationRequest') ||
|
||||||
// TODO (DESKTOP-8025) roundtrip these frames
|
|
||||||
fullPath.includes('chat_folder') ||
|
|
||||||
// TODO (DESKTOP-9209) roundtrip these frames when feature is added
|
// TODO (DESKTOP-9209) roundtrip these frames when feature is added
|
||||||
fullPath.includes('poll_terminate')
|
fullPath.includes('poll_terminate')
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user