fix(files): аватары и баннеры доступны авторизованным пользователям
Аватар нельзя было показать в списке друзей: файл без комнаты был доступен только загрузившему. Добавлено назначение файла (`files.purpose`: attachment | avatar | banner, миграция 00006): аватары и баннеры отдаются любому авторизованному пользователю, вложения по-прежнему проверяются по правам комнаты, а непривязанные загрузки остаются приватными.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { request } from './client';
|
||||
|
||||
/**
|
||||
* Приглашения на сервер (AGENT.md §7.9).
|
||||
*
|
||||
* Присоединиться можно только по приглашению, поэтому клиент умеет:
|
||||
* разобрать код из ссылки `https://<домен>/invite/<код>`, показать
|
||||
* предпросмотр (`GET /invites/{code}`) и принять его (`POST /invites/{code}`).
|
||||
*/
|
||||
|
||||
export interface InviteGuildPreview {
|
||||
id: string;
|
||||
name: string;
|
||||
icon_file_id?: string;
|
||||
description?: string;
|
||||
member_count: number;
|
||||
is_member: boolean;
|
||||
}
|
||||
|
||||
export interface InvitePreview {
|
||||
code: string;
|
||||
guild_id: string;
|
||||
guild: InviteGuildPreview;
|
||||
}
|
||||
|
||||
export const inviteQueryKey = (code: string) => ['invites', code] as const;
|
||||
|
||||
/**
|
||||
* Достаёт код приглашения из ссылки или «сырого» кода.
|
||||
* Возвращает `null`, если строка не похожа ни на то, ни на другое.
|
||||
*/
|
||||
export function parseInviteCode(input: string): string | null {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
const fromUrl = (value: string): string | null => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
const segments = url.pathname.split('/').filter((segment) => segment !== '');
|
||||
const index = segments.indexOf('invite');
|
||||
const code = index === -1 ? undefined : segments[index + 1];
|
||||
return code === undefined || code === '' ? null : code;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
if (/^https?:\/\//iu.test(trimmed)) {
|
||||
return fromUrl(trimmed);
|
||||
}
|
||||
// Сырой код: буквы и цифры без разделителей (алфавит сервера — a-z0-9).
|
||||
return /^[0-9a-z]+$/iu.test(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/** `GET /invites/{code}` — предпросмотр приглашения. */
|
||||
export async function fetchInvite(code: string, signal?: AbortSignal): Promise<InvitePreview> {
|
||||
const payload = await request<unknown>(
|
||||
`/invites/${encodeURIComponent(code)}`,
|
||||
signal === undefined ? {} : { signal },
|
||||
);
|
||||
const record = asRecord(payload);
|
||||
const invite = asRecord(record?.['invite']);
|
||||
const guild = asRecord(record?.['guild']);
|
||||
const guildId = asString(guild?.['id']);
|
||||
if (guild === null || guildId === undefined) {
|
||||
throw new Error('malformed invite payload');
|
||||
}
|
||||
const preview: InvitePreview = {
|
||||
code: asString(invite?.['code']) ?? code,
|
||||
guild_id: guildId,
|
||||
guild: {
|
||||
id: guildId,
|
||||
name: asString(guild['name']) ?? guildId,
|
||||
member_count: typeof guild['member_count'] === 'number' ? guild['member_count'] : 0,
|
||||
is_member: guild['is_member'] === true,
|
||||
},
|
||||
};
|
||||
const icon = asString(guild['icon_file_id']);
|
||||
const description = asString(guild['description']);
|
||||
if (icon !== undefined) {
|
||||
preview.guild.icon_file_id = icon;
|
||||
}
|
||||
if (description !== undefined) {
|
||||
preview.guild.description = description;
|
||||
}
|
||||
return preview;
|
||||
}
|
||||
|
||||
/** `POST /invites/{code}` — вступить на сервер; сервер отвечает его id. */
|
||||
export async function acceptInvite(code: string): Promise<string> {
|
||||
const payload = await request<{ guild_id?: unknown }>(`/invites/${encodeURIComponent(code)}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const guildId = asString(payload.guild_id);
|
||||
if (guildId === undefined) {
|
||||
throw new Error('malformed invite accept payload');
|
||||
}
|
||||
return guildId;
|
||||
}
|
||||
Reference in New Issue
Block a user