mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-01 20:46:08 +00:00
425 lines
12 KiB
TypeScript
425 lines
12 KiB
TypeScript
import { promises } from 'node:fs';
|
|
import { basename, join } from 'node:path';
|
|
import {
|
|
type ColorResolvable,
|
|
DiscordEpoch,
|
|
EmbedColors,
|
|
type EmojiResolvable,
|
|
type Logger,
|
|
type ObjectToLower,
|
|
type ObjectToSnake,
|
|
type TypeArray,
|
|
} from '..';
|
|
import type { Cache } from '../../cache';
|
|
import { type APIPartialEmoji, FormattingPatterns, GatewayIntentBits } from '../../types';
|
|
|
|
/**
|
|
* Calculates the shard ID for a guild based on its ID.
|
|
* @param guildId The ID of the guild.
|
|
* @param shards The number of shards to calculate the ID for.
|
|
* @returns The shard ID.
|
|
*/
|
|
export function calculateShardId(guildId: string, shards?: number) {
|
|
return Number((BigInt(guildId) >> 22n) % BigInt(shards ?? 1));
|
|
}
|
|
|
|
// It is used for resolve color for better performance
|
|
const ColorLookup: { [key: string]: number } = {
|
|
Random: -1, // Special value for random color
|
|
...EmbedColors,
|
|
};
|
|
|
|
/**
|
|
* Resolves the color to a numeric representation.
|
|
* @param color The color to resolve.
|
|
* @returns The numeric representation of the color.
|
|
*/
|
|
export function resolveColor(color: ColorResolvable): number {
|
|
const type = typeof color;
|
|
|
|
if (type === 'number') {
|
|
if (!Number.isInteger(color) || (color as number) < 0) throw new Error(`Invalid color: ${color}`);
|
|
return color as number;
|
|
}
|
|
|
|
if (type === 'string') {
|
|
const lookupColor = ColorLookup[color as keyof typeof EmbedColors];
|
|
if (lookupColor) {
|
|
return lookupColor === -1 ? Math.floor(Math.random() * 0xffffff) : lookupColor;
|
|
}
|
|
return (color as string).startsWith('#') ? Number.parseInt((color as string).slice(1), 16) : EmbedColors.Default;
|
|
}
|
|
|
|
return Array.isArray(color) && color.length >= 3
|
|
? (color[0] << 16) | (color[1] << 8) | color[2]
|
|
: EmbedColors.Default;
|
|
}
|
|
|
|
/**
|
|
* Delays the resolution of a Promise by the specified time.
|
|
* @param time The time in milliseconds to delay the resolution.
|
|
* @param result The value to resolve with after the delay.
|
|
* @returns A Promise that resolves after the specified time with the provided result.
|
|
*/
|
|
export function delay<T>(time: number, result?: T): Promise<T> {
|
|
return new Promise(r => setTimeout(r, time, result));
|
|
}
|
|
|
|
/**
|
|
* Checks if a given value is an object.
|
|
* @param o The value to check.
|
|
* @returns `true` if the value is an object, otherwise `false`.
|
|
*/
|
|
export function isObject(o: any): o is Record<string, unknown> {
|
|
return o && typeof o === 'object' && !Array.isArray(o);
|
|
}
|
|
|
|
/**
|
|
* Merges multiple options objects together, deeply extending objects.
|
|
* @param defaults The default options object.
|
|
* @param options Additional options objects to merge.
|
|
* @returns The merged options object.
|
|
*/
|
|
export function MergeOptions<T>(defaults: any, ...options: any[]): T {
|
|
const option = options.shift();
|
|
if (!option) {
|
|
return defaults;
|
|
}
|
|
|
|
return MergeOptions(
|
|
{
|
|
...option,
|
|
...Object.fromEntries(
|
|
Object.entries(defaults).map(([key, value]) => [
|
|
key,
|
|
isObject(value) ? MergeOptions(value, option?.[key] || {}) : (option?.[key] ?? value),
|
|
]),
|
|
),
|
|
},
|
|
...options,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Splits an array into two arrays based on the result of a predicate function.
|
|
* @param arr The array to split.
|
|
* @param func The predicate function used to test elements of the array.
|
|
* @returns An object containing two arrays: one with elements that passed the test and one with elements that did not.
|
|
*/
|
|
export function filterSplit<Element, Never = Element>(
|
|
arr: (Element | Never)[],
|
|
func: (value: Element | Never) => boolean,
|
|
) {
|
|
const expect: Element[] = [];
|
|
const never: Never[] = [];
|
|
|
|
for (const element of arr) {
|
|
const test = func(element);
|
|
if (test) expect.push(element as Element);
|
|
else never.push(element as Never);
|
|
}
|
|
|
|
return { expect, never };
|
|
}
|
|
|
|
/**
|
|
* Represents a base handler class.
|
|
*/
|
|
export class BaseHandler {
|
|
/**
|
|
* Initializes a new instance of the BaseHandler class.
|
|
* @param logger The logger instance.
|
|
*/
|
|
constructor(protected logger: Logger) {}
|
|
|
|
/**
|
|
* Filters a file path.
|
|
* @param path The path to filter.
|
|
* @returns `true` if the path passes the filter, otherwise `false`.
|
|
*/
|
|
filter = (path: string) => !!path;
|
|
|
|
/**
|
|
* Recursively retrieves all files in a directory.
|
|
* @param dir The directory path.
|
|
* @returns A Promise that resolves to an array of file paths.
|
|
*/
|
|
protected async getFiles(dir: string) {
|
|
const files: string[] = [];
|
|
|
|
for (const i of await promises.readdir(dir, { withFileTypes: true })) {
|
|
if (i.isDirectory()) {
|
|
files.push(...(await this.getFiles(join(dir, i.name))));
|
|
} else if (this.filter(join(dir, i.name))) {
|
|
files.push(join(dir, i.name));
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Loads files from given paths.
|
|
* @param paths The paths of the files to load.
|
|
* @returns A Promise that resolves to an array of loaded files.
|
|
*/
|
|
protected loadFiles<T extends NonNullable<unknown>>(paths: string[]): Promise<T[]> {
|
|
return Promise.all(paths.map(path => magicImport(path).then(file => file.default ?? file)));
|
|
}
|
|
|
|
/**
|
|
* Loads files from given paths along with additional information.
|
|
* @param paths The paths of the files to load.
|
|
* @returns A Promise that resolves to an array of objects containing name, file, and path.
|
|
*/
|
|
protected loadFilesK<T>(paths: string[]): Promise<{ name: string; file: T; path: string }[]> {
|
|
return Promise.all(
|
|
paths.map(path =>
|
|
magicImport(path).then(file => {
|
|
return {
|
|
name: basename(path),
|
|
file,
|
|
path,
|
|
};
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a camelCase object to snake_case.
|
|
* @param target The object to convert.
|
|
* @returns The converted object.
|
|
*/
|
|
export function toSnakeCase<Obj extends Record<string, any>>(target: Obj): ObjectToSnake<Obj> {
|
|
const result: Record<string, any> = {};
|
|
for (const [key, value] of Object.entries(target)) {
|
|
switch (typeof value) {
|
|
case 'string':
|
|
case 'bigint':
|
|
case 'boolean':
|
|
case 'function':
|
|
case 'number':
|
|
case 'symbol':
|
|
case 'undefined':
|
|
result[ReplaceRegex.snake(key)] = value;
|
|
break;
|
|
case 'object': {
|
|
if (Array.isArray(value)) {
|
|
result[ReplaceRegex.snake(key)] = value.map(prop =>
|
|
typeof prop === 'object' && prop ? toSnakeCase(prop) : prop,
|
|
);
|
|
break;
|
|
}
|
|
if (isObject(value)) {
|
|
result[ReplaceRegex.snake(key)] = toSnakeCase(value);
|
|
break;
|
|
}
|
|
if (!Number.isNaN(value)) {
|
|
result[ReplaceRegex.snake(key)] = null;
|
|
break;
|
|
}
|
|
result[ReplaceRegex.snake(key)] = toSnakeCase(value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result as ObjectToSnake<Obj>;
|
|
}
|
|
|
|
/**
|
|
* Convert a snake_case object to camelCase.
|
|
* @param target The object to convert.
|
|
* @returns The converted object.
|
|
*/
|
|
export function toCamelCase<Obj extends Record<string, any>>(target: Obj): ObjectToLower<Obj> {
|
|
const result: Record<string, any> = {};
|
|
for (const [key, value] of Object.entries(target)) {
|
|
switch (typeof value) {
|
|
case 'string':
|
|
case 'bigint':
|
|
case 'boolean':
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'number':
|
|
case 'undefined':
|
|
result[ReplaceRegex.camel(key)] = value;
|
|
break;
|
|
case 'object': {
|
|
if (Array.isArray(value)) {
|
|
result[ReplaceRegex.camel(key)] = value.map(prop =>
|
|
typeof prop === 'object' && prop ? toCamelCase(prop) : prop,
|
|
);
|
|
break;
|
|
}
|
|
if (isObject(value)) {
|
|
result[ReplaceRegex.camel(key)] = toCamelCase(value);
|
|
break;
|
|
}
|
|
if (!Number.isNaN(value)) {
|
|
result[ReplaceRegex.camel(key)] = null;
|
|
break;
|
|
}
|
|
result[ReplaceRegex.camel(key)] = toCamelCase(value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return result as ObjectToLower<Obj>;
|
|
}
|
|
|
|
export const ReplaceRegex = {
|
|
camel: (s: string) => {
|
|
return s.toLowerCase().replace(/(_\S)/gi, a => a[1].toUpperCase());
|
|
},
|
|
snake: (s: string) => {
|
|
return s.replace(/[A-Z]/g, a => `_${a.toLowerCase()}`);
|
|
},
|
|
};
|
|
|
|
export async function magicImport(path: string) {
|
|
try {
|
|
if ('Deno' in globalThis) throw new Error('https://github.com/denoland/deno/issues/26136');
|
|
return require(path);
|
|
} catch {
|
|
return new Function('path', 'return import(`file:///${path}?update=${Date.now()}`)')(path);
|
|
}
|
|
}
|
|
|
|
export type OnFailCallback = (error: unknown) => any;
|
|
|
|
export function fakePromise<T = unknown | Promise<unknown>>(
|
|
value: T,
|
|
): {
|
|
then<R>(callback: (arg: Awaited<T>) => R): R;
|
|
} {
|
|
if (value instanceof Promise) return value as any;
|
|
return {
|
|
then: callback => callback(value as Awaited<T>),
|
|
};
|
|
}
|
|
|
|
export function lazyLoadPackage<T>(mod: string): T | undefined {
|
|
try {
|
|
return require(mod);
|
|
} catch (e) {
|
|
// biome-ignore lint/suspicious/noConsoleLog:
|
|
// biome-ignore lint/suspicious/noConsole:
|
|
console.log(`Cannot import ${mod}`, e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
export function isCloudfareWorker() {
|
|
//@ts-expect-error
|
|
return process.platform === 'browser';
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Convert a timestamp to a snowflake.
|
|
* @param id The timestamp to convert.
|
|
* @returns The snowflake.
|
|
*/
|
|
export function snowflakeToTimestamp(id: string): bigint {
|
|
return (BigInt(id) >> 22n) + DiscordEpoch;
|
|
}
|
|
|
|
export function resolvePartialEmoji(emoji: EmojiResolvable): APIPartialEmoji | undefined {
|
|
if (typeof emoji === 'string') {
|
|
const groups: Partial<APIPartialEmoji> | undefined = emoji.match(FormattingPatterns.Emoji)?.groups;
|
|
if (groups) {
|
|
return {
|
|
animated: !!groups.animated,
|
|
name: groups.name!,
|
|
id: groups.id!,
|
|
};
|
|
}
|
|
if (emoji.includes('%')) {
|
|
emoji = encodeURIComponent(emoji);
|
|
}
|
|
if (!(emoji.includes(':') || emoji.match(/\d{17,20}/g))) {
|
|
return { name: emoji, id: null };
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!(emoji.id && emoji.name)) return;
|
|
return { id: emoji.id, name: emoji.name, animated: !!emoji.animated };
|
|
}
|
|
|
|
export async function resolveEmoji(emoji: EmojiResolvable, cache: Cache): Promise<APIPartialEmoji | undefined> {
|
|
const partial = resolvePartialEmoji(emoji);
|
|
if (partial) return partial;
|
|
|
|
if (typeof emoji === 'string') {
|
|
if (!emoji.match(/\d{17,20}/g)) return;
|
|
const fromCache = await cache.emojis?.get(emoji);
|
|
return (
|
|
fromCache && {
|
|
animated: fromCache.animated,
|
|
id: fromCache.id,
|
|
name: fromCache.name,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (emoji.id) {
|
|
const fromCache = await cache.emojis?.get(emoji.id);
|
|
if (fromCache)
|
|
return {
|
|
animated: fromCache.animated,
|
|
id: fromCache.id,
|
|
name: fromCache.name,
|
|
};
|
|
}
|
|
return;
|
|
}
|
|
|
|
export function encodeEmoji(rawEmoji: APIPartialEmoji) {
|
|
return rawEmoji.id ? `${rawEmoji.name}:${rawEmoji.id}` : `${rawEmoji.name}`;
|
|
}
|
|
|
|
export function hasProps<T extends Record<any, any>>(target: T, props: TypeArray<keyof T>): boolean {
|
|
if (Array.isArray(props)) {
|
|
return props.every(x => hasProps(target, x));
|
|
}
|
|
if (!((props as T[number]) in target)) {
|
|
return false;
|
|
}
|
|
if (typeof target[props] === 'string' && !target[props as T[number]].length) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function hasIntent(intents: number, target: keyof typeof GatewayIntentBits | GatewayIntentBits) {
|
|
const intent = typeof target === 'string' ? GatewayIntentBits[target] : target;
|
|
return (intents & intent) === intent;
|
|
}
|
|
|
|
export function toArrayBuffer(buffer: Buffer) {
|
|
const arrayBuffer = new ArrayBuffer(buffer.length);
|
|
const view = new Uint8Array(arrayBuffer);
|
|
for (let i = 0; i < buffer.length; ++i) {
|
|
view[i] = buffer[i];
|
|
}
|
|
return arrayBuffer;
|
|
}
|
|
|
|
export function toBuffer(arrayBuffer: ArrayBuffer) {
|
|
const buffer = Buffer.alloc(arrayBuffer.byteLength);
|
|
const view = new Uint8Array(arrayBuffer);
|
|
for (let i = 0; i < buffer.length; ++i) {
|
|
buffer[i] = view[i];
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
export function assertString(value: unknown, message?: string): asserts value is string {
|
|
if (!(typeof value === 'string' && value !== '')) {
|
|
throw new Error(message ?? 'Value is not a string');
|
|
}
|
|
}
|