Files

125 lines
4.3 KiB
TypeScript
Raw Permalink Normal View History

import { apiError, json, type FetchRoute } from './helpers';
import type { GuildSound } from '@/api/sounds';
/**
* Маршруты ручек звуков для тестов: список, загрузка, переименование,
* удаление и просьба сыграть. Состояние живёт в замыкании, поэтому ответы
* согласованы между запросами (как у настоящего сервера).
*/
export interface SoundRoutesOptions {
guildId?: string;
/** Набор звуков «сервера»: его же проверяют тесты. */
sounds?: GuildSound[];
/** Ошибка на `POST .../play` (лимит 429, `voice.not_connected` и т.п.). */
playError?: { code: string; status: number; retryAfterMs?: number };
/** Ошибка на загрузку звука (`sound.name_taken`, `sounds.limit_reached`). */
uploadError?: { code: string; status: number };
}
export interface SoundRoutes {
routes: FetchRoute[];
sounds: GuildSound[];
/** Сколько раз просили сыграть звук. */
playCount: () => number;
}
export function soundRoutes(options: SoundRoutesOptions = {}): SoundRoutes {
const guildId = options.guildId ?? 'g-1';
const sounds = [...(options.sounds ?? [])];
let plays = 0;
const list = (kind: string | null): GuildSound[] =>
kind === null ? sounds : sounds.filter((sound) => sound.kind === kind);
const routes: FetchRoute[] = [
{
match: `/guilds/${guildId}/sounds/`,
method: 'POST',
response: ({ url }) => {
plays += 1;
if (options.playError !== undefined) {
const { code, status, retryAfterMs } = options.playError;
return json(
{
error: {
code,
message: 'error',
...(retryAfterMs === undefined ? {} : { retry_after_ms: retryAfterMs }),
},
},
status,
);
}
// `/guilds/{id}/sounds/{sound_id}/play`
const soundId = url.split('/sounds/')[1]?.split('/')[0] ?? '';
const known = sounds.find((sound) => sound.id === soundId);
return known === undefined ? apiError('sounds.not_found', 404) : json({ ok: true });
},
},
{
match: `/guilds/${guildId}/sounds`,
method: 'POST',
response: ({ url }) => {
if (options.uploadError !== undefined) {
return apiError(options.uploadError.code, options.uploadError.status);
}
const kind = url.includes('kind=ui') ? 'ui' : 'soundboard';
const created: GuildSound = {
id: `s-${sounds.length + 1}`,
name: 'uploaded',
file_id: '950',
kind,
...(kind === 'ui' ? { event: 'member_join' as const } : {}),
};
sounds.push(created);
return json({ sound: created });
},
},
{
match: `/guilds/${guildId}/sounds/`,
method: 'PATCH',
response: ({ url, body }) => {
const soundId = url.split('/sounds/')[1]?.split(/[/?]/u)[0] ?? '';
const patch = (body ?? {}) as Record<string, unknown>;
const index = sounds.findIndex((sound) => sound.id === soundId);
const current = sounds[index];
if (current === undefined) {
return apiError('sounds.not_found', 404);
}
const next: GuildSound = {
...current,
...(typeof patch['name'] === 'string' ? { name: patch['name'] } : {}),
};
sounds[index] = next;
return json({ sound: next });
},
},
{
match: `/guilds/${guildId}/sounds/`,
method: 'DELETE',
response: ({ url }) => {
const soundId = url.split('/sounds/')[1]?.split(/[/?]/u)[0] ?? '';
const index = sounds.findIndex((sound) => sound.id === soundId);
if (index >= 0) {
sounds.splice(index, 1);
}
return json({ ok: true });
},
},
{
match: `/guilds/${guildId}/sounds`,
response: ({ url }) => {
const kind = url.includes('kind=ui')
? 'ui'
: url.includes('kind=soundboard')
? 'soundboard'
: null;
return json({ sounds: list(kind) });
},
},
];
return { routes, sounds, playCount: () => plays };
}