/** Инициалы для аватара-заглушки: до двух первых букв значимых слов. */ 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); }