mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
227 lines
7.2 KiB
TypeScript
227 lines
7.2 KiB
TypeScript
import { COUNTRY_FLAG_HEIGHT, COUNTRY_FLAG_WIDTH, getCountryFlagPosition, getCountryLabel, normalizeCountryCode } from './country-flags';
|
|
import { getBoardFlagDefinition, normalizeBoardFlagKind, type BoardFlagKind } from './board-flags';
|
|
|
|
const COUNTRY_FLAG_SPRITE_PATH = 'assets/icons/flags-1.png';
|
|
const TOR_COUNTRY_CODE = 'xx';
|
|
const TOR_AUTHOR_FLAG_LABEL = 'This user is on Tor';
|
|
export const TOR_AUTHOR_FLAG_LABEL_KEY = 'tor_author_flag_label';
|
|
const FLAG_TEXT_PATTERN = /^flag:([a-z-]+):([a-z0-9-]+)$/i;
|
|
const SHORT_FLAG_TEXT_PATTERN = /^(country|geo|pol|political|meme|memeflag|memeflags|pony|mlp):([a-z0-9-]+)$/i;
|
|
const COUNTRY_EMOJI_TEXT_PATTERN = /^([a-z]{2}|catalonia|eu|fam|xe|xs|xw|xk|xx)-emoji$/i;
|
|
|
|
type FlagSource =
|
|
| { type: 'country'; code: string }
|
|
| {
|
|
type: BoardFlagKind;
|
|
code: string;
|
|
};
|
|
|
|
interface AuthorFlairLike {
|
|
text?: unknown;
|
|
code?: unknown;
|
|
country?: unknown;
|
|
type?: unknown;
|
|
kind?: unknown;
|
|
expiresAt?: unknown;
|
|
}
|
|
|
|
interface AuthorLike {
|
|
flairs?: unknown;
|
|
community?: {
|
|
flairs?: unknown;
|
|
};
|
|
subplebbit?: {
|
|
flairs?: unknown;
|
|
};
|
|
}
|
|
|
|
interface CommentLike {
|
|
'5chan'?: unknown;
|
|
author?: unknown;
|
|
flairs?: unknown;
|
|
raw?: {
|
|
comment?: unknown;
|
|
};
|
|
}
|
|
|
|
export interface AuthorFlagViewModel {
|
|
key: string;
|
|
type: 'country' | BoardFlagKind;
|
|
code: string;
|
|
label: string;
|
|
labelKey?: string;
|
|
spritePath: string;
|
|
width: number;
|
|
height: number;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
const toString = (value: unknown): string | undefined => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
|
|
const normalizeFlairsContainer = (flairs: unknown): unknown => {
|
|
if (Array.isArray(flairs)) return flairs;
|
|
return isRecord(flairs) && Array.isArray(flairs.author) ? flairs.author : undefined;
|
|
};
|
|
|
|
export const getAuthorFlagFlairs = (author: unknown): unknown => {
|
|
if (!isRecord(author)) return undefined;
|
|
|
|
const authorLike = author as AuthorLike;
|
|
return normalizeFlairsContainer(authorLike.community?.flairs) ?? normalizeFlairsContainer(authorLike.subplebbit?.flairs) ?? normalizeFlairsContainer(authorLike.flairs);
|
|
};
|
|
|
|
const getFiveChanFlagFlair = (comment: unknown): AuthorFlairLike | undefined => {
|
|
if (!isRecord(comment)) return undefined;
|
|
|
|
const fiveChan = (comment as CommentLike)['5chan'];
|
|
if (!isRecord(fiveChan) || !isRecord(fiveChan.flag)) return undefined;
|
|
|
|
const flag = fiveChan.flag;
|
|
const text = toString(flag.text);
|
|
const type = toString(flag.type);
|
|
const code = toString(flag.code) ?? toString(fiveChan.country);
|
|
const country = toString(fiveChan.country);
|
|
|
|
if (!text && (!type || !code)) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
text,
|
|
type,
|
|
code,
|
|
country,
|
|
};
|
|
};
|
|
|
|
export const getCommentFlagFlairs = (comment: unknown, author?: unknown): unknown => {
|
|
if (!isRecord(comment)) return getAuthorFlagFlairs(author);
|
|
|
|
const commentLike = comment as CommentLike;
|
|
const fiveChanFlagFlair = getFiveChanFlagFlair(comment) ?? getFiveChanFlagFlair(commentLike.raw?.comment);
|
|
if (fiveChanFlagFlair) {
|
|
return [fiveChanFlagFlair];
|
|
}
|
|
|
|
return getAuthorFlagFlairs(author ?? commentLike.author) ?? normalizeFlairsContainer(commentLike.flairs);
|
|
};
|
|
|
|
const isFreshFlair = (flair: AuthorFlairLike, nowSeconds: number) =>
|
|
typeof flair.expiresAt !== 'number' || !Number.isFinite(flair.expiresAt) || flair.expiresAt > nowSeconds;
|
|
|
|
const parseTextFlagSource = (text: string | undefined): FlagSource | undefined => {
|
|
if (!text) return undefined;
|
|
|
|
const flagMatch = text.match(FLAG_TEXT_PATTERN);
|
|
if (flagMatch) {
|
|
const [, kindValue, code] = flagMatch;
|
|
const normalizedKindValue = kindValue.toLowerCase();
|
|
if (normalizedKindValue === 'country' || normalizedKindValue === 'geo') {
|
|
return { type: 'country', code };
|
|
}
|
|
const kind = normalizeBoardFlagKind(normalizedKindValue);
|
|
return kind ? { type: kind, code } : undefined;
|
|
}
|
|
|
|
const shortMatch = text.match(SHORT_FLAG_TEXT_PATTERN);
|
|
if (shortMatch) {
|
|
const [, kindValue, code] = shortMatch;
|
|
const normalizedKindValue = kindValue.toLowerCase();
|
|
if (normalizedKindValue === 'country' || normalizedKindValue === 'geo') {
|
|
return { type: 'country', code };
|
|
}
|
|
const kind = normalizeBoardFlagKind(normalizedKindValue);
|
|
return kind ? { type: kind, code } : undefined;
|
|
}
|
|
|
|
const countryEmojiMatch = text.match(COUNTRY_EMOJI_TEXT_PATTERN);
|
|
return countryEmojiMatch ? { type: 'country', code: countryEmojiMatch[1] } : undefined;
|
|
};
|
|
|
|
const parseStructuredFlagSource = (flair: AuthorFlairLike): FlagSource | undefined => {
|
|
const typeValue = toString(flair.type) ?? toString(flair.kind);
|
|
if (!typeValue) return undefined;
|
|
|
|
const normalizedTypeValue = typeValue.toLowerCase();
|
|
if (normalizedTypeValue === 'country' || normalizedTypeValue === 'geo') {
|
|
const code = toString(flair.country) ?? toString(flair.code);
|
|
return code ? { type: 'country', code } : undefined;
|
|
}
|
|
|
|
const kind = normalizeBoardFlagKind(normalizedTypeValue);
|
|
const code = toString(flair.code);
|
|
return kind && code ? { type: kind, code } : undefined;
|
|
};
|
|
|
|
const toCountryFlagViewModel = (codeValue: string): AuthorFlagViewModel | undefined => {
|
|
const code = normalizeCountryCode(codeValue);
|
|
const position = getCountryFlagPosition(code);
|
|
if (!code || !position) return undefined;
|
|
|
|
return {
|
|
key: `country:${code}`,
|
|
type: 'country',
|
|
code,
|
|
label: code === TOR_COUNTRY_CODE ? TOR_AUTHOR_FLAG_LABEL : (getCountryLabel(code) ?? code.toUpperCase()),
|
|
...(code === TOR_COUNTRY_CODE ? { labelKey: TOR_AUTHOR_FLAG_LABEL_KEY } : {}),
|
|
spritePath: COUNTRY_FLAG_SPRITE_PATH,
|
|
width: COUNTRY_FLAG_WIDTH,
|
|
height: COUNTRY_FLAG_HEIGHT,
|
|
x: position.x,
|
|
y: position.y,
|
|
};
|
|
};
|
|
|
|
const toBoardFlagViewModel = (type: BoardFlagKind, code: string): AuthorFlagViewModel | undefined => {
|
|
const flag = getBoardFlagDefinition(type, code);
|
|
return flag
|
|
? {
|
|
key: `${flag.kind}:${flag.code}`,
|
|
type: flag.kind,
|
|
code: flag.code,
|
|
label: flag.label,
|
|
spritePath: flag.spritePath,
|
|
width: flag.width,
|
|
height: flag.height,
|
|
x: flag.x,
|
|
y: flag.y,
|
|
}
|
|
: undefined;
|
|
};
|
|
|
|
const toFlagViewModel = (source: FlagSource | undefined): AuthorFlagViewModel | undefined => {
|
|
if (!source) return undefined;
|
|
return source.type === 'country' ? toCountryFlagViewModel(source.code) : toBoardFlagViewModel(source.type, source.code);
|
|
};
|
|
|
|
export const getAuthorFlagViewModels = (flairs: unknown, nowSeconds = Date.now() / 1000): AuthorFlagViewModel[] => {
|
|
if (!Array.isArray(flairs)) return [];
|
|
|
|
const seenKeys = new Set<string>();
|
|
const flags: AuthorFlagViewModel[] = [];
|
|
|
|
for (const flair of flairs) {
|
|
if (typeof flair !== 'object' || flair === null) {
|
|
continue;
|
|
}
|
|
|
|
const flairRecord = flair as AuthorFlairLike;
|
|
if (!isFreshFlair(flairRecord, nowSeconds)) {
|
|
continue;
|
|
}
|
|
|
|
const flag = toFlagViewModel(parseStructuredFlagSource(flairRecord) ?? parseTextFlagSource(toString(flairRecord.text)));
|
|
if (!flag || seenKeys.has(flag.key)) {
|
|
continue;
|
|
}
|
|
|
|
seenKeys.add(flag.key);
|
|
flags.push(flag);
|
|
}
|
|
|
|
return flags;
|
|
};
|