Import distribution lists from backup

This commit is contained in:
Fedor Indutny 2024-05-07 09:47:46 -07:00 committed by GitHub
parent 3e51e4ef5d
commit 7cd07eb7b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 122 additions and 1 deletions

View file

@ -13,6 +13,7 @@ import * as log from '../../logging/log';
import { StorySendMode } from '../../types/Stories';
import type { ServiceIdString } from '../../types/ServiceId';
import { fromAciObject, fromPniObject } from '../../types/ServiceId';
import { isStoryDistributionId } from '../../types/StoryDistributionId';
import * as Errors from '../../types/errors';
import type {
ConversationAttributesType,
@ -32,6 +33,8 @@ import { isAciString } from '../../util/isAciString';
import { createBatcher } from '../../util/batcher';
import { PhoneNumberDiscoverability } from '../../util/phoneNumberDiscoverability';
import { PhoneNumberSharingMode } from '../../util/phoneNumberSharingMode';
import { bytesToUuid } from '../../util/uuidToBytes';
import { missingCaseError } from '../../util/missingCaseError';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SendStatus } from '../../messages/MessageSendState';
import type { SendStateByConversationId } from '../../messages/MessageSendState';
@ -237,6 +240,11 @@ export class BackupImportStream extends Writable {
convo = this.ourConversation;
} else if (recipient.group) {
convo = await this.fromGroup(recipient.group);
} else if (recipient.distributionList) {
await this.fromDistributionList(recipient.distributionList);
// Not a conversation
return;
} else {
log.warn(`${this.logId}: unsupported recipient item`);
return;
@ -504,6 +512,76 @@ export class BackupImportStream extends Writable {
return attrs;
}
private async fromDistributionList(
list: Backups.IDistributionList
): Promise<void> {
strictAssert(
Bytes.isNotEmpty(list.distributionId),
'Missing distribution list id'
);
const id = bytesToUuid(list.distributionId);
strictAssert(isStoryDistributionId(id), 'Invalid distribution list id');
strictAssert(
list.privacyMode != null,
'Missing distribution list privacy mode'
);
let isBlockList: boolean;
const { PrivacyMode } = Backups.DistributionList;
switch (list.privacyMode) {
case PrivacyMode.ALL:
strictAssert(
!list.memberRecipientIds?.length,
'Distribution list with ALL privacy mode has members'
);
isBlockList = true;
break;
case PrivacyMode.ALL_EXCEPT:
strictAssert(
list.memberRecipientIds?.length,
'Distribution list with ALL_EXCEPT privacy mode has no members'
);
isBlockList = true;
break;
case PrivacyMode.ONLY_WITH:
isBlockList = false;
break;
case PrivacyMode.UNKNOWN:
throw new Error('Invalid privacy mode for distribution list');
default:
throw missingCaseError(list.privacyMode);
}
const result = {
id,
name: list.name ?? '',
deletedAtTimestamp:
list.deletionTimestamp == null
? undefined
: getTimestampFromLong(list.deletionTimestamp),
allowsReplies: list.allowReplies === true,
isBlockList,
members: (list.memberRecipientIds || []).map(recipientId => {
const convo = this.recipientIdToConvo.get(recipientId.toNumber());
strictAssert(convo != null, 'Missing story distribution list member');
strictAssert(
convo.serviceId,
'Story distribution list member has no serviceId'
);
return convo.serviceId;
}),
// Default values
senderKeyInfo: undefined,
storageNeedsSync: false,
};
await Data.createNewStoryDistribution(result);
}
private async fromChat(chat: Backups.IChat): Promise<void> {
strictAssert(chat.id != null, 'chat must have an id');
strictAssert(chat.recipientId != null, 'chat must have a recipientId');

View file

@ -3,14 +3,21 @@
import createDebug from 'debug';
import Long from 'long';
import { StorageState } from '@signalapp/mock-server';
import { Proto, StorageState } from '@signalapp/mock-server';
import { generateStoryDistributionId } from '../../types/StoryDistributionId';
import { MY_STORY_ID } from '../../types/Stories';
import { uuidToBytes } from '../../util/uuidToBytes';
import * as durations from '../../util/durations';
import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap';
export const debug = createDebug('mock:test:backups');
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
const DISTRIBUTION1 = generateStoryDistributionId();
describe('backups', function (this: Mocha.Suite) {
this.timeout(100 * durations.MINUTE);
@ -46,6 +53,33 @@ describe('backups', function (this: Mocha.Suite) {
state = state.pin(pinned);
// Create empty My Story
state = state.addRecord({
type: IdentifierType.STORY_DISTRIBUTION_LIST,
record: {
storyDistributionList: {
allowsReplies: true,
identifier: uuidToBytes(MY_STORY_ID),
isBlockList: true,
name: MY_STORY_ID,
recipientServiceIds: [pinned.device.aci],
},
},
});
state = state.addRecord({
type: IdentifierType.STORY_DISTRIBUTION_LIST,
record: {
storyDistributionList: {
allowsReplies: true,
identifier: uuidToBytes(DISTRIBUTION1),
isBlockList: false,
name: 'friend',
recipientServiceIds: [friend.device.aci],
},
},
});
await phone.setStorageState(state);
app = await bootstrap.link();
@ -138,6 +172,15 @@ describe('backups', function (this: Mocha.Suite) {
.waitFor();
await snapshot('conversation');
debug('Switching to stories nav tab');
await window.getByTestId('NavTabsItem--Stories').click();
debug('Opening story privacy');
await window.locator('.StoriesTab__MoreActionsIcon').click();
await window.getByRole('button', { name: 'Story Privacy' }).click();
await snapshot('story privacy');
},
this.test
);