91 lines
3.1 KiB
TypeScript
91 lines
3.1 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)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Разбирает число прав из строковой битовой маски, не падая на мусоре. */
|
||
|
|
export function parseBigInt(value: string | number | undefined): bigint {
|
||
|
|
if (value === undefined) {
|
||
|
|
return 0n;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return BigInt(value);
|
||
|
|
} catch {
|
||
|
|
return 0n;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Бит ADMINISTRATOR в маске прав (совпадает с серверной константой). */
|
||
|
|
export const PERMISSION_ADMINISTRATOR = 1n << 3n;
|
||
|
|
export const PERMISSION_MANAGE_GUILD = 1n << 5n;
|
||
|
|
export const PERMISSION_MANAGE_CHANNELS = 1n << 6n;
|
||
|
|
export const PERMISSION_OWNER = 1n << 62n;
|
||
|
|
|
||
|
|
/** Проверяет право в списке масок прав текущего пользователя. */
|
||
|
|
export function hasPermission(permissions: readonly string[], mask: bigint): boolean {
|
||
|
|
for (const value of permissions) {
|
||
|
|
const parsed = parseBigInt(value);
|
||
|
|
if (parsed === PERMISSION_OWNER || (parsed & mask) !== 0n) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Может ли пользователь управлять сервером (создавать комнаты и т.п.). */
|
||
|
|
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)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Человекочитаемая дата/время для списков (сессии, аудит). */
|
||
|
|
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);
|
||
|
|
}
|