124 lines
3.1 KiB
TypeScript
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();
|