Signal-Desktop/ts/badges/parseBadgesFromServer.ts

124 lines
3.1 KiB
TypeScript

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as z from 'zod';
import { isEmpty } from 'lodash';
import { isRecord } from '../util/isRecord';
import { isNormalNumber } from '../util/isNormalNumber';
import * as log from '../logging/log';
import type { BadgeType, BadgeImageType } from './types';
import { parseBadgeCategory } from './BadgeCategory';
import { BadgeImageTheme, parseBadgeImageTheme } from './BadgeImageTheme';
const MAX_BADGES = 1000;
const badgeFromServerSchema = z.object({
category: z.string(),
description: z.string(),
id: z.string(),
name: z.string(),
svg: z.string(),
svgs: z.array(z.record(z.string())).length(3),
expiration: z.number().optional(),
visible: z.boolean().optional(),
});
export function parseBadgesFromServer(
value: unknown,
updatesUrl: string
): Array<BadgeType> {
if (!Array.isArray(value)) {
return [];
}
const result: Array<BadgeType> = [];
const numberOfBadgesToParse = Math.min(value.length, MAX_BADGES);
for (let i = 0; i < numberOfBadgesToParse; i += 1) {
const item = value[i];
const parseResult = badgeFromServerSchema.safeParse(item);
if (!parseResult.success) {
log.warn(
'parseBadgesFromServer got an invalid item',
parseResult.error.format()
);
continue;
}
const {
category,
description: descriptionTemplate,
expiration,
id,
name,
svg,
svgs,
visible,
} = parseResult.data;
const images = parseImages(svgs, svg, updatesUrl);
if (images.length !== 4) {
log.warn('Got invalid number of SVGs from the server');
continue;
}
result.push({
id,
category: parseBadgeCategory(category),
name,
descriptionTemplate,
images,
...(isNormalNumber(expiration) && typeof visible === 'boolean'
? {
expiresAt: expiration * 1000,
isVisible: visible,
}
: {}),
});
}
return result;
}
const parseImages = (
rawSvgs: ReadonlyArray<Record<string, string>>,
rawSvg: string,
updatesUrl: string
): Array<BadgeImageType> => {
const result: Array<BadgeImageType> = [];
for (const item of rawSvgs) {
if (!isRecord(item)) {
log.warn('Got invalid SVG from the server');
continue;
}
const image: BadgeImageType = {};
for (const [rawTheme, filename] of Object.entries(item)) {
if (typeof filename !== 'string') {
log.warn('Got an SVG from the server that lacked a valid filename');
continue;
}
const theme = parseBadgeImageTheme(rawTheme);
image[theme] = { url: parseImageFilename(filename, updatesUrl) };
}
if (isEmpty(image)) {
log.warn('Got an SVG from the server that lacked valid values');
} else {
result.push(image);
}
}
result.push({
[BadgeImageTheme.Transparent]: {
url: parseImageFilename(rawSvg, updatesUrl),
},
});
return result;
};
const parseImageFilename = (filename: string, updatesUrl: string): string =>
new URL(`/static/badges/${filename}`, updatesUrl).toString();