mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-08-06 21:34:09 +01:00
Improve processing of sticker pack manifest
This commit is contained in:
@@ -4,6 +4,32 @@
|
||||
import { assert } from 'chai';
|
||||
import * as Stickers from '../../types/Stickers.preload.ts';
|
||||
import { isPackIdValid, redactPackId } from '../../util/Stickers.std.ts';
|
||||
import type { SignalService as Proto } from '../../protobuf/index.std.ts';
|
||||
|
||||
const PACK_ID = 'b9439fa5fdc8b9873fe64f01b88b8ccf';
|
||||
|
||||
function makeSticker(
|
||||
id: number,
|
||||
emoji: string | null = null
|
||||
): Proto.StickerPack.Sticker {
|
||||
return { $unknown: [], id, emoji };
|
||||
}
|
||||
|
||||
function makeManifest({
|
||||
cover = null,
|
||||
stickers = [],
|
||||
}: {
|
||||
cover?: Proto.StickerPack.Sticker | null;
|
||||
stickers?: Array<Proto.StickerPack.Sticker>;
|
||||
}): Proto.StickerPack {
|
||||
return {
|
||||
$unknown: [],
|
||||
title: 'title',
|
||||
author: 'author',
|
||||
cover,
|
||||
stickers,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Stickers', () => {
|
||||
describe('getDataFromLink', () => {
|
||||
@@ -129,7 +155,7 @@ describe('Stickers', () => {
|
||||
});
|
||||
|
||||
it('returns true for valid pack IDs', () => {
|
||||
assert.isTrue(isPackIdValid('b9439fa5fdc8b9873fe64f01b88b8ccf'));
|
||||
assert.isTrue(isPackIdValid(PACK_ID));
|
||||
assert.isTrue(isPackIdValid('3eff225a1036a58a7530b312dd92f8d8'));
|
||||
assert.isTrue(isPackIdValid('DDFD48B8097DA7A4E928192B10963F6A'));
|
||||
});
|
||||
@@ -143,4 +169,94 @@ describe('Stickers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseStickerPackManifest', () => {
|
||||
it('throws if the pack has no cover and no stickers', () => {
|
||||
assert.throws(
|
||||
() => Stickers.parseStickerPackManifest(PACK_ID, makeManifest({})),
|
||||
/no cover, and no stickers/
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates a pack with more stickers than the maximum', () => {
|
||||
const result = Stickers.parseStickerPackManifest(
|
||||
PACK_ID,
|
||||
makeManifest({
|
||||
stickers: Array.from(
|
||||
{ length: Stickers.MAX_STICKERS_PER_PACK * 10 },
|
||||
(_, id) => makeSticker(id)
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
assert.strictEqual(result.stickerCount, Stickers.MAX_STICKERS_PER_PACK);
|
||||
// The cover is the first sticker, so it isn't in nonCoverStickers
|
||||
assert.strictEqual(result.coverStickerId, 0);
|
||||
assert.strictEqual(
|
||||
result.nonCoverStickers.length,
|
||||
Stickers.MAX_STICKERS_PER_PACK - 1
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the first sticker as the cover', () => {
|
||||
const stickers = [makeSticker(0), makeSticker(1), makeSticker(2)];
|
||||
|
||||
const result = Stickers.parseStickerPackManifest(
|
||||
PACK_ID,
|
||||
makeManifest({ stickers })
|
||||
);
|
||||
|
||||
assert.strictEqual(result.coverStickerId, 0);
|
||||
assert.isTrue(result.coverIncludedInList);
|
||||
assert.strictEqual(result.stickerCount, 3);
|
||||
assert.deepEqual(
|
||||
result.nonCoverStickers.map(sticker => sticker.id),
|
||||
[1, 2]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles a cover that is not one of the stickers', () => {
|
||||
const result = Stickers.parseStickerPackManifest(
|
||||
PACK_ID,
|
||||
makeManifest({
|
||||
cover: makeSticker(99),
|
||||
stickers: [makeSticker(0), makeSticker(1)],
|
||||
})
|
||||
);
|
||||
|
||||
assert.strictEqual(result.coverStickerId, 99);
|
||||
assert.isFalse(result.coverIncludedInList);
|
||||
assert.deepEqual(
|
||||
result.nonCoverStickers.map(sticker => sticker.id),
|
||||
[0, 1]
|
||||
);
|
||||
});
|
||||
|
||||
it('takes the emoji from the sticker list if the cover has none', () => {
|
||||
const result = Stickers.parseStickerPackManifest(
|
||||
PACK_ID,
|
||||
makeManifest({
|
||||
cover: makeSticker(1),
|
||||
stickers: [makeSticker(1, '😀'), makeSticker(2, '😉')],
|
||||
})
|
||||
);
|
||||
|
||||
assert.strictEqual(result.coverProto.emoji, '😀');
|
||||
});
|
||||
|
||||
it('drops stickers with no id', () => {
|
||||
const result = Stickers.parseStickerPackManifest(
|
||||
PACK_ID,
|
||||
makeManifest({
|
||||
cover: makeSticker(0),
|
||||
stickers: [makeSticker(1), { $unknown: [], id: null, emoji: null }],
|
||||
})
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
result.nonCoverStickers.map(sticker => sticker.id),
|
||||
[1]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,9 @@ import {
|
||||
import { getExistingAttachmentDataForReuse } from '../util/attachments/deduplicateAttachment.preload.ts';
|
||||
import { Emoji } from '../axo/emoji.std.ts';
|
||||
|
||||
const { isNumber, reject, groupBy, values, chunk } = lodash;
|
||||
const { isNumber, groupBy, values, chunk } = lodash;
|
||||
|
||||
export const MAX_STICKERS_PER_PACK = 1024;
|
||||
|
||||
const log = createLogger('Stickers');
|
||||
|
||||
@@ -524,6 +526,57 @@ export async function removeEphemeralPack(packId: string): Promise<void> {
|
||||
await DataWriter.deleteStickerPack(packId);
|
||||
}
|
||||
|
||||
export function parseStickerPackManifest(
|
||||
packId: string,
|
||||
proto: Proto.StickerPack
|
||||
): {
|
||||
coverProto: Proto.StickerPack.Sticker.Params;
|
||||
coverStickerId: number;
|
||||
coverIncludedInList: boolean;
|
||||
nonCoverStickers: Array<Proto.StickerPack.Sticker.Params>;
|
||||
stickerCount: number;
|
||||
} {
|
||||
let { stickers } = proto;
|
||||
|
||||
if (stickers.length > MAX_STICKERS_PER_PACK) {
|
||||
log.warn(
|
||||
`parseStickerPackManifest: pack ${redactPackId(packId)} has ` +
|
||||
`${stickers.length} stickers, truncating to ${MAX_STICKERS_PER_PACK}`
|
||||
);
|
||||
stickers = stickers.slice(0, MAX_STICKERS_PER_PACK);
|
||||
}
|
||||
|
||||
const stickerCount = stickers.length;
|
||||
|
||||
const coverProto = proto.cover || stickers[0];
|
||||
const coverStickerId = dropNull(coverProto ? coverProto.id : undefined);
|
||||
|
||||
if (!coverProto || !isNumber(coverStickerId)) {
|
||||
throw new Error(
|
||||
`Sticker pack ${redactPackId(
|
||||
packId
|
||||
)} is malformed - it has no cover, and no stickers`
|
||||
);
|
||||
}
|
||||
|
||||
const coverSticker = stickers.find(sticker => sticker.id === coverStickerId);
|
||||
const nonCoverStickers = stickers.filter(
|
||||
sticker => sticker.id != null && sticker.id !== coverStickerId
|
||||
);
|
||||
|
||||
if (coverSticker && !coverProto.emoji) {
|
||||
coverProto.emoji = coverSticker.emoji;
|
||||
}
|
||||
|
||||
return {
|
||||
coverProto,
|
||||
coverStickerId,
|
||||
coverIncludedInList: nonCoverStickers.length < stickerCount,
|
||||
nonCoverStickers,
|
||||
stickerCount,
|
||||
};
|
||||
}
|
||||
|
||||
export async function downloadEphemeralPack(
|
||||
packId: string,
|
||||
packKey: string
|
||||
@@ -569,32 +622,13 @@ export async function downloadEphemeralPack(
|
||||
const ciphertext = await getStickerPackManifest(packId);
|
||||
const plaintext = decryptSticker(packKey, ciphertext);
|
||||
const proto = Proto.StickerPack.decode(plaintext);
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : null;
|
||||
const stickerCount = proto.stickers.length;
|
||||
|
||||
const coverProto = proto.cover || firstStickerProto;
|
||||
const coverStickerId = coverProto ? coverProto.id : null;
|
||||
|
||||
if (!coverProto || !isNumber(coverStickerId)) {
|
||||
throw new Error(
|
||||
`Sticker pack ${redactPackId(
|
||||
packId
|
||||
)} is malformed - it has no cover, and no stickers`
|
||||
);
|
||||
}
|
||||
|
||||
const nonCoverStickers = reject(
|
||||
proto.stickers,
|
||||
sticker => !isNumber(sticker.id) || sticker.id === coverStickerId
|
||||
);
|
||||
const coverSticker = proto.stickers.filter(
|
||||
sticker => isNumber(sticker.id) && sticker.id === coverStickerId
|
||||
);
|
||||
if (coverSticker[0] && !coverProto.emoji) {
|
||||
coverProto.emoji = coverSticker[0].emoji;
|
||||
}
|
||||
|
||||
const coverIncludedInList = nonCoverStickers.length < stickerCount;
|
||||
const {
|
||||
coverProto,
|
||||
coverStickerId,
|
||||
coverIncludedInList,
|
||||
nonCoverStickers,
|
||||
stickerCount,
|
||||
} = parseStickerPackManifest(packId, proto);
|
||||
|
||||
const pack = {
|
||||
...STICKER_PACK_DEFAULTS,
|
||||
@@ -812,32 +846,13 @@ async function doDownloadStickerPack(
|
||||
const ciphertext = await getStickerPackManifest(packId);
|
||||
const plaintext = decryptSticker(packKey, ciphertext);
|
||||
const proto = Proto.StickerPack.decode(plaintext);
|
||||
const firstStickerProto = proto.stickers ? proto.stickers[0] : undefined;
|
||||
const stickerCount = proto.stickers.length;
|
||||
const parsed = parseStickerPackManifest(packId, proto);
|
||||
const { stickerCount } = parsed;
|
||||
|
||||
coverProto = proto.cover || firstStickerProto;
|
||||
coverStickerId = dropNull(coverProto ? coverProto.id : undefined);
|
||||
|
||||
if (!coverProto || !isNumber(coverStickerId)) {
|
||||
throw new Error(
|
||||
`Sticker pack ${redactPackId(
|
||||
packId
|
||||
)} is malformed - it has no cover, and no stickers`
|
||||
);
|
||||
}
|
||||
|
||||
nonCoverStickers = reject(
|
||||
proto.stickers,
|
||||
sticker => !isNumber(sticker.id) || sticker.id === coverStickerId
|
||||
);
|
||||
const coverSticker = proto.stickers.filter(
|
||||
sticker => isNumber(sticker.id) && sticker.id === coverStickerId
|
||||
);
|
||||
if (coverSticker[0] && !coverProto.emoji) {
|
||||
coverProto.emoji = coverSticker[0].emoji;
|
||||
}
|
||||
|
||||
coverIncludedInList = nonCoverStickers.length < stickerCount;
|
||||
coverProto = parsed.coverProto;
|
||||
coverStickerId = parsed.coverStickerId;
|
||||
coverIncludedInList = parsed.coverIncludedInList;
|
||||
nonCoverStickers = parsed.nonCoverStickers;
|
||||
|
||||
// status can be:
|
||||
// - 'known'
|
||||
|
||||
Reference in New Issue
Block a user