From cf121a870ec1b0d64bec7aca9dd75506c8ef2cdc Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Mon, 27 Feb 2023 23:08:09 -0600 Subject: [PATCH] Fix: more client eris mismatches (#2807) * fix: type errors in nayu * fix: optional file arg in createmessage * fix: add ClientEvents hack * fix: add DiscordRESTError * fix: fmt * fix: fmt * fix: shard prop on guild * fix: permission.has type --- packages/client/src/Client.ts | 5 +- packages/client/src/Constants.ts | 3 +- packages/client/src/Structures/Permission.ts | 5 +- .../client/src/Structures/channels/Private.ts | 2 +- .../client/src/Structures/channels/Text.ts | 2 +- .../client/src/Structures/guilds/Guild.ts | 5 + packages/client/src/index.ts | 2 + packages/client/src/typings.ts | 92 +++++++++++++++++++ packages/client/src/utils/DiscordRESTError.ts | 86 +++++++++++++++++ 9 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 packages/client/src/utils/DiscordRESTError.ts diff --git a/packages/client/src/Client.ts b/packages/client/src/Client.ts index 050d374d7..d65405e6a 100644 --- a/packages/client/src/Client.ts +++ b/packages/client/src/Client.ts @@ -27,7 +27,8 @@ import { delay, getBotIdFromToken, iconBigintToHash, iconHashToBigInt } from '@d import EventEmitter from 'node:events' import Base from './Base.js' import Collection from './Collection.js' -import { Intents, IntentStrings } from './Constants.js' +import type { IntentStrings } from './Constants.js'; +import { Intents } from './Constants.js' import { CHANNEL, CHANNEL_BULK_DELETE, @@ -2381,7 +2382,7 @@ export interface ClientOptions { /** How many times to attempt resuming. */ maxResumeAttempts?: number /** The intents to use when connection to gateway. */ - intents?: GatewayIntents | number | (IntentStrings | number)[] + intents?: GatewayIntents | number | Array /** Whether or not to automatically reconnect to gateway. */ autoreconnect?: boolean /** diff --git a/packages/client/src/Constants.ts b/packages/client/src/Constants.ts index 969231d8e..2b442cfde 100644 --- a/packages/client/src/Constants.ts +++ b/packages/client/src/Constants.ts @@ -557,4 +557,5 @@ export const WebhookTypes = { APPLICATION: 3 }; -export type IntentStrings = keyof typeof Intents; \ No newline at end of file +export type IntentStrings = keyof typeof Intents; +export type PermissionClientStrings = keyof typeof Permissions; diff --git a/packages/client/src/Structures/Permission.ts b/packages/client/src/Structures/Permission.ts index 29fdf8404..bb751bb17 100644 --- a/packages/client/src/Structures/Permission.ts +++ b/packages/client/src/Structures/Permission.ts @@ -2,6 +2,7 @@ import { BitwisePermissionFlags } from '@discordeno/types' import { Base } from '../Base.js' import type { BigString } from '../Client.js' +import { PermissionClientStrings, Permissions } from '../Constants.js' export class Permission { allow: bigint @@ -36,13 +37,13 @@ export class Permission { } /** Check if this permission allows a specific permission */ - has(permission: bigint | keyof typeof BitwisePermissionFlags): boolean { + has(permission: bigint | PermissionClientStrings): boolean { if (this.isAdmin) return true if (typeof permission === 'bigint') { return (this.allow & permission) === permission } - return !!(this.allow & BigInt(BitwisePermissionFlags[permission])) + return !!(this.allow & Permissions[permission]) } toString() { diff --git a/packages/client/src/Structures/channels/Private.ts b/packages/client/src/Structures/channels/Private.ts index 1c15f3733..d110a5291 100644 --- a/packages/client/src/Structures/channels/Private.ts +++ b/packages/client/src/Structures/channels/Private.ts @@ -36,7 +36,7 @@ export class PrivateChannel extends Channel { } /** Create a message in a text channel */ - async createMessage(content: MessageContent, file: FileContent | FileContent[]): Promise { + async createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise { return await this.client.createMessage.call(this.client, this.id, content, file) } diff --git a/packages/client/src/Structures/channels/Text.ts b/packages/client/src/Structures/channels/Text.ts index 3152df0e5..50116c318 100644 --- a/packages/client/src/Structures/channels/Text.ts +++ b/packages/client/src/Structures/channels/Text.ts @@ -109,7 +109,7 @@ export class TextChannel extends GuildChannel { * @arg {String} file.name What to name the file * @returns {Promise} */ - async createMessage(content: MessageContent, file: FileContent | FileContent[]) { + async createMessage(content: MessageContent, file?: FileContent | FileContent[]) { return this.client.createMessage.call(this.client, this.id, content, file) } diff --git a/packages/client/src/Structures/guilds/Guild.ts b/packages/client/src/Structures/guilds/Guild.ts index 913798ca8..b79b3e11b 100644 --- a/packages/client/src/Structures/guilds/Guild.ts +++ b/packages/client/src/Structures/guilds/Guild.ts @@ -24,6 +24,7 @@ import type Client from '../../Client.js' import type { ImageFormat, ImageSize } from '../../Client.js' import Collection from '../../Collection.js' import { BANNER, GUILD_DISCOVERY_SPLASH, GUILD_ICON, GUILD_SPLASH } from '../../Endpoints.js' +import type Shard from '../../gateway/Shard.js' import type { AnyGuildChannel, AnyThreadChannel, @@ -185,10 +186,14 @@ export class Guild extends Base { voiceStates = new Collection() /** The cached stage instances in this guild. */ stageInstances = new Collection() + /** The shard that manages this guild. */ + shard: Shard; constructor(data: DiscordGuild, client: Client) { super(data.id) this.client = client + this.shard = client.shards.get(client.guildShardMap[this.id] || (Base.getDiscordEpoch(data.id) % (client.options.maxShards as number)) || 0)!; + this.ownerID = data.owner_id this.unavailable = !!data.unavailable diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 814fae492..fcc4bcaac 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -47,3 +47,5 @@ export * from './Structures/users/User.js' export * from './typings.js' export * from './utils/BrowserWebSocket.js' export * from './utils/Bucket.js' +export * from './utils/DiscordRESTError.js' +export * from './utils/generate.js' diff --git a/packages/client/src/typings.ts b/packages/client/src/typings.ts index 278ef640b..7f7079f8c 100644 --- a/packages/client/src/typings.ts +++ b/packages/client/src/typings.ts @@ -37,6 +37,16 @@ import type GuildAuditLogEntry from './Structures/guilds/AuditLogEntry.js' import type GuildIntegration from './Structures/guilds/Integration.js' import type Member from './Structures/guilds/Member.js' import type User from './Structures/users/User.js' +import type { Guild, Invite } from './index.js' +import type Role from './Structures/guilds/Role.js' +import type StageInstance from './Structures/guilds/StageInstance.js' +import type UnavailableGuild from './Structures/guilds/Unavailable.js' +import type AutocompleteInteraction from './Structures/interactions/Autocomplete.js' +import type CommandInteraction from './Structures/interactions/Command.js' +import type ComponentInteraction from './Structures/interactions/Component.js' +import type PingInteraction from './Structures/interactions/Ping.js' +import type UnknownInteraction from './Structures/interactions/Unknown.js' +import type { IncomingHttpHeaders } from 'node:http' export type ApplicationCommandStructure = ChatInputApplicationCommandStructure | MessageApplicationCommandStructure | UserApplicationCommandStructure export type ChatInputApplicationCommand = ApplicationCommand @@ -885,3 +895,85 @@ export const MessageFlags = { EPHEMERAL: 64, LOADING: 128, } + +export interface EventListeners { + channelCreate: [channel: AnyGuildChannel]; + channelDelete: [channel: AnyChannel]; + channelPinUpdate: [channel: TextableChannel, timestamp: number, oldTimestamp: number]; + channelUpdate: [channel: AnyGuildChannel, oldChannel: any]; + connect: [id: number]; + debug: [message: string, id?: number]; + disconnect: []; + error: [err: Error, id?: number]; + guildAvailable: [guild: Guild]; + guildBanAdd: [guild: Guild, user: User]; + guildBanRemove: [guild: Guild, user: User]; + guildCreate: [guild: Guild]; + guildDelete: [guild: any]; + guildEmojisUpdate: [guild: any, emojis: Emoji[], oldEmojis: Emoji[] | null]; + guildMemberAdd: [guild: Guild, member: Member]; + guildMemberChunk: [guild: Guild, member: Member[]]; + guildMemberRemove: [guild: Guild, member: Member | any]; + guildMemberUpdate: [guild: Guild, member: Member, oldMember: any | null]; + guildRoleCreate: [guild: Guild, role: Role]; + guildRoleDelete: [guild: Guild, role: Role]; + guildRoleUpdate: [guild: Guild, role: Role, oldRole: any]; + guildScheduledEventCreate: [event: any]; + guildScheduledEventDelete: [event: any]; + guildScheduledEventUpdate: [event: any, oldEvent: any | null]; + guildScheduledEventUserAdd: [event: any, user: User | Uncached]; + guildScheduledEventUserRemove: [event: any, user: User | Uncached]; + guildStickersUpdate: [guild: any, stickers: Sticker[], oldStickers: Sticker[] | null]; + guildUnavailable: [guild: UnavailableGuild]; + guildUpdate: [guild: Guild, oldGuild: any]; + hello: [trace: string[], id: number]; + interactionCreate: [interaction: PingInteraction | CommandInteraction | ComponentInteraction | AutocompleteInteraction | UnknownInteraction]; + inviteCreate: [guild: Guild, invite: Invite]; + inviteDelete: [guild: Guild, invite: Invite]; + messageCreate: [message: Message]; + messageDelete: [message: any]; + messageDeleteBulk: [messages: any[]]; + messageReactionAdd: [message: any, emoji: PartialEmoji, reactor: Member | Uncached]; + messageReactionRemove: [message: any, emoji: PartialEmoji, userID: string]; + messageReactionRemoveAll: [message: any]; + messageReactionRemoveEmoji: [message: any, emoji: PartialEmoji]; + messageUpdate: [message: Message, oldMessage: any | null]; + presenceUpdate: [other: Member, oldPresence: any | null]; + rawREST: [request: any]; + rawWS: [packet: any, id: number]; + ready: []; + shardPreReady: [id: number]; + stageInstanceCreate: [stageInstance: StageInstance]; + stageInstanceDelete: [stageInstance: StageInstance]; + stageInstanceUpdate: [stageInstance: StageInstance, oldStageInstance: any | null]; + threadCreate: [channel: AnyThreadChannel]; + threadDelete: [channel: AnyThreadChannel]; + threadListSync: [guild: Guild, deletedThreads: Array, activeThreads: AnyThreadChannel[], joinedThreadsMember: ThreadMember[]]; + threadMembersUpdate: [channel: AnyThreadChannel, addedMembers: ThreadMember[], removedMembers: Array]; + threadMemberUpdate: [channel: AnyThreadChannel, member: ThreadMember, oldMember: any]; + threadUpdate: [channel: AnyThreadChannel, oldChannel: any | null]; + typingStart: [channel: GuildTextableChannel | Uncached, user: User | Uncached, member: Member] + | [channel: PrivateChannel | Uncached, user: User | Uncached, member: null]; + unavailableGuildCreate: [guild: UnavailableGuild]; + unknown: [packet: any, id?: number]; + userUpdate: [user: User, oldUser: PartialUser | null]; + voiceChannelJoin: [member: Member, channel: AnyVoiceChannel]; + voiceChannelLeave: [member: Member, channel: AnyVoiceChannel]; + voiceChannelSwitch: [member: Member, newChannel: AnyVoiceChannel, oldChannel: AnyVoiceChannel]; + voiceStateUpdate: [member: Member, oldState: any]; + warn: [message: string, id?: number]; + webhooksUpdate: [data: any]; +} + +export interface ClientEvents extends EventListeners { + shardDisconnect: [err: Error | undefined, id: number]; + shardReady: [id: number]; + shardResume: [id: number]; +} + +export interface HTTPResponse { + code: number; + message: string; + errors?: HTTPResponse + headers: IncomingHttpHeaders +} diff --git a/packages/client/src/utils/DiscordRESTError.ts b/packages/client/src/utils/DiscordRESTError.ts new file mode 100644 index 000000000..c93a5f75d --- /dev/null +++ b/packages/client/src/utils/DiscordRESTError.ts @@ -0,0 +1,86 @@ +import type { ClientRequest, IncomingHttpHeaders, IncomingMessage } from 'http' +import type { HTTPResponse } from '../typings.js' + +export class DiscordRESTError extends Error { + code: number = -1 + req!: ClientRequest + res!: IncomingMessage + response!: HTTPResponse + + constructor(req: ClientRequest, res: IncomingMessage, response: HTTPResponse, stack: string) { + super() + + Object.defineProperty(this, 'req', { + enumerable: false, + value: req, + }) + Object.defineProperty(this, 'res', { + enumerable: false, + value: res, + }) + Object.defineProperty(this, 'response', { + enumerable: false, + value: response, + }) + + Object.defineProperty(this, 'code', { + enumerable: false, + value: +response.code || -1, + }) + let message = response.message || 'Unknown error' + if (response.errors) { + message += '\n ' + this.flattenErrors(response.errors).join('\n ') + } else { + const errors = this.flattenErrors(response) + if (errors.length > 0) { + message += '\n ' + errors.join('\n ') + } + } + Object.defineProperty(this, 'message', { + enumerable: false, + value: message, + }) + + if (stack) { + this.stack = this.name + ': ' + this.message + '\n' + stack + } else { + Error.captureStackTrace(this, DiscordRESTError) + } + } + + get headers(): IncomingHttpHeaders { + return this.response.headers + } + + get name(): string { + return `${this.constructor.name} [${this.code}]` + } + + flattenErrors(errors: HTTPResponse, keyPrefix?: string): string[] { + let messages: string[] = [] + for (const fieldName of Object.keys(errors)) { + if (fieldName === 'message' || fieldName === 'code') { + continue + } + + const prefix = `${keyPrefix ?? ""}${fieldName}`; + + // @ts-expect-error js hack from eris + if (errors[fieldName]._errors) { + // @ts-expect-error js hack from eris + messages = messages.concat(errors[fieldName]._errors.map((obj: any) => `${prefix}: ${obj.message as string}`)) + // @ts-expect-error js hack from eris + } else if (Array.isArray(errors[fieldName])) { + // @ts-expect-error js hack from eris + messages = messages.concat(errors[fieldName].map((str: string) => `${prefix}: ${str}`)) + // @ts-expect-error js hack from eris + } else if (typeof errors[fieldName] === 'object') { + // @ts-expect-error js hack from eris + messages = messages.concat(this.flattenErrors(errors[fieldName], `${prefix}.`)) + } + } + return messages + } +} + +export default DiscordRESTError