// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import createDebug from 'debug'; import type { Group, PrimaryDevice, Server, StorageStateRecord, } from '@signalapp/mock-server'; import { StorageState, Proto } from '@signalapp/mock-server'; import path from 'path'; import fs from 'fs/promises'; import { range } from 'lodash'; import { CallLinkRootKey } from '@signalapp/ringrtc'; import { App } from '../playwright'; import { Bootstrap } from '../bootstrap'; import type { BootstrapOptions } from '../bootstrap'; import { MY_STORY_ID } from '../../types/Stories'; import { uuidToBytes } from '../../util/uuidToBytes'; import { artAddStickersRoute } from '../../util/signalRoutes'; import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc'; export const debug = createDebug('mock:test:storage'); export { App, Bootstrap }; const GROUP_SIZE = 8; const IdentifierType = Proto.ManifestRecord.Identifier.Type; export type InitStorageResultType = Readonly<{ bootstrap: Bootstrap; app: App; group: Group; members: ReadonlyArray; }>; // // This function creates an initial storage service state that includes: // // - All contacts from contact sync (first contact pinned) // - A pinned group with GROUP_SIZE members (from the contacts) // - Account with e164 and profileKey // // In addition to above, this function will queue one incoming message in the // group, and one for the first contact (so that both will appear in the left // pane). export async function initStorage( options?: BootstrapOptions ): Promise { // Creates primary device, contacts const bootstrap = new Bootstrap(options); await bootstrap.init(); try { // Populate storage service const { contacts, phone } = bootstrap; const [firstContact] = contacts; const members = [...contacts].slice(0, GROUP_SIZE); const group = await phone.createGroup({ title: 'Mock Group', members: [phone, ...members], }); let state = StorageState.getEmpty(); state = state.updateAccount({ profileKey: phone.profileKey.serialize(), e164: phone.device.number, givenName: phone.profileName, }); state = state .addGroup(group, { whitelisted: true, }) .pinGroup(group); for (const contact of contacts) { state = state.addContact(contact, { identityState: Proto.ContactRecord.IdentityState.VERIFIED, whitelisted: true, identityKey: contact.publicKey.serialize(), profileKey: contact.profileKey.serialize(), givenName: contact.profileName, }); } state = state.pin(firstContact); state = state.addRecord({ type: IdentifierType.STORY_DISTRIBUTION_LIST, record: { storyDistributionList: { allowsReplies: true, identifier: uuidToBytes(MY_STORY_ID), isBlockList: true, name: MY_STORY_ID, recipientServiceIds: [], }, }, }); await phone.setStorageState(state); // Link new device const app = await bootstrap.link(); const { desktop } = bootstrap; // Send a message to the group and the first contact const contactSend = contacts[0].sendText(desktop, 'hello from contact', { timestamp: bootstrap.getTimestamp(), sealed: true, }); const groupSend = members[0].sendText(desktop, 'hello in group', { timestamp: bootstrap.getTimestamp(), sealed: true, group, }); await Promise.all([contactSend, groupSend]); return { bootstrap, app, group, members }; } catch (error) { await bootstrap.saveLogs(); throw error; } } export const FIXTURES = path.join(__dirname, '..', '..', '..', 'fixtures'); export const EMPTY = new Uint8Array(0); export type StickerPackType = Readonly<{ id: Buffer; key: Buffer; stickerCount: number; }>; export const STICKER_PACKS: ReadonlyArray = [ { id: Buffer.from('c40ed069cdc2b91eccfccf25e6bcddfc', 'hex'), key: Buffer.from( 'cefadd6e81c128680aead1711eb5c92c10f63bdfbc78528a4519ba682de396e4', 'hex' ), stickerCount: 1, }, { id: Buffer.from('ae8fedafda4768fd3384d4b3b9db963d', 'hex'), key: Buffer.from( '53f4aa8b95e1c2e75afab2328fe67eb6d7affbcd4f50cd4da89dfc325dbc73ca', 'hex' ), stickerCount: 1, }, ]; export function getStickerPackLink(pack: StickerPackType): string { return artAddStickersRoute .toWebUrl({ packId: pack.id.toString('hex'), packKey: pack.key.toString('hex'), }) .toString(); } export function getStickerPackRecordPredicate( pack: StickerPackType ): (record: StorageStateRecord) => boolean { return ({ type, record }: StorageStateRecord): boolean => { if (type !== IdentifierType.STICKER_PACK) { return false; } return pack.id.equals(record.stickerPack?.packId ?? EMPTY); }; } export async function storeStickerPacks( server: Server, stickerPacks: ReadonlyArray ): Promise { await Promise.all( stickerPacks.map(async ({ id, stickerCount }) => { const hexId = id.toString('hex'); await server.storeStickerPack({ id, manifest: await fs.readFile( path.join(FIXTURES, `stickerpack-${hexId}.bin`) ), stickers: await Promise.all( range(0, stickerCount).map(async index => fs.readFile( path.join(FIXTURES, `stickerpack-${hexId}-${index}.bin`) ) ) ), }); }) ); } export function getCallLinkRecordPredicate( roomId: string ): (record: StorageStateRecord) => boolean { return ({ type, record }: StorageStateRecord): boolean => { const rootKeyBytes = record.callLink?.rootKey; if (type !== IdentifierType.CALL_LINK || rootKeyBytes == null) { return false; } const recordRootKey = CallLinkRootKey.fromBytes(Buffer.from(rootKeyBytes)); const recordRoomId = getRoomIdFromRootKey(recordRootKey); return roomId === recordRoomId; }; }