2e038a1d3f
- миграция 00012: таблица webhooks, снимок имени и аватара в messages (AGENT.md 7.11)
- API: список/создание/правка/удаление и пересоздание токена (MANAGE_WEBHOOKS,
step-up при создании, аудит), загрузка аватара отдельной multipart-ручкой
- исполнение POST /webhooks/{id}/{token} без сессии: content, username,
avatar_url, файлы создателя вебхука, лимит 30/мин на вебхук
- клиент: пункт настроек «Комната» со списком вебхуков, копированием ссылки,
пересозданием токена и удалением
- сообщения вебхуков: имя, аватар и значок в ленте вместо «неизвестного автора»
- fix: неполный ответ REST больше не затирает данные READY-снапшота
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
/** Инициалы для аватара-заглушки: до двух первых букв значимых слов. */
|
|
export function initials(name: string): string {
|
|
const parts = name
|
|
.trim()
|
|
.split(/\s+/u)
|
|
.filter((part) => part.length > 0);
|
|
if (parts.length === 0) {
|
|
return '?';
|
|
}
|
|
const first = parts[0] ?? '';
|
|
if (parts.length === 1) {
|
|
return [...first].slice(0, 2).join('').toLocaleUpperCase();
|
|
}
|
|
const second = parts[1] ?? '';
|
|
return `${[...first][0] ?? ''}${[...second][0] ?? ''}`.toLocaleUpperCase();
|
|
}
|
|
|
|
/** Стабильный оттенок для заглушки сервера/пользователя. */
|
|
export function hueFromId(id: string): number {
|
|
let hash = 0;
|
|
for (let index = 0; index < id.length; index += 1) {
|
|
hash = (hash * 31 + id.charCodeAt(index)) % 360;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
/**
|
|
* Ссылка на файл аватара. Файловый сервис отдаётся тем же origin
|
|
* (`/files/{id}`), поэтому отдельный хост не нужен.
|
|
*/
|
|
export function avatarUrl(fileId: string | undefined): string | null {
|
|
return fileId === undefined || fileId === '' ? null : `/files/${encodeURIComponent(fileId)}`;
|
|
}
|
|
|
|
/**
|
|
* Права приходят с сервера списком имён (`permissions.Names` в Go):
|
|
* например `["VIEW_GUILD", "MANAGE_CHANNELS"]`. Проверяем вхождение имени.
|
|
*/
|
|
export const PERMISSION_ADMINISTRATOR = 'ADMINISTRATOR';
|
|
export const PERMISSION_MANAGE_GUILD = 'MANAGE_GUILD';
|
|
export const PERMISSION_MANAGE_CHANNELS = 'MANAGE_CHANNELS';
|
|
export const PERMISSION_MANAGE_MESSAGES = 'MANAGE_MESSAGES';
|
|
export const PERMISSION_MANAGE_EMOJIS = 'MANAGE_EMOJIS';
|
|
export const PERMISSION_MANAGE_SOUNDS = 'MANAGE_SOUNDS';
|
|
export const PERMISSION_BAN_MEMBERS = 'BAN_MEMBERS';
|
|
export const PERMISSION_VIEW_AUDIT_LOG = 'VIEW_AUDIT_LOG';
|
|
export const PERMISSION_USE_SOUNDBOARD = 'USE_SOUNDBOARD';
|
|
export const PERMISSION_SEND_MESSAGES = 'SEND_MESSAGES';
|
|
export const PERMISSION_ATTACH_FILES = 'ATTACH_FILES';
|
|
export const PERMISSION_ADD_REACTIONS = 'ADD_REACTIONS';
|
|
export const PERMISSION_CONNECT_VOICE = 'CONNECT_VOICE';
|
|
export const PERMISSION_MUTE_MEMBERS = 'MUTE_MEMBERS';
|
|
export const PERMISSION_DEAFEN_MEMBERS = 'DEAFEN_MEMBERS';
|
|
export const PERMISSION_MOVE_MEMBERS = 'MOVE_MEMBERS';
|
|
export const PERMISSION_MANAGE_WEBHOOKS = 'MANAGE_WEBHOOKS';
|
|
|
|
/** Есть ли у пользователя право с указанным именем. */
|
|
export function hasPermission(permissions: readonly string[], name: string): boolean {
|
|
return permissions.includes(name);
|
|
}
|
|
|
|
/** Может ли пользователь управлять сервером (создавать комнаты и т.п.). */
|
|
export function canManageGuild(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
): boolean {
|
|
if (userId !== undefined && userId === ownerId) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_GUILD) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_CHANNELS)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь менять настройки сервера (имя, описание, удаление).
|
|
* `MANAGE_CHANNELS` сюда не входит: его достаточно для комнат, но не для
|
|
* самого сервера — сервер разрешает `PATCH /guilds/{id}` только владельцу,
|
|
* ADMINISTRATOR и MANAGE_GUILD.
|
|
*/
|
|
export function canManageGuildSettings(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
): boolean {
|
|
if (userId !== undefined && userId === ownerId) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_GUILD)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь открыть администрирование сервера: владелец,
|
|
* ADMINISTRATOR/MANAGE_GUILD или администратор инстанса.
|
|
*/
|
|
export function canAdministerGuild(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
return isInstanceAdmin || canManageGuildSettings(permissions, ownerId, userId);
|
|
}
|
|
|
|
/** Права модератора голосовых комнат: что доступно пользователю на сервере. */
|
|
export interface VoiceModerationPermissions {
|
|
mute: boolean;
|
|
deafen: boolean;
|
|
move: boolean;
|
|
}
|
|
|
|
/**
|
|
* Права на голосовую модерацию (AGENT.md 7.14): серверный мьют, глушение и
|
|
* перемещение. Администратор инстанса видит все пункты, как и в остальном UI.
|
|
*/
|
|
export function voiceModerationPermissions(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): VoiceModerationPermissions {
|
|
if (isInstanceAdmin || (userId !== undefined && userId === ownerId)) {
|
|
return { mute: true, deafen: true, move: true };
|
|
}
|
|
const administrator = hasPermission(permissions, PERMISSION_ADMINISTRATOR);
|
|
return {
|
|
mute: administrator || hasPermission(permissions, PERMISSION_MUTE_MEMBERS),
|
|
deafen: administrator || hasPermission(permissions, PERMISSION_DEAFEN_MEMBERS),
|
|
move: administrator || hasPermission(permissions, PERMISSION_MOVE_MEMBERS),
|
|
};
|
|
}
|
|
|
|
/** Может ли пользователь войти в голосовую комнату. */
|
|
export function canConnectVoice(
|
|
permissions: readonly string[],
|
|
isInstanceAdmin = false,
|
|
channelCanConnect?: boolean,
|
|
): boolean {
|
|
if (channelCanConnect === false) {
|
|
return false;
|
|
}
|
|
if (isInstanceAdmin) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_CONNECT_VOICE)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь модерировать сообщения (удалять и закреплять чужие).
|
|
* Права приходят на уровне сервера, поэтому это приближение: сервер всё равно
|
|
* проверит права в конкретной комнате.
|
|
*/ export function canManageMessages(
|
|
permissions: readonly string[],
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
if (isInstanceAdmin) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_MESSAGES)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь управлять эмодзи сервера (AGENT.md 7.12): владелец,
|
|
* ADMINISTRATOR, MANAGE_EMOJIS или администратор инстанса. `MANAGE_GUILD`
|
|
* сюда не входит: сервер проверяет именно `MANAGE_EMOJIS`.
|
|
*/
|
|
export function canManageEmojis(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
if (isInstanceAdmin || (userId !== undefined && userId === ownerId)) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_EMOJIS)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь управлять вебхуками комнаты (AGENT.md 7.11):
|
|
* владелец, ADMINISTRATOR, MANAGE_WEBHOOKS или администратор инстанса.
|
|
*/
|
|
export function canManageWebhooks(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
if (isInstanceAdmin || (userId !== undefined && userId === ownerId)) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_WEBHOOKS)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь банить участников (AGENT.md 7.17): владелец,
|
|
* ADMINISTRATOR, BAN_MEMBERS или администратор инстанса.
|
|
*/
|
|
export function canBanMembers(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
if (isInstanceAdmin || (userId !== undefined && userId === ownerId)) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_BAN_MEMBERS)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь смотреть журнал аудита (AGENT.md 7.18): владелец,
|
|
* ADMINISTRATOR, VIEW_AUDIT_LOG или администратор инстанса.
|
|
*/
|
|
export function canViewAuditLog(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
if (isInstanceAdmin || (userId !== undefined && userId === ownerId)) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_VIEW_AUDIT_LOG)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь управлять звуками сервера (AGENT.md 7.13): владелец,
|
|
* ADMINISTRATOR, MANAGE_SOUNDS или администратор инстанса.
|
|
*/
|
|
export function canManageSounds(
|
|
permissions: readonly string[],
|
|
ownerId: string,
|
|
userId: string | undefined,
|
|
isInstanceAdmin = false,
|
|
): boolean {
|
|
if (isInstanceAdmin || (userId !== undefined && userId === ownerId)) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_MANAGE_SOUNDS)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Может ли пользователь играть звуки саундборда (AGENT.md 7.13): право
|
|
* `USE_SOUNDBOARD`. Присутствие в голосовой комнате проверяется отдельно —
|
|
* сервер отвечает `voice.not_connected`, если мы не в комнате.
|
|
*/
|
|
export function canUseSoundboard(permissions: readonly string[], isInstanceAdmin = false): boolean {
|
|
if (isInstanceAdmin) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_USE_SOUNDBOARD)
|
|
);
|
|
}
|
|
|
|
/** Может ли пользователь отправлять сообщения в комнату. */
|
|
export function canSendMessages(
|
|
permissions: readonly string[],
|
|
isInstanceAdmin = false,
|
|
channelCanSend?: boolean,
|
|
): boolean {
|
|
if (channelCanSend === false) {
|
|
return false;
|
|
}
|
|
if (isInstanceAdmin) {
|
|
return true;
|
|
}
|
|
return (
|
|
hasPermission(permissions, PERMISSION_ADMINISTRATOR) ||
|
|
hasPermission(permissions, PERMISSION_SEND_MESSAGES)
|
|
);
|
|
}
|
|
|
|
/** Человекочитаемая дата/время для списков (сессии, аудит). */ export function formatDateTime(
|
|
value: string,
|
|
locale: string,
|
|
): string {
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return value;
|
|
}
|
|
return new Intl.DateTimeFormat(locale, {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
}).format(date);
|
|
}
|