Files
glchat/web/src/api/emojis.ts
T

140 lines
5.1 KiB
TypeScript
Raw Normal View History

import { CUSTOM_EMOJI_NAME_PATTERN, customEmojiToken } from '@/lib/customEmoji';
import { request } from './client';
/**
* Кастомные эмодзи сервера (AGENT.md 7.12).
*
* Контракт один и тот же в трёх местах: `GET /guilds/{id}/emojis`, снапшот
* READY (`guilds[].emojis`) и событие шлюза `GUILD_EMOJIS_UPDATE`. Эмодзи
* в тексте и в реакции — токен `<:имя:file_id>` (`<a:…>` у анимированных),
* поэтому разбор payload'а общий для всех источников.
*/
export interface GuildEmoji {
id: string;
name: string;
file_id: string;
animated: boolean;
/** Токен для сообщения и реакции: `<:имя:file_id>` или `<a:имя:file_id>`. */
token: string;
/** Ссылка на картинку (REST); в снапшоте READY её нет. */
url?: string;
/** Размер файла: сервер присылает его не во всех ответах. */
size_bytes?: number;
}
/** Лимиты из контракта: 100 эмодзи на сервер, 512 КБ на файл. */
export const EMOJI_LIMIT_PER_GUILD = 100;
export const EMOJI_MAX_BYTES = 512 * 1024;
/** Форматы, которые принимает сервер. */
export const EMOJI_CONTENT_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] as const;
/** Разрешён ли файл по формату (размер проверяется отдельно). */
export function isAllowedEmojiType(file: File): boolean {
return (EMOJI_CONTENT_TYPES as readonly string[]).includes(file.type);
}
/** Проходит ли имя клиентскую проверку (та же, что на сервере). */
export function isValidEmojiName(name: string): boolean {
return CUSTOM_EMOJI_NAME_PATTERN.test(name);
}
export const guildEmojisQueryKey = (guildId: string) => ['guilds', guildId, 'emojis'] as const;
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
}
function asString(value: unknown): string | undefined {
return typeof value === 'string' && value !== '' ? value : undefined;
}
/**
* Разбор одного эмодзи. Токен берём из ответа, а если сервер его не прислал —
* собираем сами: формат один и тот же, а без токена эмодзи бесполезен.
*/
export function parseGuildEmoji(value: unknown): GuildEmoji | null {
const record = asRecord(value);
if (record === null) {
return null;
}
const id = asString(record['id']);
const name = asString(record['name']);
const fileId = asString(record['file_id']);
if (id === undefined || name === undefined || fileId === undefined) {
return null;
}
const animated = record['animated'] === true;
const emoji: GuildEmoji = {
id,
name,
file_id: fileId,
animated,
token: asString(record['token']) ?? customEmojiToken(name, fileId, animated),
};
const url = asString(record['url']);
const sizeBytes = record['size_bytes'];
if (url !== undefined) {
emoji.url = url;
}
if (typeof sizeBytes === 'number' && Number.isFinite(sizeBytes) && sizeBytes >= 0) {
emoji.size_bytes = sizeBytes;
}
return emoji;
}
/** Разбор списка эмодзи: лишние и битые записи отбрасываются. */
export function parseGuildEmojiList(value: unknown): GuildEmoji[] {
if (!Array.isArray(value)) {
return [];
}
return value.map(parseGuildEmoji).filter((item): item is GuildEmoji => item !== null);
}
export async function fetchGuildEmojis(
guildId: string,
signal?: AbortSignal,
): Promise<GuildEmoji[]> {
const payload = await request<{ emojis?: unknown }>(
`/guilds/${encodeURIComponent(guildId)}/emojis`,
signal === undefined ? {} : { signal },
);
return parseGuildEmojiList(payload.emojis);
}
/** Загрузка: multipart с полями `name` и `file` (право MANAGE_EMOJIS). */
export async function uploadGuildEmoji(
guildId: string,
name: string,
file: File,
): Promise<GuildEmoji | null> {
const form = new FormData();
form.append('name', name);
form.append('file', file);
const payload = await request<{ emoji?: unknown }>(
`/guilds/${encodeURIComponent(guildId)}/emojis`,
{ method: 'POST', form },
);
return parseGuildEmoji(payload.emoji);
}
export async function renameGuildEmoji(
guildId: string,
emojiId: string,
name: string,
): Promise<GuildEmoji | null> {
const payload = await request<{ emoji?: unknown }>(
`/guilds/${encodeURIComponent(guildId)}/emojis/${encodeURIComponent(emojiId)}`,
{ method: 'PATCH', body: { name } },
);
return parseGuildEmoji(payload.emoji);
}
export async function deleteGuildEmoji(guildId: string, emojiId: string): Promise<void> {
await request<{ ok: true }>(
`/guilds/${encodeURIComponent(guildId)}/emojis/${encodeURIComponent(emojiId)}`,
{ method: 'DELETE' },
);
}