diff --git a/.eggignore b/.eggignore new file mode 100644 index 0000000..ecde67e --- /dev/null +++ b/.eggignore @@ -0,0 +1,3 @@ +extends .gitignore +./src/test/**/* + diff --git a/README.md b/README.md index d2191f7..f750ffe 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,15 @@ You can import the package from https://deno.land/x/harmony/mod.ts (with latest version) or can add a version too, and raw GitHub URL (latest unpublished version) https://raw.githubusercontent.com/harmonyland/harmony/main/mod.ts too. +You can also check(not import) the module in https://nest.land/package/harmony (link for importing is in the site). + For a quick example, run this: ```bash deno run --allow-net https://deno.land/x/harmony/examples/ping.ts ``` -And input your bot's token and Intents. +And input your bot's token. Here is a small example of how to use harmony, diff --git a/egg.json b/egg.json new file mode 100644 index 0000000..43e6d96 --- /dev/null +++ b/egg.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://x.nest.land/eggs@0.3.4/src/schema.json", + "name": "harmony", + "entry": "./mod.ts", + "description": "An easy to use Discord API Library for Deno.", + "homepage": "https://github.com/harmonyland/harmony", + "version": "v1.1.3", + "files": [ + "./src/**/*", + "./deps.ts", + "./README.md", + "./LICENSE", + "./banner.png", + "./CONTRIBUTING.md", + "./CODE_OF_CONDUCT.md", + "./examples/*" + ], + "checkFormat": "npx eslint src", + "checkTests": false, + "checkInstallation": false, + "check": true, + "unlisted": false, + "ignore": [] +} diff --git a/mod.ts b/mod.ts index bdfbde3..227ef63 100644 --- a/mod.ts +++ b/mod.ts @@ -14,7 +14,8 @@ export { CommandBuilder, CommandCategory, CommandsManager, - CategoriesManager + CategoriesManager, + CommandsLoader } from './src/models/command.ts' export type { CommandContext, CommandOptions } from './src/models/command.ts' export { @@ -42,6 +43,7 @@ export { ReactionUsersManager } from './src/managers/reactionUsers.ts' export { MessagesManager } from './src/managers/messages.ts' export { RolesManager } from './src/managers/roles.ts' export { UsersManager } from './src/managers/users.ts' +export { InviteManager } from './src/managers/invites.ts' export { Application } from './src/structures/application.ts' // export { ImageURL } from './src/structures/cdn.ts' export { Channel } from './src/structures/channel.ts' diff --git a/src/gateway/handlers/guildCreate.ts b/src/gateway/handlers/guildCreate.ts index 4549834..1d90d74 100644 --- a/src/gateway/handlers/guildCreate.ts +++ b/src/gateway/handlers/guildCreate.ts @@ -27,6 +27,11 @@ export const guildCreate: GatewayEventHandler = async ( if (d.voice_states !== undefined) await guild.voiceStates.fromPayload(d.voice_states) + for (const emojiPayload of d.emojis) { + if (emojiPayload.id === null) continue + await gateway.client.emojis.set(emojiPayload.id, emojiPayload) + } + if (hasGuild === undefined) { // It wasn't lazy load, so emit event gateway.client.emit('guildCreate', guild) diff --git a/src/gateway/handlers/guildDelete.ts b/src/gateway/handlers/guildDelete.ts index 5685a70..f554f99 100644 --- a/src/gateway/handlers/guildDelete.ts +++ b/src/gateway/handlers/guildDelete.ts @@ -13,6 +13,7 @@ export const guildDelete: GatewayEventHandler = async ( await guild.channels.flush() await guild.roles.flush() await guild.presences.flush() + await guild.emojis.flush() await gateway.client.guilds._delete(d.id) gateway.client.emit('guildDelete', guild) diff --git a/src/managers/base.ts b/src/managers/base.ts index e522d31..442b7fc 100644 --- a/src/managers/base.ts +++ b/src/managers/base.ts @@ -60,6 +60,13 @@ export class BaseManager { return collection } + async *[Symbol.asyncIterator](): AsyncIterableIterator { + const arr = (await this.array()) ?? [] + const { readable, writable } = new TransformStream() + arr.forEach((el) => writable.getWriter().write(el)) + yield* readable.getIterator() + } + /** Deletes everything from Cache */ flush(): any { return this.client.cache.deleteCache(this.cacheName) diff --git a/src/managers/baseChild.ts b/src/managers/baseChild.ts index adc96f8..0842859 100644 --- a/src/managers/baseChild.ts +++ b/src/managers/baseChild.ts @@ -39,4 +39,11 @@ export class BaseChildManager { } return collection } + + async *[Symbol.asyncIterator](): AsyncIterableIterator { + const arr = (await this.array()) ?? [] + const { readable, writable } = new TransformStream() + arr.forEach((el: unknown) => writable.getWriter().write(el)) + yield* readable.getIterator() + } } diff --git a/src/managers/guildChannels.ts b/src/managers/guildChannels.ts index 9d86342..2bc6837 100644 --- a/src/managers/guildChannels.ts +++ b/src/managers/guildChannels.ts @@ -66,8 +66,7 @@ export class GuildChannelsManager extends BaseChildManager< async create(options: CreateChannelOptions): Promise { if (options.name === undefined) throw new Error('name is required for GuildChannelsManager#create') - const res = ((await this.client.rest.post(GUILD_CHANNELS(this.guild.id)), - { + const res = ((await this.client.rest.post(GUILD_CHANNELS(this.guild.id), { name: options.name, type: options.type, topic: options.topic, @@ -83,7 +82,7 @@ export class GuildChannelsManager extends BaseChildManager< ? options.parent.id : options.parent, nsfw: options.nsfw - }) as unknown) as GuildChannelPayload + })) as unknown) as GuildChannelPayload await this.set(res.id, res) const channel = await this.get(res.id) diff --git a/src/managers/guilds.ts b/src/managers/guilds.ts index 8faf478..772f476 100644 --- a/src/managers/guilds.ts +++ b/src/managers/guilds.ts @@ -147,6 +147,7 @@ export class GuildManager extends BaseManager { /** Sets a value to Cache */ async set(key: string, value: GuildPayload): Promise { + value = { ...value } if ('roles' in value) value.roles = [] if ('emojis' in value) value.emojis = [] if ('members' in value) value.members = [] diff --git a/src/models/rest.ts b/src/models/rest.ts index daab5d1..b88f82a 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -3,6 +3,7 @@ import { Embed } from '../structures/embed.ts' import { MessageAttachment } from '../structures/message.ts' import { Collection } from '../utils/collection.ts' import { Client } from './client.ts' +import { simplifyAPIError } from '../utils/err_fmt.ts' export type RequestMethods = | 'get' @@ -37,15 +38,36 @@ export interface DiscordAPIErrorPayload { code?: number message?: string errors: object + requestData: { [key: string]: any } } export class DiscordAPIError extends Error { name = 'DiscordAPIError' error?: DiscordAPIErrorPayload - constructor(message?: string, error?: DiscordAPIErrorPayload) { - super(message) - this.error = error + constructor(error: string | DiscordAPIErrorPayload) { + super() + const fmt = Object.entries( + typeof error === 'object' ? simplifyAPIError(error.errors) : {} + ) + this.message = + typeof error === 'string' + ? `${error} ` + : `\n${error.method} ${error.url.slice(7)} returned ${error.status}\n(${ + error.code ?? 'unknown' + }) ${error.message}${ + fmt.length === 0 + ? '' + : `\n${fmt + .map( + (e) => + ` at ${e[0]}:\n${e[1] + .map((e) => ` - ${e}`) + .join('\n')}` + ) + .join('\n')}\n` + }` + if (typeof error === 'object') this.error = error } } @@ -269,7 +291,7 @@ export class RESTManager { const headers: RequestHeaders = { 'User-Agent': this.userAgent ?? - `DiscordBot (harmony, https://github.com/harmony-org/harmony)` + `DiscordBot (harmony, https://github.com/harmonyland/harmony)` } if (this.token !== undefined) { @@ -319,7 +341,9 @@ export class RESTManager { } } const form = new FormData() - files.forEach((file, index) => form.append(`file${index + 1}`, file.blob, file.name)) + files.forEach((file, index) => + form.append(`file${index + 1}`, file.blob, file.name) + ) const json = JSON.stringify(body) form.append('payload_json', json) if (body === undefined) body = {} @@ -448,43 +472,21 @@ export class RESTManager { new DiscordAPIError(`Request was Unauthorized. Invalid Token.\n${text}`) ) + const _data = { ...data } + if (_data?.headers !== undefined) delete _data.headers + if (_data?.method !== undefined) delete _data.method + // At this point we know it is error const error: DiscordAPIErrorPayload = { - url: response.url, + url: new URL(response.url).pathname, status, method: data.method, code: body?.code, message: body?.message, - errors: Object.fromEntries( - Object.entries( - (body?.errors as { - [name: string]: { - _errors: Array<{ code: string; message: string }> - } - }) ?? {} - ).map((entry) => { - return [entry[0], entry[1]._errors ?? []] - }) - ) + errors: body?.errors ?? {}, + requestData: _data } - // if (typeof error.errors === 'object') { - // const errors = error.errors as { - // [name: string]: { _errors: Array<{ code: string; message: string }> } - // } - // console.log(`%cREST Error:`, 'color: #F14C39;') - // Object.entries(errors).forEach((entry) => { - // console.log(` %c${entry[0]}:`, 'color: #12BC79;') - // entry[1]._errors.forEach((e) => { - // console.log( - // ` %c${e.code}: %c${e.message}`, - // 'color: skyblue;', - // 'color: #CECECE;' - // ) - // }) - // }) - // } - if ( [ HttpResponseCode.BadRequest, @@ -493,9 +495,9 @@ export class RESTManager { HttpResponseCode.MethodNotAllowed ].includes(status) ) { - reject(new DiscordAPIError(Deno.inspect(error), error)) + reject(new DiscordAPIError(error)) } else if (status === HttpResponseCode.GatewayUnavailable) { - reject(new DiscordAPIError(Deno.inspect(error), error)) + reject(new DiscordAPIError(error)) } else reject(new DiscordAPIError('Request - Unknown Error')) } diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index eac0486..fcb2577 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,6 +1,7 @@ import { Guild } from '../structures/guild.ts' import { Interaction } from '../structures/slash.ts' import { + InteractionPayload, InteractionType, SlashCommandChoice, SlashCommandOption, @@ -12,8 +13,7 @@ import { Collection } from '../utils/collection.ts' import { Client } from './client.ts' import { RESTManager } from './rest.ts' import { SlashModule } from './slashModule.ts' -import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' -import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' +import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' export class SlashCommand { slash: SlashCommandsManager @@ -372,7 +372,9 @@ export interface SlashOptions { publicKey?: string } -/** Slash Client represents an Interactions Client which can be used without Harmony Client. */ +const encoder = new TextEncoder() +const decoder = new TextDecoder('utf-8') + export class SlashClient { id: string | (() => string) client?: Client @@ -518,25 +520,52 @@ export class SlashClient { cmd.handler(interaction) } + /** Verify HTTP based Interaction */ async verifyKey( - rawBody: string | Uint8Array | Buffer, - signature: string, - timestamp: string + rawBody: string | Uint8Array, + signature: string | Uint8Array, + timestamp: string | Uint8Array ): Promise { if (this.publicKey === undefined) throw new Error('Public Key is not present') - return edverify( - signature, - Buffer.concat([ - Buffer.from(timestamp, 'utf-8'), - Buffer.from( - rawBody instanceof Uint8Array - ? new TextDecoder().decode(rawBody) - : rawBody - ) - ]), - this.publicKey - ).catch(() => false) + + const fullBody = new Uint8Array([ + ...(typeof timestamp === 'string' + ? encoder.encode(timestamp) + : timestamp), + ...(typeof rawBody === 'string' ? encoder.encode(rawBody) : rawBody) + ]) + + return edverify(signature, fullBody, this.publicKey).catch(() => false) + } + + /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction */ + async verifyServerRequest(req: { + headers: Headers + method: string + body: Deno.Reader + respond: (options: { + status?: number + body?: string | Uint8Array + }) => Promise + }): Promise { + if (req.method.toLowerCase() !== 'post') return false + + const signature = req.headers.get('x-signature-ed25519') + const timestamp = req.headers.get('x-signature-timestamp') + if (signature === null || timestamp === null) return false + + const rawbody = await Deno.readAll(req.body) + const verify = await this.verifyKey(rawbody, signature, timestamp) + if (!verify) return false + + try { + const payload: InteractionPayload = JSON.parse(decoder.decode(rawbody)) + const res = new Interaction(this as any, payload, {}) + return res + } catch (e) { + return false + } } async verifyOpineRequest(req: any): Promise { diff --git a/src/structures/cdn.ts b/src/structures/cdn.ts index 83ff27c..78c8495 100644 --- a/src/structures/cdn.ts +++ b/src/structures/cdn.ts @@ -3,11 +3,13 @@ import { ImageFormats, ImageSize } from '../types/cdn.ts' /** Function to get Image URL from a resource on Discord CDN */ export const ImageURL = ( url: string, - format: ImageFormats | undefined = 'png', - size: ImageSize | undefined = 128 + format: ImageFormats = 'png', + size: ImageSize = 128 ): string => { - size = size === undefined ? 128 : size if (url.includes('a_')) { - return `${url}.${format === undefined ? 'gif' : format}?size=${size}` - } else return `${url}.${format === 'gif' ? 'png' : format}?size=${size}` + return `${url}.${format === 'dynamic' ? 'gif' : format}?size=${size}` + } else + return `${url}.${ + format === 'gif' || format === 'dynamic' ? 'png' : format + }?size=${size}` } diff --git a/src/structures/emoji.ts b/src/structures/emoji.ts index fe71889..19cd8e1 100644 --- a/src/structures/emoji.ts +++ b/src/structures/emoji.ts @@ -1,8 +1,10 @@ import { Client } from '../models/client.ts' +import { ImageSize } from '../types/cdn.ts' import { EmojiPayload } from '../types/emoji.ts' -import { EMOJI } from '../types/endpoint.ts' +import { CUSTOM_EMOJI, EMOJI } from '../types/endpoint.ts' import { Snowflake } from '../utils/snowflake.ts' import { Base } from './base.ts' +import { ImageURL } from './cdn.ts' import { Guild } from './guild.ts' import { Role } from './role.ts' import { User } from './user.ts' @@ -54,6 +56,18 @@ export class Emoji extends Base { this.available = data.available } + /** + * Gets emoji image URL + */ + emojiImageURL( + format: 'png' | 'gif' | 'dynamic' = 'png', + size: ImageSize = 512 + ): string | undefined { + return this.id != null + ? `${ImageURL(CUSTOM_EMOJI(this.id), format, size)}` + : undefined + } + /** Modify the given emoji. Requires the MANAGE_EMOJIS permission. Returns the updated emoji object on success. Fires a Guild Emojis Update Gateway event. */ async edit(data: ModifyGuildEmojiParams): Promise { if (this.id === null) throw new Error('Emoji ID is not valid.') diff --git a/src/structures/guild.ts b/src/structures/guild.ts index 11a9d93..81bbca7 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -32,9 +32,13 @@ import { User } from './user.ts' import { Application } from './application.ts' import { GUILD_BAN, + GUILD_BANNER, GUILD_BANS, + GUILD_DISCOVERY_SPLASH, + GUILD_ICON, GUILD_INTEGRATIONS, - GUILD_PRUNE + GUILD_PRUNE, + GUILD_SPLASH } from '../types/endpoint.ts' import { GuildVoiceStatesManager } from '../managers/guildVoiceStates.ts' import { RequestMembersOptions } from '../gateway/index.ts' @@ -42,6 +46,8 @@ import { GuildPresencesManager } from '../managers/presences.ts' import { TemplatePayload } from '../types/template.ts' import { Template } from './template.ts' import { DiscordAPIError } from '../models/rest.ts' +import { ImageFormats, ImageSize } from '../types/cdn.ts' +import { ImageURL } from './cdn.ts' export class GuildBan extends Base { guild: Guild @@ -258,6 +264,58 @@ export class Guild extends SnowflakeBase { } } + /** + * Gets guild icon URL + */ + iconURL( + format: ImageFormats = 'png', + size: ImageSize = 512 + ): string | undefined { + return this.icon != null + ? `${ImageURL(GUILD_ICON(this.id, this.icon), format, size)}` + : undefined + } + + /** + * Gets guild splash URL + */ + splashURL( + format: ImageFormats = 'png', + size: ImageSize = 512 + ): string | undefined { + return this.splash != null + ? `${ImageURL(GUILD_SPLASH(this.id, this.splash), format, size)}` + : undefined + } + + /** + * Gets guild discover splash URL + */ + discoverSplashURL( + format: ImageFormats = 'png', + size: ImageSize = 512 + ): string | undefined { + return this.discoverySplash != null + ? `${ImageURL( + GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash), + format, + size + )}` + : undefined + } + + /** + * Gets guild banner URL + */ + bannerURL( + format: ImageFormats = 'png', + size: ImageSize = 512 + ): string | undefined { + return this.banner != null + ? `${ImageURL(GUILD_BANNER(this.id, this.banner), format, size)}` + : undefined + } + /** * Gets Everyone role of the Guild */ diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 9b8cc10..3714697 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -86,28 +86,18 @@ export class InteractionUser extends User { } export class Interaction extends SnowflakeBase { - /** Type of Interaction */ - type: InteractionType - /** Interaction Token */ + /** This will be `SlashClient` in case of `SlashClient#verifyServerRequest` */ + client: Client + type: number token: string /** Interaction ID */ id: string - /** Data sent with Interaction. Only applies to Application Command */ - data?: InteractionApplicationCommandData - /** Channel in which Interaction was initiated */ - channel?: TextChannel | GuildTextChannel - /** Guild in which Interaction was initiated */ - guild?: Guild - /** Member object of who initiated the Interaction */ - member?: Member - /** User object of who invoked Interaction */ - user: User - /** Whether we have responded to Interaction or not */ - responded: boolean = false - /** Resolved data for Snowflakes in Slash Command Arguments */ - resolved: InteractionApplicationCommandResolved - /** Whether response was deferred or not */ - deferred: boolean = false + data: InteractionData + channel: GuildTextChannel + guild: Guild + member: Member + _savedHook?: Webhook + _respond?: (data: InteractionResponsePayload) => unknown constructor( client: Client, diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index 7d89da2..5f05f8d 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -145,12 +145,15 @@ export class TextChannel extends Channel { emoji: Emoji | string ): Promise { if (emoji instanceof Emoji) { - emoji = emoji.getEmojiString + emoji = `${emoji.name}:${emoji.id}` + } else if (emoji.length > 4) { + if (!isNaN(Number(emoji))) { + const findEmoji = await this.client.emojis.get(emoji) + if (findEmoji !== undefined) emoji = `${findEmoji.name}:${findEmoji.id}` + else throw new Error(`Emoji not found: ${emoji}`) + } } - if (message instanceof Message) { - message = message.id - } - + if (message instanceof Message) message = message.id const encodedEmoji = encodeURI(emoji) await this.client.rest.put( @@ -165,11 +168,15 @@ export class TextChannel extends Channel { user?: User | Member | string ): Promise { if (emoji instanceof Emoji) { - emoji = emoji.getEmojiString - } - if (message instanceof Message) { - message = message.id + emoji = `${emoji.name}:${emoji.id}` + } else if (emoji.length > 4) { + if (!isNaN(Number(emoji))) { + const findEmoji = await this.client.emojis.get(emoji) + if (findEmoji !== undefined) emoji = `${findEmoji.name}:${findEmoji.id}` + else throw new Error(`Emoji not found: ${emoji}`) + } } + if (message instanceof Message) message = message.id if (user !== undefined) { if (typeof user !== 'string') { user = user.id diff --git a/src/test/index.ts b/src/test/index.ts index ba93b2a..f44cc21 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -117,7 +117,7 @@ client.on('messageCreate', async (msg: Message) => { msg.channel.send('Failed...') } } else if (msg.content === '!react') { - msg.addReaction('🤔') + msg.addReaction('a:programming:785013658257195008') } else if (msg.content === '!wait_for') { msg.channel.send('Send anything!') const [receivedMsg] = await client.waitFor( @@ -210,11 +210,30 @@ client.on('messageCreate', async (msg: Message) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion msg.member as Member ) - msg.channel.send( - Object.entries(permissions.serialize()) - .map((e) => `${e[0]}: ${e[1] === true ? '`✅`' : '`❌`'}`) - .join('\n') - ) + msg.channel.send(`Your permissions:\n${permissions.toArray().join('\n')}`) + } else if (msg.content === '!addAllRoles') { + const roles = await msg.guild?.roles.array() + if (roles !== undefined) { + roles.forEach(async (role) => { + await msg.member?.roles.add(role) + console.log(role) + }) + } + } else if (msg.content === '!createAndAddRole') { + if (msg.guild !== undefined) { + const role = await msg.guild.roles.create({ + name: 'asdf', + permissions: 0 + }) + await msg.member?.roles.add(role) + } + } else if (msg.content === '!roles') { + let buf = 'Roles:' + if (msg.member === undefined) return + for await (const role of msg.member.roles) { + buf += `\n${role.name}` + } + msg.reply(buf) } }) diff --git a/src/test/slash-http.ts b/src/test/slash-http.ts new file mode 100644 index 0000000..02f9836 --- /dev/null +++ b/src/test/slash-http.ts @@ -0,0 +1,44 @@ +import { SlashClient } from '../../mod.ts' +import { SLASH_ID, SLASH_PUB_KEY, SLASH_TOKEN } from './config.ts' +import { listenAndServe } from 'https://deno.land/std@0.90.0/http/server.ts' + +const slash = new SlashClient({ + id: SLASH_ID, + token: SLASH_TOKEN, + publicKey: SLASH_PUB_KEY +}) + +await slash.commands.bulkEdit([ + { + name: 'ping', + description: 'Just ping!' + } +]) + +const options = { port: 8000 } +console.log('Listen on port: ' + options.port.toString()) +listenAndServe(options, async (req) => { + const verify = await slash.verifyServerRequest(req) + if (verify === false) + return req.respond({ status: 401, body: 'not authorized' }) + + const respond = async (d: any): Promise => + req.respond({ + status: 200, + body: JSON.stringify(d), + headers: new Headers({ + 'content-type': 'application/json' + }) + }) + + const body = JSON.parse( + new TextDecoder('utf-8').decode(await Deno.readAll(req.body)) + ) + if (body.type === 1) return await respond({ type: 1 }) + await respond({ + type: 4, + data: { + content: 'Pong!' + } + }) +}) diff --git a/src/types/cdn.ts b/src/types/cdn.ts index 4fbfc13..b78bd48 100644 --- a/src/types/cdn.ts +++ b/src/types/cdn.ts @@ -1,2 +1,2 @@ export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 -export type ImageFormats = 'jpg' | 'jpeg' | 'png' | 'webp' | 'gif' +export type ImageFormats = 'jpg' | 'jpeg' | 'png' | 'webp' | 'gif' | 'dynamic' diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts index 8631f1f..9e77396 100644 --- a/src/utils/bitfield.ts +++ b/src/utils/bitfield.ts @@ -14,7 +14,10 @@ export class BitField { #flags: { [name: string]: number | bigint } = {} bitfield: bigint - constructor(flags: { [name: string]: number | bigint }, bits: any) { + constructor( + flags: { [name: string]: number | bigint }, + bits: BitFieldResolvable + ) { this.#flags = flags this.bitfield = BitField.resolve(this.#flags, bits) } @@ -104,11 +107,11 @@ export class BitField { if (bit instanceof BitField) return this.resolve(flags, bit.bitfield) if (Array.isArray(bit)) return (bit.map as any)((p: any) => this.resolve(flags, p)).reduce( - (prev: any, p: any) => prev | p, - 0 + (prev: bigint, p: bigint) => prev | p, + 0n ) if (typeof bit === 'string' && typeof flags[bit] !== 'undefined') - return flags[bit] + return BigInt(flags[bit]) const error = new RangeError('BITFIELD_INVALID') throw error } diff --git a/src/utils/err_fmt.ts b/src/utils/err_fmt.ts new file mode 100644 index 0000000..6639984 --- /dev/null +++ b/src/utils/err_fmt.ts @@ -0,0 +1,23 @@ +export interface SimplifiedError { + [name: string]: string[] +} + +export function simplifyAPIError(errors: any): SimplifiedError { + const res: SimplifiedError = {} + function fmt(obj: any, acum: string = ''): void { + if (typeof obj._errors === 'object' && Array.isArray(obj._errors)) + res[acum] = obj._errors.map((e: any) => `${e.code}: ${e.message}`) + else { + Object.entries(obj).forEach((obj: [string, any]) => { + const arrayIndex = !isNaN(Number(obj[0])) + if (arrayIndex) obj[0] = `[${obj[0]}]` + if (acum !== '' && !arrayIndex) acum += '.' + fmt(obj[1], (acum += obj[0])) + }) + } + } + Object.entries(errors).forEach((obj: [string, any]) => { + fmt(obj[1], obj[0]) + }) + return res +}