diff --git a/packages/client/.c8rc.json b/packages/client/.c8rc.json new file mode 100644 index 000000000..f135073e4 --- /dev/null +++ b/packages/client/.c8rc.json @@ -0,0 +1,7 @@ +{ + "all": true, + "src": "src", + "reporter": ["text", "lcov"], + "include": ["src/**/*.ts"], + "exclude": ["tests"] +} diff --git a/packages/client/.mocharc.json b/packages/client/.mocharc.json new file mode 100644 index 000000000..4689d4a04 --- /dev/null +++ b/packages/client/.mocharc.json @@ -0,0 +1,10 @@ +{ + "require": "ts-node/register", + "loader": "ts-node/esm", + "recursive": true, + "timeout": 2000, + "watch-extensions": "ts", + "watch-files": ["src", "tests"], + "enable-source-maps": true, + "parallel": false +} diff --git a/packages/client/.swcrc b/packages/client/.swcrc new file mode 100644 index 000000000..5ae43bb99 --- /dev/null +++ b/packages/client/.swcrc @@ -0,0 +1,31 @@ +{ + "minify": true, + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "target": "es2022", + "keepClassNames": true, + "loose": true, + "minify": { + "compress": { + "unused": true + }, + "mangle": true + } + }, + "module": { + "type": "es6", + "strict": false, + "strictMode": true, + "lazy": false, + "noInterop": false + }, + "sourceMaps": "inline" +} \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 000000000..7538251aa --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,47 @@ +{ + "name": "@discordeno/client", + "version": "18.0.0-alpha.1", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/discordeno/discordeno.git" + }, + "scripts": { + "build": "swc --delete-dir-on-start src --out-dir dist", + "build:type": "tsc --declaration --emitDeclarationOnly --declarationDir dist", + "release-build": "yarn build && yarn build:type", + "fmt": "eslint --fix \"src/**/*.ts*\"", + "lint": "eslint \"src/**/*.ts*\"", + "test:unit-coverage": "c8 mocha --no-warnings 'tests/**/*.spec.ts'", + "test:unit": "c8 --r lcov mocha --no-warnings 'tests/**/*.spec.ts' && node ../../scripts/coveragePathFixing.js client", + "test:deno-unit": "swc tests --delete-dir-on-start --out-dir denoTestsDist && node ../../scripts/fixDenoTestExtension.js && deno test -A --import-map ../../denoImportMap.json denoTestsDist", + "test:unit:watch": "mocha --no-warnings --watch --parallel 'tests/**/*.spec.ts'", + "test:type": "tsc --noEmit", + "test:test-type": "tsc --project tsconfig.test.json" + }, + "dependencies": { + "@discordeno/gateway": "18.0.0-alpha.1", + "@discordeno/rest": "18.0.0-alpha.1", + "@discordeno/utils": "18.0.0-alpha.1" + }, + "devDependencies": { + "@discordeno/types": "18.0.0-alpha.1", + "@swc/cli": "^0.1.57", + "@swc/core": "^1.3.21", + "@types/chai": "^4", + "@types/mocha": "^10", + "@types/node": "^18.11.9", + "@types/sinon": "^10.0.13", + "c8": "^7.12.0", + "chai": "^4.3.7", + "eslint": "^8.0.1", + "eslint-config-discordeno": "*", + "mocha": "^10.1.0", + "sinon": "^15.0.0", + "tsconfig": "*", + "typescript": "^4.9.3" + } +} diff --git a/packages/client/src/Base.ts b/packages/client/src/Base.ts new file mode 100644 index 000000000..f20717004 --- /dev/null +++ b/packages/client/src/Base.ts @@ -0,0 +1,77 @@ +import type { BigString } from './Client.js' + +export class Base { + /** Internal storage of the id done in bigint to preserve memory */ + _id: bigint + + constructor(id: BigString) { + this._id = BigInt(id) + } + + /** The snowflake id */ + get id(): string { + return this._id.toString() + } + + set id(value: BigString) { + this._id = BigInt(value) + } + + get createdAt(): number { + return Number(this._id / 4194304n + 1420070400000n) + } + + /** + * Calculates the timestamp in milliseconds associated with a Discord ID/snowflake. + * @deprecated Recommend using Object.createdAt or Client.snowflakeToTimestamp if you want to get a timestamp from a id. This is not desired but supported only to keep a similar api to eris. + */ + static getCreatedAt(id: string): number { + return Base.getDiscordEpoch(id) + 1420070400000 + } + + /** + * Gets the number of milliseconds since epoch represented by an ID/snowflake + * @deprecated Recommend using Object.createdAt or Client.snowflakeToTimestamp if you want to get a timestamp from a id. This is not desired but supported only to keep a similar api to eris. + */ + static getDiscordEpoch(id: string): number { + // @ts-expect-error some eris magic at play here + return Math.floor(id / 4194304) + } + + toString(): string { + return `[${this.constructor.name} ${this.id}]` + } + + toJSON(props: string[] = []): Record { + const json = { + id: this.id, + createdAt: this.createdAt, + } + for (const prop of props) { + // @ts-expect-error some eris magic at play here + const value = this[prop] + const type = typeof value + if (value === undefined) { + continue + } else if ((type !== 'object' && type !== 'function' && type !== 'bigint') || value === null) { + // @ts-expect-error some eris magic at play here + json[prop] = value + } else if (value.toJSON !== undefined) { + // @ts-expect-error some eris magic at play here + json[prop] = value.toJSON() + } else if (value.values !== undefined) { + // @ts-expect-error some eris magic at play here + json[prop] = [...value.values()] + } else if (type === 'bigint') { + // @ts-expect-error some eris magic at play here + json[prop] = value.toString() + } else if (type === 'object') { + // @ts-expect-error some eris magic at play here + json[prop] = value + } + } + return json + } +} + +export default Base diff --git a/packages/client/src/Client.ts b/packages/client/src/Client.ts new file mode 100644 index 000000000..575dd782f --- /dev/null +++ b/packages/client/src/Client.ts @@ -0,0 +1,2345 @@ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DiscordAllowedMentions, DiscordAuditLogEntry, DiscordInvite, DiscordTemplate, GatewayIntents } from '@discordeno/types' +import { + AllowedMentionsTypes, + ChannelTypes, + type DiscordChannel, + type DiscordGetGatewayBot, + type DiscordGuild, + type DiscordIntegration, + type DiscordMember, + type DiscordMemberWithUser, + type DiscordMessage, + type DiscordRole, + type DiscordThreadMember, + type DiscordUser, + type GetMessagesOptions, + type OverwriteTypes, +} from '@discordeno/types' +import { delay } from '@discordeno/utils' +import EventEmitter from 'events' +import Base from './Base.js' +import Collection from './Collection.js' +import { + CHANNEL, + CHANNEL_BULK_DELETE, + CHANNEL_CROSSPOST, + CHANNEL_FOLLOW, + CHANNEL_INVITES, + CHANNEL_MESSAGE, + CHANNEL_MESSAGES, + CHANNEL_MESSAGE_REACTION, + CHANNEL_MESSAGE_REACTIONS, + CHANNEL_MESSAGE_REACTION_USER, + CHANNEL_PERMISSION, + CHANNEL_PIN, + CHANNEL_PINS, + CHANNEL_TYPING, + CHANNEL_WEBHOOKS, + COMMAND, + COMMANDS, + COMMAND_PERMISSIONS, + CUSTOM_EMOJI_GUILD, + DISCOVERY_CATEGORIES, + DISCOVERY_VALIDATION, + GATEWAY, + GATEWAY_BOT, + GUILD, + GUILDS, + GUILD_AUDIT_LOGS, + GUILD_BAN, + GUILD_BANS, + GUILD_CHANNELS, + GUILD_COMMAND, + GUILD_COMMANDS, + GUILD_COMMAND_PERMISSIONS, + GUILD_DISCOVERY, + GUILD_DISCOVERY_CATEGORY, + GUILD_EMOJI, + GUILD_EMOJIS, + GUILD_INTEGRATION, + GUILD_INTEGRATIONS, + GUILD_INTEGRATION_SYNC, + GUILD_INVITES, + GUILD_MEMBER, + GUILD_MEMBERS, + GUILD_MEMBERS_SEARCH, + GUILD_MEMBER_ROLE, + GUILD_PREVIEW, + GUILD_PRUNE, + GUILD_ROLE, + GUILD_ROLES, + GUILD_STICKER, + GUILD_STICKERS, + GUILD_TEMPLATE, + GUILD_TEMPLATES, + GUILD_TEMPLATE_GUILD, + GUILD_VANITY_URL, + GUILD_VOICE_REGIONS, + GUILD_VOICE_STATE, + GUILD_WEBHOOKS, + GUILD_WELCOME_SCREEN, + GUILD_WIDGET, + GUILD_WIDGET_SETTINGS, + INTERACTION_RESPOND, + INVITE, + OAUTH2_APPLICATION, + STAGE_INSTANCE, + STAGE_INSTANCES, + STICKER, + STICKER_PACKS, + THREADS_ARCHIVED, + THREADS_ARCHIVED_JOINED, + THREADS_GUILD_ACTIVE, + THREAD_MEMBER, + THREAD_MEMBERS, + THREAD_WITHOUT_MESSAGE, + THREAD_WITH_MESSAGE, + USER, + USER_CHANNELS, + USER_GUILD, + USER_GUILDS, + VOICE_REGIONS, + WEBHOOK, + WEBHOOK_MESSAGE, + WEBHOOK_TOKEN, + WEBHOOK_TOKEN_SLACK, +} from './Endpoints.js' +import ShardManager from './gateway/ShardManager.js' +import RequestHandler from './RequestHandler.js' +import type CategoryChannel from './Structures/channels/Category.js' +import Channel from './Structures/channels/Channel.js' +import type NewsChannel from './Structures/channels/News.js' +import PrivateChannel from './Structures/channels/Private.js' +import type StageChannel from './Structures/channels/Stage.js' +import type TextChannel from './Structures/channels/Text.js' +import type TextVoiceChannel from './Structures/channels/TextVoice.js' +import ThreadMember from './Structures/channels/threads/Member.js' +import type NewsThreadChannel from './Structures/channels/threads/NewsThread.js' +import type PrivateThreadChannel from './Structures/channels/threads/PrivateThread.js' +import type PublicThreadChannel from './Structures/channels/threads/PublicThread.js' +import GuildAuditLogEntry from './Structures/guilds/AuditLogEntry.js' +import Guild from './Structures/guilds/Guild.js' +import GuildIntegration from './Structures/guilds/Integration.js' +import Member from './Structures/guilds/Member.js' +import GuildPreview from './Structures/guilds/Preview.js' +import Role from './Structures/guilds/Role.js' +import StageInstance from './Structures/guilds/StageInstance.js' +import GuildTemplate from './Structures/guilds/Template.js' +import type UnavailableGuild from './Structures/guilds/Unavailable.js' +import Invite from './Structures/Invite.js' +import Message from './Structures/Message.js' +import Permission from './Structures/Permission.js' +import ExtendedUser from './Structures/users/Extended.js' +import User from './Structures/users/User.js' +import type { + AllowedMentions, + AnyChannel, + AnyGuildChannel, + ApplicationCommand, + ApplicationCommandPermissions, + ApplicationCommandStructure, + ChannelFollow, + ChannelPosition, + CreateChannelInviteOptions, + CreateChannelOptions, + CreateGuildOptions, + CreateStickerOptions, + CreateThreadOptions, + CreateThreadWithoutMessageOptions, + DiscoveryCategory, + DiscoveryMetadata, + DiscoveryOptions, + DiscoverySubcategoryResponse, + EditChannelOptions, + EditChannelPositionOptions, + EditStickerOptions, + Emoji, + EmojiOptions, + FileContent, + GetArchivedThreadsOptions, + GetGuildAuditLogOptions, + GetGuildBansOptions, + GetMessageReactionOptions, + GetPruneOptions, + GetRESTGuildMembersOptions, + GetRESTGuildsOptions, + GuildApplicationCommandPermissions, + GuildAuditLog, + GuildBan, + GuildOptions, + GuildTemplateOptions, + GuildVanity, + IntegrationOptions, + InteractionResponse, + ListedChannelThreads, + ListedGuildThreads, + MemberOptions, + MessageContent, + MessageContentEdit, + MessageWebhookContent, + OAuthApplicationInfo, + PruneMemberOptions, + PurgeChannelOptions, + RoleOptions, + StageInstanceOptions, + Sticker, + StickerPack, + VoiceRegion, + VoiceStateOptions, + Webhook, + WebhookOptions, + WebhookPayload, + WelcomeScreen, + WelcomeScreenOptions, + Widget, + WidgetData, +} from './typings.js' + +export class Client extends EventEmitter { + /** The cleaned up version of the provided configurations for the client. */ + options: ParsedClientOptions + /** The token used for this client. */ + token: string + /** The timestamp in milliseconds when this client was created. */ + startTime = Date.now() + + CDN_URL = 'https://cdn.discordapp.com' + CLIENT_URL = 'https://discord.com' + + guilds = new Collection() + unavailableGuilds = new Collection() + users = new Collection() + _channelGuildMap = new Collection() + _threadGuildMap = new Collection() + _privateChannelMap = new Collection() + privateChannels = new Collection() + + guildShardMap: Record + + /** Rest manager. Not recommended. */ + requestHandler: RequestHandler + /** Gateway manager. Not recommended */ + shards: ShardManager + /** The gateway url to connect to. */ + gatewayURL: string = '' + /** Whether or not the client is fully ready. */ + ready = false + + constructor(token: string, options: ClientOptions) { + super() + + this.options = { + apiVersion: options.apiVersion ?? 10, + allowedMentions: this._formatAllowedMentions(options.allowedMentions), + defaultImageFormat: options.defaultImageFormat ?? 'png', + defaultImageSize: options.defaultImageSize ?? 128, + proxyURL: options.proxyURL, + proxyRestAuthorization: options.proxyRestAuthorization, + applicationId: options.applicationId, + messageLimit: options.messageLimit, + seedVoiceConnections: options.seedVoiceConnections ?? true, + shardConcurrency: options.shardConcurrency ?? 'auto', + maxShards: options.maxShards ?? 'auto', + compress: false, + // compress: options.compress ?? false, + lastShardID: options.lastShardID, + maxResumeAttempts: options.maxResumeAttempts ?? Infinity, + intents: options.intents ?? 0, + } + + this.token = token + + this.guildShardMap = {} + this.requestHandler = new RequestHandler(this, {}) + + this.shards = new ShardManager(this, { + concurrency: this.options.shardConcurrency, + }) + + // NO PROXY REST START ALARMS + if (!this.proxyURL) this.requestHandler.warnUser() + + this.shards = new ShardManager(this, { + concurrency: typeof this.options.shardConcurrency === 'number' ? this.options.shardConcurrency : undefined, + }) + } + + /** The amount of time in milliseconds that this client has been online for. */ + get uptime(): number { + return Date.now() - this.startTime + } + + /** The api version to use. */ + get apiVersion(): ApiVersions { + return this.options.apiVersion + } + + /** Change the api version when making requests. */ + set apiVersion(version: ApiVersions) { + this.options.apiVersion = version + } + + /** The base url that will be used when making requests for discord api. */ + get BASE_URL(): string { + return `/api/v${this.apiVersion}` + } + + /** The url to the REST proxy to send requests to. */ + get proxyURL(): string { + return this.options.proxyURL ?? '' + } + + /** The password/authorization to confirm that these request made to your rest proxy are indeed from you and not a hacker. */ + get proxyRestAuthorization(): string { + return this.options.proxyRestAuthorization ?? '' + } + + /** The application id(NOT the bot id). The bot id and application id are the same for newer bots but older bots have different ids. */ + get applicationId(): BigString { + return this.options.applicationId + } + + /** Whether or not to seed voice connections. */ + get seedVoiceConnections(): boolean { + return this.options.seedVoiceConnections + } + + get id(): BigString { + return getBotIdFromToken(this.token) + } + + get channelGuildMap(): Record { + return this._channelGuildMap.toRecord() + } + + get threadGuildMap(): Record { + return this._threadGuildMap.toRecord() + } + + get privateChannelMap(): Record { + return this._privateChannelMap.toRecord() + } + + /** Tells all shards to connect. This will call `getBotGateway()`, which is ratelimited. */ + async connect(): Promise { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + if (typeof this.token !== 'string') throw new Error(`Invalid token "${this.token}"`) + + try { + const data = await this.getBotGateway() + + if (data.url.includes('?')) { + data.url = data.url.substring(0, data.url.indexOf('?')) + } + if (!data.url.endsWith('/')) { + data.url += '/' + } + this.gatewayURL = `${data.url}?v=${API_VERSION}&encoding=${'json'}` + + if (this.options.compress) { + this.gatewayURL += '&compress=zlib-stream' + } + + if (this.options.maxShards === 'auto') { + this.options.maxShards = data.shards + if (this.options.lastShardID === undefined) { + this.options.lastShardID = data.shards - 1 + } + } + + if (this.options.shardConcurrency === 'auto' && data.session_start_limit && typeof data.session_start_limit.max_concurrency === 'number') { + this.shards.setConcurrency(data.session_start_limit.max_concurrency) + } + + for (let i = this.options.firstShardID; i <= this.options.lastShardID; ++i) { + this.shards.spawn(i) + } + } catch (err) { + if (!this.options.autoreconnect) { + throw err + } + + const reconnectDelay = this.options.reconnectDelay(this.lastReconnectDelay, this.reconnectAttempts) + + await delay(reconnectDelay) + this.lastReconnectDelay = reconnectDelay + this.reconnectAttempts = this.reconnectAttempts + 1 + + return await this.connect() + } + } + + /** Make a request to the discord api. */ + async makeRequest(data: RequestData) { + return await fetch(`${this.proxyURL}/${this.BASE_URL}/${data.url}`, { + method: data.method, + headers: { + 'Content-Type': 'application/json', + Authorization: this.proxyRestAuthorization, + 'X-Audit-Log-Reason': data.reason ?? '', + ...(data.headers ?? {}), + }, + body: data.body + ? JSON.stringify(data.body, (str) => { + if (str.endsWith('ID')) str = str.substring(0, str.length - 2) + 'Id' + + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) + }) + : undefined, + }) + .then(async (res) => await res.json()) + .catch((error) => { + console.log(error) + return null + }) + } + + /** Make a GET request to the discord api. */ + async get(url: string) { + return await this.makeRequest({ + method: 'GET', + url, + }) + } + + /** Make a POST request to the discord api. */ + async post( + url: string, + payload?: { + body?: Record + reason?: string + file?: FileContent | FileContent[] + }, + ) { + return await this.makeRequest({ + method: 'POST', + url, + body: payload?.body, + reason: payload?.reason, + file: payload?.file, + }) + } + + /** Make a PATCH request to the discord api. */ + async patch( + url: string, + payload?: { + body?: Record | null | string | any[] + reason?: string + file?: FileContent | FileContent[] + }, + ) { + return await this.makeRequest({ + method: 'PATCH', + url, + body: payload?.body, + reason: payload?.reason, + file: payload?.file, + }) + } + + /** Make a PUT request to the discord api. */ + async put( + url: string, + payload?: { + body?: Record | any[] + reason?: string + }, + ) { + return await this.makeRequest({ + method: 'PUT', + url, + body: payload?.body, + reason: payload?.reason, + }) + } + + /** Make a DELETE request to the discord api. */ + async delete(url: string, payload?: { reason?: string }) { + return await this.makeRequest({ + method: 'DELETE', + url, + reason: payload?.reason, + }) + } + + /** Add a guild discovery subcategory */ + async addGuildDiscoverySubcategory(guildID: BigString, categoryID: BigString, reason?: string): Promise { + return await this.post(GUILD_DISCOVERY_CATEGORY(guildID, categoryID), { + reason, + }) + } + + /** Add a role to a guild member */ + async addGuildMemberRole(guildID: BigString, memberID: BigString, roleID: BigString, reason?: string): Promise { + return await this.put(GUILD_MEMBER_ROLE(guildID, memberID, roleID), { + reason, + }) + } + + /** Add a reaction to a message */ + async addMessageReaction(channelID: BigString, messageID: BigString, reaction: string): Promise { + if (reaction === decodeURI(reaction)) { + reaction = encodeURIComponent(reaction) + } + + return await this.put(CHANNEL_MESSAGE_REACTION_USER(channelID, messageID, reaction, '@me')) + } + + /** Ban a user from a guild */ + async banGuildMember(guildID: BigString, userID: BigString, deleteMessageDays = 0, reason?: string): Promise { + if (deleteMessageDays < 0 || deleteMessageDays > 7) { + return await Promise.reject(new Error(`Invalid deleteMessageDays value (${deleteMessageDays}), should be a number between 0-7 inclusive`)) + } + + return await this.put(GUILD_BAN(guildID, userID), { + reason, + body: { delete_message_days: deleteMessageDays }, + }) + } + + /** Bulk create/edit global application commands */ + async bulkEditCommands(commands: ApplicationCommandStructure[]): Promise { + for (const command of commands) { + if (command.name !== undefined) { + if (command.type === 1 || command.type === undefined) { + command.name = command.name.toLowerCase() + if (!command.name.match(/^[\w-]{1,32}$/)) { + throw new Error('Slash Command names must match the regular expression "^[\\w-]{1,32}$"') + } + } + } + } + + return await this.put(COMMANDS(this.applicationId), { body: commands }) + } + + /** Bulk create/edit guild application commands */ + async bulkEditGuildCommands(guildID: BigString, commands: ApplicationCommand[]): Promise { + for (const command of commands) { + if (command.name !== undefined) { + if (command.type === 1 || command.type === undefined) { + command.name = command.name.toLowerCase() + if (!command.name.match(/^[\w-]{1,32}$/)) { + throw new Error('Slash Command names must match the regular expression "^[\\w-]{1,32}$"') + } + } + } + } + + return await this.put(GUILD_COMMANDS(this.applicationId, guildID), { + body: commands, + }) + } + + /** Create a channel in a guild */ + async createChannel(guildID: BigString, name: string): Promise + async createChannel(guildID: BigString, name: string, type: ChannelTypes.GuildText, options?: CreateChannelOptions): Promise + async createChannel(guildID: BigString, name: string, type: ChannelTypes.GuildVoice, options?: CreateChannelOptions): Promise + async createChannel(guildID: BigString, name: string, type: ChannelTypes.GuildCategory, options?: CreateChannelOptions): Promise + async createChannel(guildID: BigString, name: string, type: ChannelTypes.GuildAnnouncement, options?: CreateChannelOptions): Promise + async createChannel(guildID: BigString, name: string, type: ChannelTypes.GuildStageVoice, options?: CreateChannelOptions): Promise + async createChannel(guildID: BigString, name: string, type?: number, options?: CreateChannelOptions): Promise { + return await this.post(GUILD_CHANNELS(guildID), { + reason: options?.reason, + body: { + name, + type: type ?? ChannelTypes.GuildText, + bitrate: options?.bitrate, + nsfw: options?.nsfw, + parent_id: options?.parentID, + permission_overwrites: options?.permissionOverwrites, + position: options?.position, + rate_limit_per_user: options?.rateLimitPerUser, + topic: options?.topic, + user_limit: options?.userLimit, + }, + }).then((channel) => Channel.from(channel, this)) + } + + /** Create an invite for a channel */ + async createChannelInvite(channelID: BigString, options: CreateChannelInviteOptions = {}, reason?: string): Promise { + return await this.post(CHANNEL_INVITES(channelID), { + body: { + max_age: options.maxAge, + max_uses: options.maxUses, + target_application_id: options.targetApplicationID, + target_type: options.targetType, + target_user_id: options.targetUserID, + temporary: options.temporary, + unique: options.unique, + }, + reason, + }).then((invite) => new Invite(invite, this)) + } + + /** Create a channel webhook */ + async createChannelWebhook(channelID: BigString, options: { name: string; avatar?: string | null }, reason?: string): Promise { + return await this.post(CHANNEL_WEBHOOKS(channelID), { + reason, + body: options, + }) + } + + /** Create a global application command */ + async createCommand(command: ApplicationCommandStructure): Promise { + if (command.name !== undefined) { + if (command.type === 1 || command.type === undefined) { + command.name = command.name.toLowerCase() + if (!command.name.match(/^[\w-]{1,32}$/)) { + throw new Error('Slash Command names must match the regular expression "^[\\w-]{1,32}$"') + } + } + } + + // @ts-expect-error some eris magic at play here + command.default_permission = command.defaultPermission + return await this.post(COMMANDS(this.applicationId), { body: command }) + } + + /** Create a guild */ + async createGuild(name: string, options?: CreateGuildOptions): Promise { + if (this.guilds.size > 9) { + throw new Error("This method can't be used when in 10 or more guilds.") + } + + return await this.post(GUILDS, { + body: { + name, + icon: options?.icon, + verification_level: options?.verificationLevel, + default_message_notifications: options?.defaultNotifications, + explicit_content_filter: options?.explicitContentFilter, + system_channel_id: options?.systemChannelID, + afk_channel_id: options?.afkChannelID, + afk_timeout: options?.afkTimeout, + roles: options?.roles, + channels: options?.channels, + }, + }).then((guild) => new Guild(guild, this)) + } + + /** Create a guild application command */ + async createGuildCommand(guildID: BigString, command: ApplicationCommandStructure): Promise { + if (command.name !== undefined) { + if (command.type === 1 || command.type === undefined) { + command.name = command.name.toLowerCase() + if (!command.name.match(/^[\w-]{1,32}$/)) { + throw new Error('Slash Command names must match the regular expression "^[\\w-]{1,32}$"') + } + } + } + // @ts-expect-error some eris magic at play here + command.default_permission = command.defaultPermission + + return await this.post(GUILD_COMMANDS(this.applicationId, guildID), { + body: command, + }) + } + + /** Create a guild emoji object */ + async createGuildEmoji(guildID: BigString, options: EmojiOptions, reason?: string): Promise { + return await this.post(GUILD_EMOJIS(guildID), { + body: { + name: options.name, + roles: options.roles, + image: options.image, + }, + reason, + }) + } + + /** Create a guild based on a template. This can only be used with bots in less than 10 guilds */ + async createGuildFromTemplate(code: string, name: string, icon?: string): Promise { + return await this.post(GUILD_TEMPLATE(code), { body: { name, icon } }).then((guild) => new Guild(guild, this)) + } + + /** Create a guild sticker */ + async createGuildSticker(guildID: BigString, options: CreateStickerOptions, reason?: string): Promise { + return await this.post(GUILD_STICKERS(guildID), { + body: { + // @ts-expect-error some eris magic at play here + description: options.description ?? '', + name: options.name, + tags: options.tags, + }, + reason, + file: options.file, + }) + } + + /** Create a template for a guild */ + async createGuildTemplate(guildID: BigString, name: string, description?: string): Promise { + return await this.post(GUILD_TEMPLATES(guildID), { + body: { + name, + description, + }, + }).then((template) => new GuildTemplate(template, this)) + } + + /** + * Respond to the interaction with a message + * Note: Use webhooks if you have already responded with an interaction response. + */ + async createInteractionResponse( + interactionID: BigString, + interactionToken: string, + options: InteractionResponse, + file?: FileContent | FileContent[], + ): Promise { + return await this.post(INTERACTION_RESPOND(interactionID, interactionToken), { + body: { + ...options, + data: { + ...options.data, + + allowed_mentions: options.data?.allowedMentions ? this._formatAllowedMentions(options.data.allowedMentions) : undefined, + allowedMentions: undefined, + }, + }, + file, + }) + } + + /** + * Create a message in a channel + * Note: If you want to DM someone, the user ID is **not** the DM channel ID. use Client.getDMChannel() to get the DM channel for a user + */ + async createMessage(channelID: BigString, content: MessageContent, file?: FileContent | FileContent[]) { + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } else if (content.embed) { + if (!content.embeds) { + content.embeds = [] + } + content.embeds.push(content.embed) + } + } + + return await this.post(CHANNEL_MESSAGES(channelID), { + body: { + ...content, + allowed_mentions: this._formatAllowedMentions(content.allowedMentions), + sticker_ids: content.stickerIDs, + ...(content.messageReference?.messageID + ? { + message_reference: { + message_id: content.messageReference.messageID.toString(), + channel_id: content.messageReference.channelID?.toString(), + guild_id: content.messageReference.guildID?.toString(), + fail_if_not_exists: content.messageReference.failIfNotExists === true, + }, + } + : {}), + }, + file, + }).then((message) => new Message(message, this)) + } + + /** Create a guild role */ + async createRole(guildID: BigString, options: Role | RoleOptions, reason?: string) { + if (options.permissions !== undefined) { + options.permissions = options.permissions instanceof Permission ? String(options.permissions.allow) : String(options.permissions) + } + + return await this.post(GUILD_ROLES(guildID), { + body: { + name: options.name, + permissions: options.permissions, + color: options.color, + hoist: options.hoist, + icon: options.icon, + mentionable: options.mentionable, + unicode_emoji: options.unicodeEmoji, + }, + reason, + }).then((r) => { + const guild = this.guilds.get(guildID) + // @ts-expect-error some eris magic at play here + const role = new Role(r, guild) + + guild?.roles.set(role.id, role) + + return role + }) + } + + /** Create a stage instance */ + async createStageInstance(channelID: BigString, options: StageInstanceOptions): Promise { + return await this.post(STAGE_INSTANCES, { + body: { + channel_id: channelID, + privacy_level: options.privacyLevel, + topic: options.topic, + }, + }).then((instance) => new StageInstance(instance, this)) + } + + /** Create a thread with an existing message */ + async createThreadWithMessage( + channelID: BigString, + messageID: BigString, + options: CreateThreadOptions, + ): Promise { + return await this.post(THREAD_WITH_MESSAGE(channelID, messageID), { + body: { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + }, + }).then((channel) => Channel.from(channel, this)) + } + + /** Create a thread without an existing message */ + async createThreadWithoutMessage(channelID: BigString, options: CreateThreadWithoutMessageOptions): Promise { + return (await this.post(THREAD_WITHOUT_MESSAGE(channelID), { + body: { + auto_archive_duration: options.autoArchiveDuration, + invitable: options.invitable, + name: options.name, + type: options.type, + }, + }).then((channel) => Channel.from(channel, this))) as PrivateThreadChannel + } + + /** Crosspost (publish) a message to subscribed channels */ + async crosspostMessage(channelID: BigString, messageID: BigString): Promise { + return await this.post(CHANNEL_CROSSPOST(channelID, messageID)).then((message) => new Message(message, this)) + } + + /** Delete a guild channel, or leave a private or group channel */ + async deleteChannel(channelID: BigString, reason?: string): Promise { + return await this.delete(CHANNEL(channelID), { + reason, + }) + } + + /** Delete a channel permission overwrite */ + async deleteChannelPermission(channelID: BigString, overwriteID: BigString, reason?: string) { + return await this.delete(CHANNEL_PERMISSION(channelID, overwriteID), { + reason, + }) + } + + /** Delete a global application command */ + async deleteCommand(commandID: BigString): Promise { + return await this.delete(COMMAND(this.applicationId, commandID)) + } + + /** Delete a guild (bot user must be owner) */ + async deleteGuild(guildID: BigString): Promise { + return await this.delete(GUILD(guildID)) + } + + /** Delete a guild application command */ + async deleteGuildCommand(guildID: BigString, commandID: BigString): Promise { + return await this.delete(GUILD_COMMAND(this.applicationId, guildID, commandID)) + } + + /** Delete a guild discovery subcategory */ + async deleteGuildDiscoverySubcategory(guildID: BigString, categoryID: BigString, reason?: string) { + return await this.delete(GUILD_DISCOVERY_CATEGORY(guildID, categoryID), { + reason, + }) + } + + /** Delete a guild emoji object */ + async deleteGuildEmoji(guildID: BigString, emojiID: BigString, reason?: string): Promise { + return await this.delete(GUILD_EMOJI(guildID, emojiID), { + reason, + }) + } + + /** Delete a guild integration */ + async deleteGuildIntegration(guildID: BigString, integrationID: BigString): Promise { + return await this.delete(GUILD_INTEGRATION(guildID, integrationID)) + } + + /** Delete a guild sticker */ + async deleteGuildSticker(guildID: BigString, stickerID: BigString, reason?: string): Promise { + return await this.delete(GUILD_STICKER(guildID, stickerID), { + reason, + }) + } + + /** Delete a guild template */ + async deleteGuildTemplate(guildID: BigString, code: string): Promise { + return await this.delete(GUILD_TEMPLATE_GUILD(guildID, code)) + } + + /** Delete an invite */ + async deleteInvite(inviteID: string, reason?: string): Promise { + return await this.delete(INVITE(inviteID), { + reason, + }) + } + + /** Delete a message */ + async deleteMessage(channelID: BigString, messageID: BigString, reason?: string): Promise { + return await this.delete(CHANNEL_MESSAGE(channelID, messageID), { + reason, + }) + } + + /** Bulk delete messages (bot accounts only) */ + async deleteMessages(channelID: BigString, messageIDs: BigString[], reason?: string): Promise { + if (messageIDs.length === 0) { + return await Promise.resolve() + } + if (messageIDs.length === 1) { + return await this.deleteMessage(channelID, messageIDs[0], reason) + } + + const oldestAllowedSnowflake = (Date.now() - 1421280000000) * 4194304 + const invalidMessage = messageIDs.find((messageID) => this.snowflakeToTimestamp(messageID) < oldestAllowedSnowflake) + if (invalidMessage) { + return await Promise.reject(new Error(`Message ${invalidMessage} is more than 2 weeks old.`)) + } + + const chunks = this.chunkArray(messageIDs, 100) + for (const chunk of chunks) { + await this.post(CHANNEL_BULK_DELETE(channelID), { + body: { messages: chunk }, + reason, + }) + } + } + + /** Delete a guild role */ + async deleteRole(guildID: BigString, roleID: BigString, reason?: string): Promise { + return await this.delete(GUILD_ROLE(guildID, roleID), { + reason, + }) + } + + /** Delete a stage instance */ + async deleteStageInstance(channelID: BigString): Promise { + return await this.delete(STAGE_INSTANCE(channelID)) + } + + /** Delete a webhook */ + async deleteWebhook(webhookID: BigString, token?: string, reason?: string): Promise { + return await this.delete(token ? WEBHOOK_TOKEN(webhookID, token) : WEBHOOK(webhookID), { + reason, + }) + } + + /** Delete a webhook message */ + async deleteWebhookMessage(webhookID: BigString, token: string, messageID: BigString): Promise { + return await this.delete(WEBHOOK_MESSAGE(webhookID, token, messageID)) + } + + /** Edit a channel's properties */ + async editChannel(channelID: BigString, options: EditChannelOptions, reason?: string): Promise { + return await this.patch(CHANNEL(channelID), { + reason, + body: { + archived: options.archived, + auto_archive_duration: options.autoArchiveDuration, + bitrate: options.bitrate, + default_auto_archive_duration: options.defaultAutoArchiveDuration, + icon: options.icon, + invitable: options.invitable, + locked: options.locked, + name: options.name, + nsfw: options.nsfw, + owner_id: options.ownerID, + parent_id: options.parentID, + position: options.position, + rate_limit_per_user: options.rateLimitPerUser, + rtc_region: options.rtcRegion, + topic: options.topic, + user_limit: options.userLimit, + video_quality_mode: options.videoQualityMode, + permission_overwrites: options.permissionOverwrites, + }, + }).then((channel) => Channel.from(channel, this)) + } + + /** Create a channel permission overwrite */ + async editChannelPermission( + channelID: BigString, + overwriteID: BigString, + allow: bigint | number, + deny: bigint | number, + type: OverwriteTypes, + reason?: string, + ): Promise { + return await this.put(CHANNEL_PERMISSION(channelID, overwriteID), { + body: { + allow: allow.toString(), + deny: deny.toString(), + type, + }, + reason, + }) + } + + /** + * Edit a guild channel's position. Note that channel position numbers are grouped by type (category, text, voice), then sorted in ascending order (lowest number is on top). + */ + async editChannelPosition(channelID: BigString, position: number, options: EditChannelPositionOptions = {}): Promise { + const guild = this.guilds.find((g) => g.channels.has(channelID)) + const channels = guild?.channels + if (!channels) { + return await Promise.reject(new Error(`Channel ${channelID} not found`)) + } + + const channel = channels.get(channelID) + if (!channel) { + return await Promise.reject(new Error(`Channel ${channelID} not found`)) + } + if (channel.position === position) { + return await Promise.resolve() + } + const min = Math.min(position, channel.position) + const max = Math.max(position, channel.position) + + const positions = channels + .filter((chan) => { + return chan.type === channel.type && min <= chan.position && chan.position <= max && chan.id !== channelID + }) + .sort((a, b) => a.position - b.position) + + if (position > channel.position) { + positions.push(channel) + } else { + positions.unshift(channel) + } + + return await this.patch(GUILD_CHANNELS(guild.id), { + body: channels.array().map((channel, index) => ({ + id: channel.id, + position: index + min, + lock_permissions: options.lockPermissions, + parent_id: options.parentID, + })), + }) + } + + /** + * Edit multiple guild channels' positions. Note that channel position numbers are grouped by type (category, text, voice), then sorted in ascending order (lowest number is on top). + */ + async editChannelPositions(guildID: BigString, channelPositions: ChannelPosition[]): Promise { + return await this.patch(GUILD_CHANNELS(guildID), { + body: channelPositions.map((channelPosition) => { + return { + id: channelPosition.id, + position: channelPosition.position, + lock_permissions: channelPosition.lockPermissions, + parent_id: channelPosition.parentID, + } + }), + }) + } + + /** Edit a global application command */ + async editCommand(commandID: BigString, command: ApplicationCommandStructure) { + if (command.name !== undefined) { + if (command.type === 1 || command.type === undefined) { + command.name = command.name.toLowerCase() + if (!command.name.match(/^[\w-]{1,32}$/)) { + throw new Error('Slash Command names must match the regular expression "^[\\w-]{1,32}$"') + } + } + } + // @ts-expect-error some eris magic at play here + command.default_permission = command.defaultPermission + return await this.patch(COMMAND(this.applicationId, commandID), { + body: command, + }) + } + + /** Edits command permissions for a specific command in a guild. */ + async editCommandPermissions( + guildID: BigString, + commandID: BigString, + permissions: ApplicationCommandPermissions[], + ): Promise { + return await this.put(COMMAND_PERMISSIONS(this.applicationId, guildID, commandID), { + body: permissions, + }) + } + + /** Edit a guild */ + async editGuild(guildID: BigString, options: GuildOptions, reason?: string): Promise { + return await this.patch(GUILD(guildID), { + body: { + name: options.name, + icon: options.icon, + verification_level: options.verificationLevel, + default_message_notifications: options.defaultNotifications, + explicit_content_filter: options.explicitContentFilter, + system_channel_id: options.systemChannelID, + system_channel_flags: options.systemChannelFlags, + rules_channel_id: options.rulesChannelID, + public_updates_channel_id: options.publicUpdatesChannelID, + preferred_locale: options.preferredLocale, + afk_channel_id: options.afkChannelID, + afk_timeout: options.afkTimeout, + owner_id: options.ownerID, + splash: options.splash, + banner: options.banner, + description: options.description, + discovery_splash: options.discoverySplash, + features: options.features, + }, + reason, + }).then((guild) => new Guild(guild, this)) + } + + /** Edit a guild application command */ + async editGuildCommand(guildID: BigString, commandID: BigString, command: ApplicationCommandStructure): Promise { + if (command.name !== undefined) { + if (command.type === 1 || command.type === undefined) { + command.name = command.name.toLowerCase() + if (!command.name.match(/^[\w-]{1,32}$/)) { + throw new Error('Slash Command names must match the regular expression "^[\\w-]{1,32}$"') + } + } + } + // @ts-expect-error some eris magic at play here + command.default_permission = command.defaultPermission + + return await this.patch(GUILD_COMMAND(this.applicationId, guildID, commandID), { + body: command, + }) + } + + /** Edit a guild's discovery data */ + async editGuildDiscovery(guildID: BigString, options: DiscoveryOptions = {}): Promise { + return await this.patch(GUILD_DISCOVERY(guildID), { + body: { + primary_category_id: options.primaryCategoryID, + keywords: options.keywords, + emoji_discoverability_enabled: options.emojiDiscoverabilityEnabled, + }, + reason: options.reason, + }) + } + + /** Edit a guild emoji object */ + async editGuildEmoji(guildID: BigString, emojiID: BigString, options: { name?: string; roles?: string[] }, reason?: string): Promise { + return await this.patch(GUILD_EMOJI(guildID, emojiID), { + body: options, + reason, + }) + } + + /** Edit a guild integration */ + async editGuildIntegration(guildID: BigString, integrationID: BigString, options: IntegrationOptions): Promise { + return await this.patch(GUILD_INTEGRATION(guildID, integrationID), { + body: { + expire_behavior: options.expireBehavior, + expire_grace_period: options.expireGracePeriod, + enable_emoticons: options.enableEmoticons, + }, + }) + } + + /** Edit a guild member */ + async editGuildMember(guildID: BigString, memberID: BigString, options: MemberOptions, reason?: string): Promise { + return await this.patch(GUILD_MEMBER(guildID, memberID), { + body: { + roles: options.roles?.filter((roleID, index) => options.roles!.indexOf(roleID) === index), + nick: options.nick, + mute: options.mute, + deaf: options.deaf, + channel_id: options.channelID, + communication_disabled_until: options.communicationDisabledUntil, + }, + reason, + }) + // @ts-expect-error some eris magic at play here + .then((member) => new Member(member, this.guilds.get(guildID), this)) + } + + /** Edit a guild sticker */ + async editGuildSticker(guildID: BigString, stickerID: BigString, options?: EditStickerOptions, reason?: string): Promise { + return await this.patch(GUILD_STICKER(guildID, stickerID), { + body: { ...options }, + reason, + }) + } + + /** Edit a guild template */ + async editGuildTemplate(guildID: BigString, code: string, options: GuildTemplateOptions): Promise { + return await this.patch(GUILD_TEMPLATE_GUILD(guildID, code), { + body: { ...options }, + }).then((template) => new GuildTemplate(template, this)) + } + + /** Modify a guild's vanity code */ + async editGuildVanity(guildID: BigString, code: string | null) { + return await this.patch(GUILD_VANITY_URL(guildID), { + body: code, + }) + } + + /** Update a user's voice state - See [caveats](https://discord.com/developers/docs/resources/guild#modify-user-voice-state-caveats) */ + async editGuildVoiceState(guildID: BigString, options: VoiceStateOptions, userID: BigString = '@me'): Promise { + return await this.patch(GUILD_VOICE_STATE(guildID, userID), { + body: { + channel_id: options.channelID, + request_to_speak_timestamp: options.requestToSpeakTimestamp, + suppress: options.suppress, + }, + }) + } + + /** Edit a guild welcome screen */ + async editGuildWelcomeScreen(guildID: BigString, options: WelcomeScreenOptions): Promise { + return await this.patch(GUILD_WELCOME_SCREEN(guildID), { + body: { + description: options.description, + enabled: options.enabled, + welcome_channels: options.welcomeChannels.map((c) => { + return { + channel_id: c.channelID, + description: c.description, + emoji_id: c.emojiID, + emoji_name: c.emojiName, + } + }), + }, + }) + } + + /** Modify a guild's widget */ + async editGuildWidget(guildID: BigString, options: Widget): Promise { + return await this.patch(GUILD_WIDGET(guildID), { body: { ...options } }) + } + + /** Edit a message */ + async editMessage(channelID: BigString, messageID: BigString, content: MessageContentEdit): Promise { + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } else if (content.embed) { + if (!content.embeds) { + content.embeds = [] + } + content.embeds.push(content.embed) + } + } + + return await this.patch(CHANNEL_MESSAGE(channelID, messageID), { + body: { + ...content, + allowed_mentions: this._formatAllowedMentions(content.allowedMentions), + }, + file: content.file, + }).then((message) => new Message(message, this)) + } + + /** Edit a guild role */ + async editRole(guildID: BigString, roleID: BigString, options: RoleOptions, reason?: string): Promise { + // @ts-expect-error some eris magic at play here + options.unicode_emoji = options.unicodeEmoji + + if (options.permissions !== undefined) { + options.permissions = options.permissions instanceof Permission ? String(options.permissions.allow) : String(options.permissions) + } + return await this.patch(GUILD_ROLE(guildID, roleID), { + body: { ...options }, + reason, + }) + // @ts-expect-error some eris magic at play here + .then((role) => new Role(role, this.guilds.get(guildID))) + } + + /** Edit a guild role's position. Note that role position numbers are highest on top and lowest at the bottom. */ + async editRolePosition(guildID: BigString, roleID: BigString, position: number): Promise { + if (guildID === roleID) { + return await Promise.reject(new Error('Cannot move default role')) + } + // @ts-expect-error some eris magic at play here + const roles = this.guilds.get(guildID).roles + const role = roles.get(roleID) + if (!role) { + return await Promise.reject(new Error(`Role ${roleID} not found`)) + } + if (role.position === position) { + return await Promise.resolve() + } + const min = Math.min(position, role.position) + const max = Math.max(position, role.position) + const positions = roles + .array() + .filter((role) => min <= role.position && role.position <= max && role.id !== roleID) + .sort((a, b) => a.position - b.position) + if (position > role.position) { + positions.push(role) + } else { + positions.unshift(role) + } + return await this.patch(GUILD_ROLES(guildID), { + body: positions.map((role, index) => ({ + id: role.id, + position: index + min, + })), + }) + } + + /** Edit properties of the bot user */ + async editSelf(options: { avatar?: string; username?: string }): Promise { + return await this.patch(USER('@me'), { body: { ...options } }).then((data) => new ExtendedUser(data, this)) + } + + /** Update a stage instance */ + async editStageInstance(channelID: BigString, options: StageInstanceOptions): Promise { + return await this.patch(STAGE_INSTANCE(channelID), { + body: { ...options }, + }).then((instance) => new StageInstance(instance, this)) + } + + /** Edit a webhook */ + async editWebhook(webhookID: BigString, options: WebhookOptions, token: string, reason?: string) { + return await this.patch(token ? WEBHOOK_TOKEN(webhookID, token) : WEBHOOK(webhookID), { + body: { + name: options.name, + avatar: options.avatar, + channel_id: options.channelID, + }, + reason, + }) + } + + /** Edit a webhook message */ + async editWebhookMessage(webhookID: BigString, token: string, messageID: BigString, options: MessageWebhookContent): Promise { + const { file, allowedMentions, ...body } = options + + return await this.patch(WEBHOOK_MESSAGE(webhookID, token, messageID), { + body: { + ...body, + allowed_mentions: this._formatAllowedMentions(allowedMentions), + }, + file, + }).then((response) => new Message(response, this)) + } + + /** Execute a slack-style webhook */ + async executeSlackWebhook( + webhookID: BigString, + token: string, + options: Record & { auth?: boolean; threadID?: string }, + ): Promise + async executeSlackWebhook( + webhookID: BigString, + token: string, + options: Record & { + auth?: boolean + threadID?: string + wait: true + }, + ): Promise + async executeSlackWebhook( + webhookID: BigString, + token: string, + options: Record & { + auth?: boolean + threadID?: string + wait?: true + }, + ): Promise { + const { wait, threadID, ...rest } = options + let qs = '' + if (wait) { + qs += '&wait=true' + } + if (threadID) { + qs += '&thread_id=' + threadID + } + return await this.post(WEBHOOK_TOKEN_SLACK(webhookID, token) + (qs ? '?' + qs : ''), { body: { ...rest } }) + } + + /** Execute a webhook */ + async executeWebhook(webhookID: BigString, token: string, options: WebhookPayload): Promise + async executeWebhook(webhookID: BigString, token: string, options: WebhookPayload & { wait: true }): Promise + async executeWebhook(webhookID: BigString, token: string, options: WebhookPayload & { wait?: boolean }): Promise { + let qs = '' + if (options.wait) { + qs += '&wait=true' + } + if (options.threadID) { + qs += '&thread_id=' + options.threadID + } + if (options.embed) { + if (!options.embeds) { + options.embeds = [] + } + options.embeds.push(options.embed) + } + + return await this.post(WEBHOOK_TOKEN(webhookID, token) + (qs ? '?' + qs : ''), { + body: { + content: options.content, + embeds: options.embeds, + username: options.username, + avatar_url: options.avatarURL, + tts: options.tts, + flags: options.flags, + allowed_mentions: this._formatAllowedMentions(options.allowedMentions), + components: options.components, + }, + file: options.file, + }).then((response) => (options.wait ? new Message(response, this) : undefined)) + } + + /** Follow a NewsChannel in another channel. This creates a webhook in the target channel */ + async followChannel(channelID: BigString, webhookChannelID: BigString): Promise { + return await this.post(CHANNEL_FOLLOW(channelID), { + body: { webhook_channel_id: webhookChannelID }, + }) + } + + /** Get all active threads in a guild */ + async getActiveGuildThreads(guildID: BigString): Promise { + return await this.get(THREADS_GUILD_ACTIVE(guildID)).then((response) => { + return { + members: response.members.map((member: DiscordThreadMember) => new ThreadMember(member, this)), + threads: response.threads.map((thread: DiscordChannel) => Channel.from(thread, this)), + } + }) + } + + /** Get all archived threads in a channel */ + async getArchivedThreads( + channelID: BigString, + type: 'private', + options?: GetArchivedThreadsOptions, + ): Promise> + async getArchivedThreads( + channelID: BigString, + type: 'public', + options?: GetArchivedThreadsOptions, + ): Promise> + async getArchivedThreads(channelID: BigString, type: 'private' | 'public', options: GetArchivedThreadsOptions = {}): Promise { + let qs = '' + if (options.limit) { + qs += `&limit=${options.limit}` + } + if (options.before) { + qs += `&before=${options.before.toISOString()}` + } + + return await this.get(THREADS_ARCHIVED(channelID, type) + (qs ? '?' + qs : '')).then((response) => { + return { + hasMore: response.has_more, + members: response.members.map((member: DiscordThreadMember) => new ThreadMember(member, this)), + threads: response.threads.map((thread: DiscordChannel) => Channel.from(thread, this)), + } + }) + } + + /** Get general and bot-specific info on connecting to the Discord gateway (e.g. connection ratelimit) */ + async getBotGateway(): Promise { + return await this.get(GATEWAY_BOT) + } + + /** Get a Channel object from a channel ID */ + getChannel(channelID: BigString): AnyChannel | undefined { + const id = channelID.toString() + + const guildID = this._channelGuildMap.get(channelID) ?? this._threadGuildMap.get(channelID) + + if (guildID) { + const guild = this.guilds.get(guildID) + if (guild) return guild.channels.get(channelID) + } + + return this.privateChannels.get(id)! + } + + /** Get all invites in a channel */ + async getChannelInvites(channelID: BigString): Promise { + return await this.get(CHANNEL_INVITES(channelID)).then((invites) => invites.map((invite: DiscordInvite) => new Invite(invite, this))) + } + + /** Get all the webhooks in a channel */ + async getChannelWebhooks(channelID: BigString): Promise { + return await this.get(CHANNEL_WEBHOOKS(channelID)) + } + + /** Get a global application command */ + async getCommand(commandID: BigString): Promise { + return await this.get(COMMAND(this.applicationId, commandID)) + } + + /** Get the a guild's application command permissions */ + async getCommandPermissions(guildID: BigString, commandID: BigString): Promise { + return await this.get(COMMAND_PERMISSIONS(this.applicationId, guildID, commandID)) + } + + /** Get the global application commands */ + async getCommands(): Promise { + return await this.get(COMMANDS(this.applicationId)) + } + + /** Get a list of discovery categories */ + async getDiscoveryCategories(): Promise { + return await this.get(DISCOVERY_CATEGORIES) + } + + /** Get a DM channel with a user, or create one if it does not exist */ + async getDMChannel(userID: BigString): Promise { + if (this._privateChannelMap.has(userID)) { + return await Promise.resolve(this.privateChannels.get(this._privateChannelMap.get(userID)!)!) + } + return await this.post(USER_CHANNELS('@me'), { + body: { + recipients: [userID], + type: 1, + }, + }).then((privateChannel) => new PrivateChannel(privateChannel, this)) + } + + /** Get a guild from the guild's emoji ID */ + async getEmojiGuild(emojiID: BigString): Promise { + return await this.get(CUSTOM_EMOJI_GUILD(emojiID)).then((result) => new Guild(result, this)) + } + + /** Get info on connecting to the Discord gateway */ + async getGateway(): Promise<{ url: string }> { + return await this.get(GATEWAY) + } + + /** Get the audit log for a guild */ + async getGuildAuditLog(guildID: BigString, options: GetGuildAuditLogOptions = {}): Promise { + let qs = '' + if (options.actionType) { + qs += `&action_type=${options.actionType}` + } + if (options.userID) { + qs += `&user_id=${options.userID}` + } + if (options.before) { + qs += `&before=${options.before}` + } + if (options.limit) { + qs += `&limit=${options.limit}` + } + + return await this.get(GUILD_AUDIT_LOGS(guildID) + (qs ? '?' + qs : '')).then((data) => { + const guild = this.guilds.get(guildID) + const users = data.users.map((u: DiscordUser) => { + const user = new User(u, this) + this.users.set(user.id, user) + return user + }) + + const threads = data.threads.map((thread: DiscordChannel) => { + const channel = Channel.from(thread, this) + guild?.threads.set(channel.id, channel) + return channel + }) + + return { + entries: data.audit_log_entries.map((entry: DiscordAuditLogEntry) => new GuildAuditLogEntry(entry, guild)), + integrations: data.integrations.map((integration: DiscordIntegration) => new GuildIntegration(integration, guild)), + threads, + users, + webhooks: data.webhooks, + } + }) + } + + /** Get a ban from the ban list of guild */ + async getGuildBan(guildID: BigString, userID: BigString): Promise { + return await this.get(GUILD_BAN(guildID, userID)).then((ban) => { + ban.user = new User(ban.user, this) + return ban + }) + } + + /** Get the ban list of a guild */ + async getGuildBans(guildID: BigString, options: GetGuildBansOptions = {}): Promise { + let qs = '' + if (options.after) { + qs += `&after=${options.after}` + } + if (options.before) { + qs += `&before=${options.before}` + } + if (options.limit) { + qs += `&limit=${options.limit && Math.min(options.limit, 1000)}` + } + + const bans = await this.get(GUILD_BANS(guildID) + (qs ? '?' + qs : '')) + + for (const ban of bans) { + const user = new User(ban.user, this) + this.users.set(user.id, user) + ban.user = user + } + + if (options.limit && options.limit > 1000 && bans.length >= 1000) { + const page = await this.getGuildBans(guildID, { + after: options.before ? undefined : bans[bans.length - 1].user.id, + before: options.before ? bans[0].user.id : undefined, + limit: options.limit - bans.length, + }) + + if (options.before) { + bans.unshift(...page) + } else { + bans.push(...page) + } + } + + return bans + } + + /** Get a guild application command */ + async getGuildCommand(guildID: BigString, commandID: BigString): Promise { + return await this.get(GUILD_COMMAND(this.applicationId, guildID, commandID)) + } + + /** Get the all of a guild's application command permissions */ + async getGuildCommandPermissions(guildID: BigString): Promise { + return await this.get(GUILD_COMMAND_PERMISSIONS(this.applicationId, guildID)) + } + + /** Get a guild's application commands */ + async getGuildCommands(guildID: BigString): Promise { + return await this.get(GUILD_COMMANDS(this.applicationId, guildID)) + } + + /** Get a guild's discovery object */ + async getGuildDiscovery(guildID: BigString): Promise { + return await this.get(GUILD_DISCOVERY(guildID)) + } + + /** Get a list of integrations for a guild */ + async getGuildIntegrations(guildID: BigString): Promise { + const guild = this.guilds.get(guildID) + return await this.get(GUILD_INTEGRATIONS(guildID)).then((integrations) => + integrations.map((integration: DiscordIntegration) => new GuildIntegration(integration, guild)), + ) + } + + /** Get all invites in a guild */ + async getGuildInvites(guildID: BigString): Promise { + return await this.get(GUILD_INVITES(guildID)).then((invites) => invites.map((invite: DiscordInvite) => new Invite(invite, this))) + } + + /** Get a guild preview for a guild. Only available for community guilds. */ + async getGuildPreview(guildID: BigString): Promise { + return await this.get(GUILD_PREVIEW(guildID)).then((data) => new GuildPreview(data, this)) + } + + /** Get a guild template */ + async getGuildTemplate(code: string): Promise { + return await this.get(GUILD_TEMPLATE(code)).then((template) => new GuildTemplate(template, this)) + } + + /** Get a guild's templates */ + async getGuildTemplates(guildID: BigString): Promise { + return await this.get(GUILD_TEMPLATES(guildID)).then((templates) => templates.map((t: DiscordTemplate) => new GuildTemplate(t, this))) + } + + /** Returns the vanity url of the guild */ + async getGuildVanity(guildID: BigString): Promise { + return await this.get(GUILD_VANITY_URL(guildID)) + } + + /** Get all the webhooks in a guild */ + async getGuildWebhooks(guildID: BigString): Promise { + return await this.get(GUILD_WEBHOOKS(guildID)) + } + + /** Get the welcome screen of a Community guild, shown to new members */ + async getGuildWelcomeScreen(guildID: BigString): Promise { + return await this.get(GUILD_WELCOME_SCREEN(guildID)) + } + + /** Get a guild's widget object */ + async getGuildWidget(guildID: BigString): Promise { + return await this.get(GUILD_WIDGET(guildID)) + } + + /** Get a guild's widget settings object. Requires MANAGE_GUILD permission */ + async getGuildWidgetSettings(guildID: BigString): Promise { + return await this.get(GUILD_WIDGET_SETTINGS(guildID)) + } + + /** Get info on an invite */ + async getInvite(inviteID: string, withCounts?: boolean): Promise { + let qs = '' + if (withCounts) { + qs += '&with_counts=true' + } + + return await this.get(INVITE(inviteID) + (qs ? '?' + qs : '')).then((invite) => new Invite(invite, this)) + } + + /** Get joined private archived threads in a channel */ + async getJoinedPrivateArchivedThreads( + channelID: BigString, + options: GetArchivedThreadsOptions = {}, + ): Promise> { + let qs = '' + if (options.before) { + qs += `&before=${options.before.toISOString()}` + } + + if (options.limit) { + qs += `&limit=${options.limit}` + } + + return await this.get(THREADS_ARCHIVED_JOINED(channelID) + (qs ? '?' + qs : '')).then((response) => { + return { + hasMore: response.has_more, + members: response.members.map((member: DiscordThreadMember) => new ThreadMember(member, this)), + threads: response.threads.map((thread: DiscordChannel) => Channel.from(thread, this)), + } + }) + } + + /** Get a previous message in a channel */ + async getMessage(channelID: BigString, messageID: BigString): Promise { + return await this.get(CHANNEL_MESSAGE(channelID, messageID)).then((message) => new Message(message, this)) + } + + /** Get a list of users who reacted with a specific reaction */ + async getMessageReaction(channelID: BigString, messageID: BigString, reaction: string, options: GetMessageReactionOptions = {}): Promise { + if (reaction === decodeURI(reaction)) { + reaction = encodeURIComponent(reaction) + } + if (!options || typeof options !== 'object') { + options = { + limit: options, + } + } + + let qs = '' + if (options.limit) { + qs += `&limit=${options.limit}` + } + if (options.after) { + qs += `&after=${options.after}` + } + + return await this.get(CHANNEL_MESSAGE_REACTION(channelID, messageID, reaction) + (qs ? '?' + qs : '')).then((users) => + users.map((user: DiscordUser) => new User(user, this)), + ) + } + + /** Get previous messages in a channel */ + async getMessages(channelID: BigString, options: GetMessagesOptions = {}): Promise { + if (!options || typeof options !== 'object') { + options = { + limit: options, + } + } + if (options.limit === undefined) { + // Legacy behavior + options.limit = 50 + } + + let limit = options.limit + if (limit && limit > 100) { + let logs: Message[] = [] + const get: (_before?: BigString, _after?: BigString) => Promise = async (_before?: BigString, _after?: BigString) => { + let qs = '' + qs += `&limit=${100}` + if (_before) qs += `&before=${_before}` + if (_after) qs += `&after=${_after}` + + const messages = await this.get(CHANNEL_MESSAGES(channelID) + (qs ? '?' + qs : '')) + if (limit <= messages.length) { + return _after + ? messages + .slice(messages.length - limit, messages.length) + .map((message: DiscordMessage) => new Message(message, this)) + .concat(logs) + : logs.concat(messages.slice(0, limit).map((message: DiscordMessage) => new Message(message, this))) + } + + limit -= messages.length + logs = _after + ? messages.map((message: DiscordMessage) => new Message(message, this)).concat(logs) + : logs.concat(messages.map((message: DiscordMessage) => new Message(message, this))) + if (messages.length < 100) { + return logs + } + + this.emit('debug', `Getting ${limit} more messages during getMessages for ${channelID}: ${_before} ${_after}`, -1) + + return await get((_before ?? !_after) && messages[messages.length - 1].id, _after && messages[0].id) + } + return await get(options.before, options.after) + } + + const messages = await this.get(CHANNEL_MESSAGES(channelID)) + return messages.map((message: DiscordMessage) => { + try { + return new Message(message, this) + } catch (err: any) { + this.emit('error', `Error creating message from channel messages\n${err.stack}\n${JSON.stringify(messages)}`) + return null + } + }) + } + + /** Get the list of sticker packs available to Nitro subscribers */ + async getNitroStickerPacks(): Promise<{ sticker_packs: StickerPack[] }> { + return await this.get(STICKER_PACKS) + } + + /** Get data on an OAuth2 application */ + async getOAuthApplication(appID: BigString): Promise { + return await this.get(OAUTH2_APPLICATION(appID || '@me')) + } + + /** Get all the pins in a channel */ + async getPins(channelID: BigString): Promise { + return await this.get(CHANNEL_PINS(channelID)).then((messages) => messages.map((message: DiscordMessage) => new Message(message, this))) + } + + /** Get the prune count for a guild */ + async getPruneCount(guildID: BigString, options: GetPruneOptions = {}): Promise { + let qs = '' + if (options.days) { + qs += `&days=${options.days}` + } + // TODO: how to put array in query string + if (options.includeRoles) { + qs += `&include_roles=${options.includeRoles}` + } + + return await this.get(GUILD_PRUNE(guildID) + (qs ? '?' + qs : '')).then((data) => data.pruned) + } + + /** Get a channel's data via the REST API. */ + async getRESTChannel(channelID: BigString): Promise { + return await this.get(CHANNEL(channelID)).then((channel: DiscordChannel) => Channel.from(channel, this)) + } + + /** Get a guild's data via the REST API. */ + async getRESTGuild(guildID: BigString, withCounts = false): Promise { + let qs = '' + if (withCounts) { + qs += `&with_conts=${withCounts}` + } + + return await this.get(GUILD(guildID) + (qs ? '?' + qs : '')).then((guild) => new Guild(guild, this)) + } + + /** Get a guild's channels via the REST API. */ + async getRESTGuildChannels(guildID: BigString): Promise { + return await this.get(GUILD_CHANNELS(guildID)).then((channels) => channels.map((channel: DiscordChannel) => Channel.from(channel, this))) + } + + /** Get a guild emoji via the REST API. */ + async getRESTGuildEmoji(guildID: BigString, emojiID: BigString): Promise { + return await this.get(GUILD_EMOJI(guildID, emojiID)) + } + + /** Get a guild's emojis via the REST API. */ + async getRESTGuildEmojis(guildID: BigString): Promise { + return await this.get(GUILD_EMOJIS(guildID)) + } + + /** Get a guild's members via the REST API. */ + async getRESTGuildMember(guildID: BigString, memberID: BigString): Promise { + return await this.get(GUILD_MEMBER(guildID, memberID)).then( + (member: DiscordMemberWithUser) => new Member(member, this.guilds.get(guildID)!, this), + ) + } + + /** Get a guild's members via the REST API. */ + async getRESTGuildMembers(guildID: BigString, options: GetRESTGuildMembersOptions = {}): Promise { + if (!options || typeof options !== 'object') { + options = { + limit: options, + } + } + let qs = '' + + if (options.limit) { + qs += `&limit=${options.limit}` + } + if (options.after) { + qs += `&after=${options.after}` + } + + return await this.get(GUILD_MEMBERS(guildID) + (qs ? '?' + qs : '')).then((members) => + members.map((member: DiscordMemberWithUser) => new Member(member, this.guilds.get(guildID)!, this)), + ) + } + + /** Get a guild's roles via the REST API. */ + async getRESTGuildRoles(guildID: BigString): Promise { + return await this.get(GUILD_ROLES(guildID)).then((roles) => roles.map((role: DiscordRole) => new Role(role, this.guilds.get(guildID)!))) + } + + /** Get a list of the user's guilds via the REST API. */ + async getRESTGuilds(options: GetRESTGuildsOptions = {}) { + if (!options || typeof options !== 'object') { + options = { + limit: options, + } + } + let qs = '' + if (options.after) { + qs += `&after=${options.after}` + } + if (options.before) { + qs += `&before=${options.before}` + } + if (options.limit) { + qs += `&limit=${options.limit}` + } + + return await this.get(USER_GUILDS('@me') + (qs ? '?' + qs : '')).then((guilds) => guilds.map((guild: DiscordGuild) => new Guild(guild, this))) + } + + /** Get a guild sticker via the REST API. */ + async getRESTGuildSticker(guildID: BigString, stickerID: BigString): Promise { + return await this.get(GUILD_STICKER(guildID, stickerID)) + } + + /** Get a guild's stickers via the REST API. */ + async getRESTGuildStickers(guildID: BigString): Promise { + return await this.get(GUILD_STICKERS(guildID)) + } + + /** Get a sticker via the REST API. */ + async getRESTSticker(stickerID: BigString): Promise { + return await this.get(STICKER(stickerID)) + } + + /** Get a user's data via the REST API. */ + async getRESTUser(userID: BigString): Promise { + return await this.get(USER(userID)).then((user) => new User(user, this)) + } + + /** Get properties of the bot user */ + async getSelf(): Promise { + return await this.get(USER('@me')).then((data) => new ExtendedUser(data, this)) + } + + /** Get the stage instance associated with a stage channel */ + async getStageInstance(channelID: BigString): Promise { + return await this.get(STAGE_INSTANCE(channelID)).then((instance) => new StageInstance(instance, this)) + } + + /** Get a list of members that are part of a thread channel */ + async getThreadMembers(channelID: BigString): Promise { + return await this.get(THREAD_MEMBERS(channelID)).then((members) => members.map((member: DiscordThreadMember) => new ThreadMember(member, this))) + } + + /** Get a list of general/guild-specific voice regions */ + async getVoiceRegions(guildID: BigString): Promise { + return guildID ? await this.get(GUILD_VOICE_REGIONS(guildID)) : await this.get(VOICE_REGIONS) + } + + /** Get a webhook */ + async getWebhook(webhookID: BigString, token: string): Promise { + return await this.get(token ? WEBHOOK_TOKEN(webhookID, token) : WEBHOOK(webhookID)) + } + + /** Get a webhook message */ + async getWebhookMessage(webhookID: BigString, token: string, messageID: BigString): Promise { + return await this.get(WEBHOOK_MESSAGE(webhookID, token, messageID)).then((message) => new Message(message, this)) + } + + /** Join a thread */ + async joinThread(channelID: BigString, userID: BigString = '@me'): Promise { + return await this.put(THREAD_MEMBER(channelID, userID)) + } + + /** Kick a user from a guild */ + async kickGuildMember(guildID: BigString, userID: BigString, reason?: string): Promise { + return await this.delete(GUILD_MEMBER(guildID, userID), { + reason, + }) + } + + /** Leave a guild */ + async leaveGuild(guildID: BigString): Promise { + return await this.delete(USER_GUILD('@me', guildID)) + } + + /** Leave a thread */ + async leaveThread(channelID: BigString, userID: BigString = '@me'): Promise { + return await this.delete(THREAD_MEMBER(channelID, userID)) + } + + /** Pin a message */ + async pinMessage(channelID: BigString, messageID: BigString): Promise { + return await this.put(CHANNEL_PIN(channelID, messageID)) + } + + /** Begin pruning a guild */ + async pruneMembers(guildID: BigString, options: PruneMemberOptions = {}): Promise { + return await this.post(GUILD_PRUNE(guildID), { + body: { + days: options.days, + compute_prune_count: options.computePruneCount, + include_roles: options.includeRoles, + }, + reason: options.reason, + }).then((data) => data.pruned) + } + + /** Purge previous messages in a channel with an optional filter (bot accounts only) */ + async purgeChannel(channelID: BigString, options: PurgeChannelOptions): Promise { + let limit = options.limit + if (limit !== -1 && limit <= 0) { + return 0 + } + const toDelete: BigString[] = [] + let deleted = 0 + let done = false + const checkToDelete: () => Promise = async () => { + const messageIDs = (done && toDelete) || (toDelete.length >= 100 && toDelete.splice(0, 100)) + if (messageIDs) { + deleted += messageIDs.length + await this.deleteMessages(channelID, messageIDs, options.reason) + if (done) { + return deleted + } + await delay(1000) + return await checkToDelete() + } else if (done) { + return deleted + } else { + await delay(250) + return await checkToDelete() + } + } + const del = async (_before?: BigString, _after?: BigString) => { + const messages = await this.getMessages(channelID, { + limit: 100, + before: _before?.toString(), + after: _after?.toString(), + }) + if (limit !== -1 && limit <= 0) { + done = true + return + } + for (const message of messages) { + if (limit !== -1 && limit <= 0) { + break + } + if (message.timestamp < Date.now() - 1209600000) { + // 14d * 24h * 60m * 60s * 1000ms + done = true + return + } + if (!options.filter || options.filter(message)) { + toDelete.push(message.id) + } + if (limit !== -1) { + limit-- + } + } + if ((limit !== -1 && limit <= 0) || messages.length < 100) { + done = true + return + } + await del(_before ?? !_after ? messages[messages.length - 1].id : undefined, _after ? messages[0].id : undefined) + } + await del(options.before, options.after) + return await checkToDelete() + } + + /** Remove a role from a guild member */ + async removeGuildMemberRole(guildID: BigString, memberID: BigString, roleID: BigString, reason?: string): Promise { + return await this.delete(GUILD_MEMBER_ROLE(guildID, memberID, roleID), { + reason, + }) + } + + /** Remove a reaction from a message */ + async removeMessageReaction(channelID: BigString, messageID: BigString, reaction: string, userID?: BigString): Promise { + if (reaction === decodeURI(reaction)) { + reaction = encodeURIComponent(reaction) + } + return await this.delete(CHANNEL_MESSAGE_REACTION_USER(channelID, messageID, reaction, userID ?? '@me')) + } + + /** Remove all reactions from a message for a single emoji. */ + async removeMessageReactionEmoji(channelID: BigString, messageID: BigString, reaction: string): Promise { + if (reaction === decodeURI(reaction)) { + reaction = encodeURIComponent(reaction) + } + return await this.delete(CHANNEL_MESSAGE_REACTION(channelID, messageID, reaction)) + } + + /** Remove all reactions from a message */ + async removeMessageReactions(channelID: BigString, messageID: BigString): Promise { + return await this.delete(CHANNEL_MESSAGE_REACTIONS(channelID, messageID)) + } + + /** Search for guild members by partial nickname/username */ + async searchGuildMembers(guildID: BigString, query: string, limit?: number): Promise { + let qs = `?query=${query}` + if (limit) { + qs += `?limit=${limit}` + } + + return await this.get(GUILD_MEMBERS_SEARCH(guildID) + qs).then((members) => { + const guild = this.guilds.get(guildID) + return members.map((member: DiscordMemberWithUser) => new Member(member, guild, this)) + }) + } + + /** Send typing status in a channel */ + async sendChannelTyping(channelID: BigString): Promise { + return await this.post(CHANNEL_TYPING(channelID)) + } + + /** Force a guild integration to sync */ + async syncGuildIntegration(guildID: BigString, integrationID: BigString): Promise { + return await this.post(GUILD_INTEGRATION_SYNC(guildID, integrationID)) + } + + /** Force a guild template to sync */ + async syncGuildTemplate(guildID: BigString, code: string): Promise { + return await this.put(GUILD_TEMPLATE_GUILD(guildID, code)).then((template) => new GuildTemplate(template, this)) + } + + /** Unban a user from a guild */ + async unbanGuildMember(guildID: BigString, userID: BigString, reason?: string): Promise { + return await this.delete(GUILD_BAN(guildID, userID), { + reason, + }) + } + + /** Unpin a message */ + async unpinMessage(channelID: BigString, messageID: BigString): Promise { + return await this.delete(CHANNEL_PIN(channelID, messageID)) + } + + /** Validate discovery search term */ + async validateDiscoverySearchTerm(term: string): Promise<{ valid: boolean }> { + return await this.get(DISCOVERY_VALIDATION + `?term=${encodeURI(term)}`) + } + + /** Converts the easy to type allowed mentions to the format discord requires. */ + _formatAllowedMentions(allowed?: AllowedMentions): DiscordAllowedMentions { + if (!allowed) { + return this.options.allowedMentions + } + const result: DiscordAllowedMentions = {} + result.parse = [] + + if (allowed.everyone) { + result.parse.push(AllowedMentionsTypes.EveryoneMentions) + } + if (allowed.roles === true) { + result.parse.push(AllowedMentionsTypes.RoleMentions) + } else if (Array.isArray(allowed.roles)) { + if (allowed.roles.length > 100) { + throw new Error('Allowed role mentions cannot exceed 100.') + } + result.roles = allowed.roles + } + if (allowed.users === true) { + result.parse.push(AllowedMentionsTypes.UserMentions) + } else if (Array.isArray(allowed.users)) { + if (allowed.users.length > 100) { + throw new Error('Allowed user mentions cannot exceed 100.') + } + result.users = allowed.users + } + if (allowed.repliedUser !== undefined) { + result.replied_user = allowed.repliedUser + } + return result + } + + _formatImage(url: string, format?: ImageFormat, size?: ImageSize): string { + if (!format) { + format = url.includes('/a_') ? 'gif' : this.options.defaultImageFormat + } + + if (!size) { + size = this.options.defaultImageSize + } + return `${this.CDN_URL}${url}.${format}?size=${size}` + } + + /** Converts a snowflake(discord id) into a timestamp. */ + snowflakeToTimestamp(snowflake: BigString): number { + return Number(BigInt(snowflake) / 4194304n + 1420070400000n) + } + + /** Convert a icon hash into a bigint. */ + iconHashToBigInt(hash: string): bigint { + return iconHashToBigInt(hash) + } + + /** Convert a icon bigint back into a hash. */ + iconBigintToHash(icon: bigint): string { + return iconBigintToHash(icon) + } + + /** Splits a large array into chunks of smaller arrays */ + chunkArray(array: T[], size = 100): T[][] { + const box: T[][] = [] + while (array.length > box.length) { + box.push(array.splice(0, 100)) + } + + return box + } + + toString() { + return `[Client ${this.id}]` + } + + toJSON(props: string[] = []): Record { + // TODO: Update this after Client is done + return Base.prototype.toJSON.call(this, [ + 'application', + 'bot', + 'channelGuildMap', + 'gatewayURL', + 'groupChannels', + 'guilds', + 'guildShardMap', + 'lastConnect', + 'lastReconnectDelay', + 'notes', + 'options', + 'presence', + 'privateChannelMap', + 'privateChannels', + 'ready', + 'reconnectAttempts', + 'relationships', + 'requestHandler', + 'shards', + 'startTime', + 'unavailableGuilds', + 'userGuildSettings', + 'users', + 'userSettings', + 'voiceConnections', + ...props, + ]) + } + + // Typescript is not so good as we developers so we need this little utility function to help it out + // Taken from https://fettblog.eu/typescript-hasownproperty/ + /** TS save way to check if a property exists in an object */ + hasProperty(obj: T, prop: Y): obj is T & Record { + return obj.hasOwnProperty(prop) + } + + isDiscordMemberWithUser(member: DiscordMember | DiscordMemberWithUser): member is DiscordMemberWithUser { + return this.hasProperty(member, 'user') + } +} + +export default Client + +export interface ClientOptions { + /** The default allowed mentions you would like to use. */ + allowedMentions?: AllowedMentions + /** The default image format you would like to use. */ + defaultImageFormat?: ImageFormat + /** The default image size you would like to use. */ + defaultImageSize?: ImageSize + /** The message limit you would like to set. */ + messageLimit?: number + /** The api version you would like to use. */ + apiVersion: ApiVersions + /** The url to the REST proxy to send requests to. This url should nly include the initial domain:port portion until api/v.... */ + proxyURL: string + /** The password/authorization to confirm that these request made to your rest proxy are indeed from you and not a hacker. */ + proxyRestAuthorization: string + /** The application id(NOT the bot id). The bot id and application id are the same for newer bots but older bots have different ids. */ + applicationId: BigString + /** Whether or not to seed voice connections. */ + seedVoiceConnections: boolean + /** The concurrency to use when starting the bot. */ + shardConcurrency?: 'auto' | number + /** How many shards to use max. */ + maxShards?: 'auto' | number + /** Whether or not to enable websocket compression. NOT REcOMMENDED. */ + compress?: boolean + /** The last shard id to use. */ + lastShardID?: number + /** How many times to attempt resuming. */ + maxResumeAttempts?: number + /** The intents to use when connection to gateway. */ + intents?: GatewayIntents +} + +export interface ParsedClientOptions { + /** The discord api version to use. */ + apiVersion: ApiVersions + /** Allowed mentions */ + allowedMentions: DiscordAllowedMentions + /** The image format to use by default. */ + defaultImageFormat: ImageFormat + /** The image size to use by default. */ + defaultImageSize: ImageSize + /** The url to the REST proxy to send requests to. This url should nly include the initial domain:port portion until api/v.... */ + proxyURL?: string + /** The password/authorization to confirm that these request made to your rest proxy are indeed from you and not a hacker. */ + proxyRestAuthorization?: string + /** The application id(NOT the bot id). The bot id and application id are the same for newer bots but older bots have different ids. */ + applicationId: BigString + /** The message limit you would like to set. */ + messageLimit?: number + /** Whether or not to seed voice connections */ + seedVoiceConnections: boolean + /** The max concurrency for the bot */ + shardConcurrency: 'auto' | number + /** How many shards to use as max */ + maxShards: 'auto' | number + /** Whether or not to enable websocket compression. NOT REcOMMENDED. */ + compress: boolean + /** The last shard id to use. */ + lastShardID?: number + /** How many times to attempt resuming. */ + maxResumeAttempts: number + /** The intents to use when connection to gateway. */ + intents: GatewayIntents +} + +// TODO: Switch bigstring to dd version in next dd release. +/** A union type of string or bigint to help make it easier for users to switch between one another. */ +export type BigString = bigint | string +/** The API versions that are supported. */ +export type ApiVersions = 10 +/** The sizes for images that are supported. */ +export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 +/** The formats for images that are supported. */ +export type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'gif' +/** The methods that are acceptable for REST. */ +export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + +export interface RequestData { + /** The method which should be used to send this request. */ + method: RequestMethod + /** The url to send this request to. */ + url: string + /** The headers you can send which will override internal headers or add others ones. */ + headers?: Record + /** The reason to add to the audit logs for this request. */ + reason?: string + /** The payload this request should send. */ + body?: Record | string | null | any[] + /** The file contents that should be sent in this request. */ + file?: FileContent | FileContent[] +} diff --git a/packages/client/src/Collection.ts b/packages/client/src/Collection.ts new file mode 100644 index 000000000..25221485b --- /dev/null +++ b/packages/client/src/Collection.ts @@ -0,0 +1,140 @@ +export class Collection extends Map { + limit: number | undefined; + + set(key: K, value: V): this { + // When this collection is limitd make sure we can add first + if ((this.limit ?? this.limit === 0) && this.size >= this.limit) { + return this; + } + + return super.set(key, value); + } + + forceSet(key: K, value: V): this { + return super.set(key, value); + } + + array(): V[] { + return [...this.values()]; + } + + /** Retrieve the value of the first element in this collection */ + first(): V | undefined { + return this.values().next().value; + } + + last(): V | undefined { + return [...this.values()][this.size - 1]; + } + + random(): V | undefined { + const array = [...this.values()]; + return array[Math.floor(Math.random() * array.length)]; + } + + find(callback: (value: V, key: K) => boolean): V | undefined { + for (const key of this.keys()) { + const value = this.get(key)!; + if (callback(value, key)) return value; + } + } + + filter(callback: (value: V, key: K) => boolean, returnArray?: true): V[]; + filter(callback: (value: V, key: K) => boolean, returnArray: false): Collection; + filter(callback: (value: V, key: K) => boolean, returnArray = true): Collection | V[] { + const relevant = new Collection(); + this.forEach((value, key) => { + if (callback(value, key)) relevant.set(key, value); + }); + + return returnArray ? relevant.array() : relevant; + } + + map(callback: (value: V, key: K) => T): T[] { + const results = []; + for (const key of this.keys()) { + const value = this.get(key)!; + results.push(callback(value, key)); + } + return results; + } + + some(callback: (value: V, key: K) => boolean): boolean { + for (const key of this.keys()) { + const value = this.get(key)!; + if (callback(value, key)) return true; + } + + return false; + } + + every(callback: (value: V, key: K) => boolean): boolean { + for (const key of this.keys()) { + const value = this.get(key)!; + if (!callback(value, key)) return false; + } + + return true; + } + + reduce(callback: (accumulator: T, value: V, key: K) => T, initialValue?: T): T { + let accumulator: T = initialValue!; + + for (const key of this.keys()) { + const value = this.get(key)!; + accumulator = callback(accumulator, value, key); + } + + return accumulator; + } + + /** + * Adds a object to the collection. + * @deprecated Recommend using Collection.set(). Keeping for the sake of Eris API. + * @deprecated extra parameter. No longer used, keeping for sake of Eris API. + */ + add(obj: V & { id: K }, extra?: unknown, replace?: boolean): V { + if (this.limit === 0) return obj; + + const existing = this.get(obj.id); + if (existing && !replace) { + return existing; + } + + this.set(obj.id, obj); + return obj; + } + + remove(obj: { id: K }): V | undefined { + const item = this.get(obj.id); + if (!item) return; + + this.delete(obj.id); + return item; + } + + update(obj: V & { id: K }, extra?: unknown, replace?: boolean): V { + const item = this.get(obj.id); + if (!item) { + this.set(obj.id, obj); + return obj; + } + + // @ts-expect-error some eris magic at play here + item.update?.(obj, extra); + return item; + } + + toRecord(): Record { + const record: Record = {}; + for (const [key, value] of this.entries()) { + // @ts-expect-error should work fine + const finalKey = typeof key === 'string' ? key : key.toString(); + record[finalKey] = value; + } + + return record; + } + } + + export default Collection; \ No newline at end of file diff --git a/packages/client/src/Endpoints.ts b/packages/client/src/Endpoints.ts new file mode 100644 index 000000000..62a49a4e6 --- /dev/null +++ b/packages/client/src/Endpoints.ts @@ -0,0 +1,136 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { BigString } from './Client.js' + +export const ORIGINAL_INTERACTION_RESPONSE = (appID: BigString, interactToken: string) => `/webhooks/${appID}/${interactToken}` +export const COMMAND = (applicationID: BigString, commandID: BigString) => `/applications/${applicationID}/commands/${commandID}` +export const COMMANDS = (applicationID: BigString) => `/applications/${applicationID}/commands` +export const COMMAND_PERMISSIONS = (applicationID: BigString, guildID: BigString, commandID: BigString) => + `/applications/${applicationID}/guilds/${guildID}/commands/${commandID}/permissions` +export const CHANNEL = (chanID: BigString) => `/channels/${chanID}` +export const CHANNEL_BULK_DELETE = (chanID: BigString) => `/channels/${chanID}/messages/bulk-delete` +export const CHANNEL_CALL_RING = (chanID: BigString) => `/channels/${chanID}/call/ring` +export const CHANNEL_CROSSPOST = (chanID: BigString, msgID: BigString) => `/channels/${chanID}/messages/${msgID}/crosspost` +export const CHANNEL_FOLLOW = (chanID: BigString) => `/channels/${chanID}/followers` +export const CHANNEL_INVITES = (chanID: BigString) => `/channels/${chanID}/invites` +export const CHANNEL_MESSAGE_REACTION = (chanID: BigString, msgID: BigString, reaction: string) => + `/channels/${chanID}/messages/${msgID}/reactions/${reaction}` +export const CHANNEL_MESSAGE_REACTION_USER = (chanID: BigString, msgID: BigString, reaction: string, userID: BigString) => + `/channels/${chanID}/messages/${msgID}/reactions/${reaction}/${userID}` +export const CHANNEL_MESSAGE_REACTIONS = (chanID: BigString, msgID: BigString) => `/channels/${chanID}/messages/${msgID}/reactions` +export const CHANNEL_MESSAGE = (chanID: BigString, msgID: BigString) => `/channels/${chanID}/messages/${msgID}` +export const CHANNEL_MESSAGES = (chanID: BigString) => `/channels/${chanID}/messages` +export const CHANNEL_MESSAGES_SEARCH = (chanID: BigString) => `/channels/${chanID}/messages/search` +export const CHANNEL_PERMISSION = (chanID: BigString, overID: BigString) => `/channels/${chanID}/permissions/${overID}` +export const CHANNEL_PERMISSIONS = (chanID: BigString) => `/channels/${chanID}/permissions` +export const CHANNEL_PIN = (chanID: BigString, msgID: BigString) => `/channels/${chanID}/pins/${msgID}` +export const CHANNEL_PINS = (chanID: BigString) => `/channels/${chanID}/pins` +export const CHANNEL_RECIPIENT = (groupID: BigString, userID: BigString) => `/channels/${groupID}/recipients/${userID}` +export const CHANNEL_TYPING = (chanID: BigString) => `/channels/${chanID}/typing` +export const CHANNEL_WEBHOOKS = (chanID: BigString) => `/channels/${chanID}/webhooks` +export const CHANNELS = '/channels' +export const CUSTOM_EMOJI_GUILD = (emojiID: BigString) => `/emojis/${emojiID}/guild` +export const DISCOVERY_CATEGORIES = '/discovery/categories' +export const DISCOVERY_VALIDATION = '/discovery/valid-term' +export const GATEWAY = '/gateway' +export const GATEWAY_BOT = '/gateway/bot' +export const GUILD = (guildID: BigString) => `/guilds/${guildID}` +export const GUILD_AUDIT_LOGS = (guildID: BigString) => `/guilds/${guildID}/audit-logs` +export const GUILD_BAN = (guildID: BigString, memberID: BigString) => `/guilds/${guildID}/bans/${memberID}` +export const GUILD_BANS = (guildID: BigString) => `/guilds/${guildID}/bans` +export const GUILD_CHANNELS = (guildID: BigString) => `/guilds/${guildID}/channels` +export const GUILD_COMMAND = (applicationID: BigString, guildID: BigString, commandID: BigString) => + `/applications/${applicationID}/guilds/${guildID}/commands/${commandID}` +export const GUILD_COMMAND_PERMISSIONS = (applicationID: BigString, guildID: BigString) => + `/applications/${applicationID}/guilds/${guildID}/commands/permissions` +export const GUILD_COMMANDS = (applicationID: BigString, guildID: BigString) => `/applications/${applicationID}/guilds/${guildID}/commands` +export const GUILD_DISCOVERY = (guildID: BigString) => `/guilds/${guildID}/discovery-metadata` +export const GUILD_DISCOVERY_CATEGORY = (guildID: BigString, categoryID: BigString) => `/guilds/${guildID}/discovery-categories/${categoryID}` +export const GUILD_EMOJI = (guildID: BigString, emojiID: BigString) => `/guilds/${guildID}/emojis/${emojiID}` +export const GUILD_EMOJIS = (guildID: BigString) => `/guilds/${guildID}/emojis` +export const GUILD_INTEGRATION = (guildID: BigString, inteID: BigString) => `/guilds/${guildID}/integrations/${inteID}` +export const GUILD_INTEGRATION_SYNC = (guildID: BigString, inteID: BigString) => `/guilds/${guildID}/integrations/${inteID}/sync` +export const GUILD_INTEGRATIONS = (guildID: BigString) => `/guilds/${guildID}/integrations` +export const GUILD_INVITES = (guildID: BigString) => `/guilds/${guildID}/invites` +export const GUILD_VANITY_URL = (guildID: BigString) => `/guilds/${guildID}/vanity-url` +export const GUILD_MEMBER = (guildID: BigString, memberID: BigString) => `/guilds/${guildID}/members/${memberID}` +export const GUILD_MEMBER_NICK = (guildID: BigString, memberID: BigString) => `/guilds/${guildID}/members/${memberID}/nick` +export const GUILD_MEMBER_ROLE = (guildID: BigString, memberID: BigString, roleID: BigString) => + `/guilds/${guildID}/members/${memberID}/roles/${roleID}` +export const GUILD_MEMBERS = (guildID: BigString) => `/guilds/${guildID}/members` +export const GUILD_MEMBERS_SEARCH = (guildID: BigString) => `/guilds/${guildID}/members/search` +export const GUILD_MESSAGES_SEARCH = (guildID: BigString) => `/guilds/${guildID}/messages/search` +export const GUILD_PREVIEW = (guildID: BigString) => `/guilds/${guildID}/preview` +export const GUILD_PRUNE = (guildID: BigString) => `/guilds/${guildID}/prune` +export const GUILD_ROLE = (guildID: BigString, roleID: BigString) => `/guilds/${guildID}/roles/${roleID}` +export const GUILD_ROLES = (guildID: BigString) => `/guilds/${guildID}/roles` +export const GUILD_STICKER = (guildID: BigString, stickerID: BigString) => `/guilds/${guildID}/stickers/${stickerID}` +export const GUILD_STICKERS = (guildID: BigString) => `/guilds/${guildID}/stickers` +export const GUILD_TEMPLATE = (code: string) => `/guilds/templates/${code}` +export const GUILD_TEMPLATES = (guildID: BigString) => `/guilds/${guildID}/templates` +export const GUILD_TEMPLATE_GUILD = (guildID: BigString, code: string) => `/guilds/${guildID}/templates/${code}` +export const GUILD_VOICE_REGIONS = (guildID: BigString) => `/guilds/${guildID}/regions` +export const GUILD_WEBHOOKS = (guildID: BigString) => `/guilds/${guildID}/webhooks` +export const GUILD_WELCOME_SCREEN = (guildID: BigString) => `/guilds/${guildID}/welcome-screen` +export const GUILD_WIDGET = (guildID: BigString) => `/guilds/${guildID}/widget.json` +export const GUILD_WIDGET_SETTINGS = (guildID: BigString) => `/guilds/${guildID}/widget` +export const GUILD_VOICE_STATE = (guildID: BigString, user: BigString) => `/guilds/${guildID}/voice-states/${user}` +export const GUILDS = '/guilds' +export const INTERACTION_RESPOND = (interactID: BigString, interactToken: string) => `/interactions/${interactID}/${interactToken}/callback` +export const INVITE = (inviteID: string) => `/invites/${inviteID}` +export const OAUTH2_APPLICATION = (appID: BigString) => `/oauth2/applications/${appID}` +export const STAGE_INSTANCE = (channelID: BigString) => `/stage-instances/${channelID}` +export const STAGE_INSTANCES = '/stage-instances' +export const STICKER = (stickerID: BigString) => `/stickers/${stickerID}` +export const STICKER_PACKS = '/sticker-packs' +export const THREAD_MEMBER = (channelID: BigString, userID: BigString) => `/channels/${channelID}/thread-members/${userID}` +export const THREAD_MEMBERS = (channelID: BigString) => `/channels/${channelID}/thread-members` +export const THREAD_WITH_MESSAGE = (channelID: BigString, msgID: BigString) => `/channels/${channelID}/messages/${msgID}/threads` +export const THREAD_WITHOUT_MESSAGE = (channelID: BigString) => `/channels/${channelID}/threads` +export const THREADS_ACTIVE = (channelID: BigString) => `/channels/${channelID}/threads/active` +export const THREADS_ARCHIVED = (channelID: BigString, type: string) => `/channels/${channelID}/threads/archived/${type}` +export const THREADS_ARCHIVED_JOINED = (channelID: BigString) => `/channels/${channelID}/users/@me/threads/archived/private` +export const THREADS_GUILD_ACTIVE = (guildID: BigString) => `/guilds/${guildID}/threads/active` +export const USER = (userID: BigString) => `/users/${userID}` +export const USER_BILLING = (userID: BigString) => `/users/${userID}/billing` +export const USER_BILLING_PAYMENTS = (userID: BigString) => `/users/${userID}/billing/payments` +export const USER_BILLING_PREMIUM_SUBSCRIPTION = (userID: BigString) => `/users/${userID}/billing/premium-subscription` +export const USER_CHANNELS = (userID: BigString) => `/users/${userID}/channels` +export const USER_CONNECTIONS = (userID: BigString) => `/users/${userID}/connections` +export const USER_CONNECTION_PLATFORM = (userID: BigString, platform: string, id: string) => `/users/${userID}/connections/${platform}/${id}` +export const USER_GUILD = (userID: BigString, guildID: BigString) => `/users/${userID}/guilds/${guildID}` +export const USER_GUILDS = (userID: BigString) => `/users/${userID}/guilds` +export const USER_MFA_CODES = (userID: BigString) => `/users/${userID}/mfa/codes` +export const USER_MFA_TOTP_DISABLE = (userID: BigString) => `/users/${userID}/mfa/totp/disable` +export const USER_MFA_TOTP_ENABLE = (userID: BigString) => `/users/${userID}/mfa/totp/enable` +export const USER_NOTE = (userID: BigString, targetID: BigString) => `/users/${userID}/note/${targetID}` +export const USER_PROFILE = (userID: BigString) => `/users/${userID}/profile` +export const USER_RELATIONSHIP = (userID: BigString, relID: BigString) => `/users/${userID}/relationships/${relID}` +export const USER_SETTINGS = (userID: BigString) => `/users/${userID}/settings` +export const USERS = '/users' +export const VOICE_REGIONS = '/voice/regions' +export const WEBHOOK = (hookID: BigString) => `/webhooks/${hookID}` +export const WEBHOOK_MESSAGE = (hookID: BigString, token: string, msgID: BigString) => `/webhooks/${hookID}/${token}/messages/${msgID}` +export const WEBHOOK_SLACK = (hookID: BigString) => `/webhooks/${hookID}/slack` +export const WEBHOOK_TOKEN = (hookID: BigString, token: string) => `/webhooks/${hookID}/${token}` +export const WEBHOOK_TOKEN_SLACK = (hookID: BigString, token: string) => `/webhooks/${hookID}/${token}/slack` + +// CDN Endpoints +export const ACHIEVEMENT_ICON = (applicationID: BigString, achievementID: BigString, icon: string) => + `/app-assets/${applicationID}/achievements/${achievementID}/icons/${icon}` +export const APPLICATION_ASSET = (applicationID: BigString, asset: string) => `/app-assets/${applicationID}/${asset}` +export const APPLICATION_ICON = (applicationID: BigString, icon: string) => `/app-icons/${applicationID}/${icon}` +export const BANNER = (guildOrUserID: BigString, hash: string) => `/banners/${guildOrUserID}/${hash}` +export const CHANNEL_ICON = (chanID: BigString, chanIcon: string) => `/channel-icons/${chanID}/${chanIcon}` +export const CUSTOM_EMOJI = (emojiID: BigString) => `/emojis/${emojiID}` +export const DEFAULT_USER_AVATAR = (userDiscriminator: string) => `/embed/avatars/${userDiscriminator}` +export const GUILD_AVATAR = (guildID: BigString, userID: BigString, guildAvatar: string) => + `/guilds/${guildID}/users/${userID}/avatars/${guildAvatar}` +export const GUILD_DISCOVERY_SPLASH = (guildID: BigString, guildDiscoverySplash: string) => `/discovery-splashes/${guildID}/${guildDiscoverySplash}` +export const GUILD_ICON = (guildID: BigString, guildIcon: string) => `/icons/${guildID}/${guildIcon}` +export const GUILD_SPLASH = (guildID: BigString, guildSplash: string) => `/splashes/${guildID}/${guildSplash}` +export const ROLE_ICON = (roleID: BigString, roleIcon: string) => `/role-icons/${roleID}/${roleIcon}` +export const TEAM_ICON = (teamID: BigString, teamIcon: string) => `/team-icons/${teamID}/${teamIcon}` +export const USER_AVATAR = (userID: BigString, userAvatar: string) => `/avatars/${userID}/${userAvatar}` + +// Client Endpoints +export const MESSAGE_LINK = (guildID: BigString, channelID: BigString, messageID: BigString) => `/channels/${guildID}/${channelID}/${messageID}` diff --git a/packages/client/src/RequestHandler.ts b/packages/client/src/RequestHandler.ts new file mode 100644 index 000000000..8bc413442 --- /dev/null +++ b/packages/client/src/RequestHandler.ts @@ -0,0 +1,117 @@ +import type { RequestMethods, RestManager } from '@discordeno/rest' +import { createRestManager } from '@discordeno/rest' +import Base from './Base.js' +import type Client from './Client.js' +import type { FileContent, RequestHandlerOptions } from './typings.js' + +// TODO: make dynamic based on package.json file +const version = '18.0.0-alpha.1' + +export class RequestHandler { + /** The client manager. */ + client: Client + /** The options this manager was configured with. */ + options: RequestHandlerOptions + /** The user agent used to make requests. */ + userAgent: string + /** The rate limits currently in cache. */ + ratelimits: Record + /** The latency information for this manager. */ + latencyRef: { + latency: number + raw: number[] + timeOffset: number + timeOffsets: number[] + lastTimeOffsetCheck: number + } + + /** Whether or not the manager is globally blocked. */ + globalBlock: boolean + /** The ready queue */ + readyQueue: unknown[] + /** The internal rest manager from dd. */ + discordeno: RestManager + + constructor(client: Client, options: RequestHandlerOptions) { + this.options = options = Object.assign( + { + // agent: client.options.agent || null, + agent: null, + baseURL: client.BASE_URL, + decodeReasons: true, + disableLatencyCompensation: false, + domain: 'discord.com', + // latencyThreshold: client.options.latencyThreshold || 30000, + latencyThreshold: 30000, + // ratelimiterOffset: client.options.ratelimiterOffset || 0, + ratelimiterOffset: 0, + // requestTimeout: client.options.requestTimeout || 15000, + requestTimeout: 15000, + }, + options, + ) + + this.client = client + this.discordeno = createRestManager({ + token: this.client.token, + baseUrl: options.baseURL ?? this.client.options.proxyURL, + }) + + this.userAgent = `DiscordBot (https://github.com/discordeno/discordeno, ${version})` + this.ratelimits = {} + this.latencyRef = { + latency: this.options.ratelimiterOffset ?? 0, + raw: new Array(10).fill(this.options.ratelimiterOffset), + timeOffset: 0, + timeOffsets: new Array(10).fill(0), + lastTimeOffsetCheck: 0, + } + this.globalBlock = false + this.readyQueue = [] + } + + /** + * @deprecated Use `.client` instead + */ + get _client(): Client { + return this.client + } + + /** + * @deprecated Useless, handled by discordeno itself. Kept for Eris api compatibility. + */ + globalUnblock(): void {} + + warnUser(): void { + // LOG IT ENOUGH TIMES TO MAKE USER SEE IT CLEARLY + for (let i = 0; i < 10; i++) { + console.warn( + '[WARNING] Using internal RestManager since no proxy rest manager was provided. THIS IS NOT RECOMMENDED. Please use a proxy rest manager. If you need help setting it up, join discord.gg/ddeno', + ) + } + } + + /** + * Make an API request + * @deprecated Use a proxy rest instead. + */ + async request(method: RequestMethods, url: string, auth?: boolean, body?: any, file?: FileContent): Promise { + if (file) body.file = file + + return await this.discordeno.makeRequest(method, url, body) + } + + routefy(url: string, method: RequestMethods): string { + return this.discordeno.simplifyUrl(url, method) + } + + toString(): string { + return '[RequestHandler]' + } + + toJSON(props: string[] = []): Record { + return Base.prototype.toJSON.call(this, ['globalBlock', 'latencyRef', 'options', 'ratelimits', 'readyQueue', 'userAgent', ...props]) + } +} + +export default RequestHandler diff --git a/packages/client/src/Structures/Invite.ts b/packages/client/src/Structures/Invite.ts new file mode 100644 index 000000000..d645e74b2 --- /dev/null +++ b/packages/client/src/Structures/Invite.ts @@ -0,0 +1,146 @@ +/* eslint-disable no-useless-call */ + +import type { DiscordInvite, DiscordInviteCreate, DiscordMemberWithUser, TargetTypes } from '@discordeno/types' +import Base from '../Base.js' +import type Client from '../Client.js' +import type Channel from './channels/Channel.js' +import Guild from './guilds/Guild.js' +import Member from './guilds/Member.js' +import User from './users/User.js' + +export class Invite { + /** The client object. */ + client: Client + /** The invite code (unique Id) */ + code: string + /** The channel this invite is for */ + channel?: Channel + /** The guild this invite is for. */ + guild?: Guild + /** The user who created this invite. */ + inviter?: User + /** The amount of times this invite has been used. */ + uses: number | null = null + /** The amount of times this invite can be used. */ + maxUses: number | null = null + /** How long the invite is valid for (in seconds) */ + maxAge: number | null = null + /** Whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */ + temporary: boolean = false + /** The time at which the invite was created */ + createdAt?: number + + presenceCount?: number | null + memberCount?: number | null + + stageInstance?: { + members: Member[] + participantCount: number + speakerCount: number + topic: string + } | null + + targetApplicationID?: string | null + targetType?: TargetTypes | null + targetUser?: User | null + + constructor(data: DiscordInvite | DiscordInviteCreate, client: Client) { + // super(); + this.client = client + this.code = data.code + this.channel = data.channel + + if (data.inviter) { + this.inviter = new User(data.inviter, client) + client.users.set(this.inviter.id, this.inviter) + } + + if (this.isInviteCreate(data)) { + this.uses = data.uses !== undefined ? data.uses : null + this.maxUses = data.max_uses !== undefined ? data.max_uses : null + this.maxAge = data.max_age !== undefined ? data.max_age : null + this.temporary = data.temporary !== undefined ? data.temporary : false + this.createdAt = Date.parse(data.created_at) + } else { + if (data.guild) { + if (client.guilds.has(data.guild.id!)) { + if (data.channel) { + // @ts-expect-error should work i think dumb partials + const channel = new GuildChannel(data.channel, client) + client.guilds.get(data.guild.id!)?.channels.set(channel.id, channel) + } + } else { + this.guild = new Guild(data.guild, client) + } + } + + this.presenceCount = data.approximate_presence_count !== undefined ? data.approximate_presence_count : null + this.memberCount = data.approximate_member_count !== undefined ? data.approximate_member_count : null + if (data.stage_instance !== undefined) { + this.stageInstance = { + members: data.stage_instance.members.map((m) => { + const member = new Member(m as DiscordMemberWithUser, this.guild, client) + this.guild.members.set(member.id, member) + return member + }), + participantCount: data.stage_instance.participant_count, + speakerCount: data.stage_instance.speaker_count, + topic: data.stage_instance.topic, + } + } else { + this.stageInstance = null + } + } + + this.targetApplicationID = data.target_application !== undefined ? data.target_application.id : null + this.targetType = data.target_type !== undefined ? data.target_type : null + this.targetUser = data.target_user !== undefined ? new User(data.target_user, client) : null + if (this.targetUser) client.users.set(this.targetUser.id, this.targetUser) + } + + /** + * @deprecated Use .client + */ + get _client(): Client { + return this.client + } + + /** + * @deprecated Use .createdAt + */ + get _createdAt(): number | undefined { + return this.createdAt + } + + /** Delete the invite */ + async delete(reason?: string): Promise { + return await this.client.deleteInvite.call(this.client, this.code, reason) + } + + toString(): string { + return `[Invite ${this.code}]` + } + + toJSON(props = []): Record { + return Base.prototype.toJSON([ + 'channel', + 'code', + 'createdAt', + 'guild', + 'maxAge', + 'maxUses', + 'memberCount', + 'presenceCount', + 'revoked', + 'temporary', + 'uses', + ...props, + ]) + } + + isInviteCreate(data: DiscordInvite | DiscordInviteCreate): data is DiscordInviteCreate { + return Reflect.has(data, 'created_at') + } +} + +export default Invite diff --git a/packages/client/src/Structures/Message.ts b/packages/client/src/Structures/Message.ts new file mode 100644 index 000000000..6eb54c3c1 --- /dev/null +++ b/packages/client/src/Structures/Message.ts @@ -0,0 +1,438 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ + +import type { DiscordEmbed, DiscordMessageActivity, DiscordStickerItem } from '@discordeno/types' +import { + MessageTypes, + type DiscordApplication, + type DiscordAttachment, + type DiscordMemberWithUser, + type DiscordMessage, + type DiscordMessageComponents, + type InteractionTypes, +} from '@discordeno/types' +import Base from '../Base.js' +import type Client from '../Client.js' +import { MESSAGE_LINK } from '../Endpoints.js' +import { MessageFlags, type GetMessageReactionOptions, type MessageContentEdit, type MessageWebhookContent } from '../typings.js' +import type NewsChannel from './channels/News.js' +import type PrivateChannel from './channels/Private.js' +import type TextChannel from './channels/Text.js' +import type NewsThreadChannel from './channels/threads/NewsThread.js' +import type PublicThreadChannel from './channels/threads/PublicThread.js' +import type Guild from './guilds/Guild.js' +import Member from './guilds/Member.js' +import User from './users/User.js' + +export class Message extends Base { + /** The client manager. */ + client: Client + /** Timestamp of message creation */ + timestamp: number + /** The type of the message */ + type: MessageTypes + /** The channel the message is in. Can be partial with only the id if the channel is not cached. */ + channel: PrivateChannel | TextChannel | NewsChannel | { id: string } + /** The message content. */ + content: string + /** An object containing the reactions on the message. Each key is a reaction emoji and each value is an object with properties `me` (Boolean) and `count` (Number) for that specific reaction emoji. */ + reactions: Record + /** The ID of the guild this message is in (undefined if in DMs) */ + guildID?: string + /** ID of the webhook that sent the message */ + webhookID?: string + /** An object containing the reference to the original message if it is a crossposted message or reply */ + messageReference?: { + /** The id of the original message this message was crossposted from */ + messageID?: string + /** The id of the channel this message was crossposted from */ + channelID?: string + /** The id of the guild this message was crossposted from */ + guildID?: string + } | null + + /** The flags that are enabled on this message. */ + flags: number + /** The message author */ + author: User + /** The message author with server-specific data */ + member?: Member + /** The message that was replied to. If undefined, message data was not received. If null, the message was deleted. */ + referencedMessage?: Message | null + /** An object containing info about the interaction the message is responding to, if applicable */ + interaction?: { + /** The id of the interaction */ + id: string + /** The type of interaction */ + type: InteractionTypes + /** The name of the command */ + name: string + /** The user who invoked the interaction */ + user: User + /** The member who invoked the interaction */ + member?: Member + } + + /** Array of mentioned users */ + mentions: User[] = [] + /** Array of mentioned roles' ids */ + roleMentions: string[] = [] + /** Array of attachments */ + attachments: DiscordAttachment[] = [] + /** Array of embeds */ + embeds: DiscordEmbed[] = [] + /** The stickers sent with the message */ + stickerItems: DiscordStickerItem[] = [] + /** An array of component objects */ + components: DiscordMessageComponents = [] + /** The activity specified in the message */ + activity?: DiscordMessageActivity + /** The application of the activity in the message */ + application?: Partial + /** The ID of the interaction's application */ + applicationID?: string + /** Timestamp of latest message edit */ + editedTimestamp?: number + /** Whether the message mentions everyone/here or not */ + mentionEveryone: boolean = false + /** Whether the message is pinned or not */ + pinned: boolean = false + /** Whether to play the message using TTS or not */ + tts: boolean = false + + constructor(data: DiscordMessage, client: Client) { + super(data.id) + + this.client = client + this.timestamp = Date.parse(data.timestamp) + + this.type = data.type || MessageTypes.Default + this.timestamp = Date.parse(data.timestamp) + this.channel = this.client.getChannel(data.channel_id) || { + id: data.channel_id, + } + this.content = '' + this.reactions = {} + this.guildID = data.guild_id + this.webhookID = data.webhook_id + + if (data.message_reference) { + this.messageReference = { + messageID: data.message_reference.message_id, + channelID: data.message_reference.channel_id, + guildID: data.message_reference.guild_id, + } + } else { + this.messageReference = null + } + + this.flags = data.flags || 0 + + this.author = new User(data.author, client) + if (!data.webhook_id) { + this.client.users.set(this.author.id, this.author) + } + + if (data.referenced_message) { + const channel = this.client.getChannel(data.referenced_message.channel_id) as TextChannel + this.referencedMessage = new Message(data.referenced_message, this.client) + + if (channel) { + channel.messages.set(this.referencedMessage.id, this.referencedMessage) + } + } else { + this.referencedMessage = data.referenced_message + } + + if (data.interaction) { + this.interaction = { + id: data.interaction.id, + type: data.interaction.type, + name: data.interaction.name, + user: new User(data.interaction.user, client), + } + + if (data.interaction.member) { + data.interaction.member.user = data.interaction.user + + if (this.guild) { + this.interaction.member = new Member( + // @ts-expect-error some eris magic at play here + data.interaction.member, + this.guild, + client, + ) + this.guild.members.set(this.interaction.member.id, this.interaction.member) + } else { + // @ts-expect-error some eris magic at play here + interactionMember = data.interaction.member + } + } else if (this.guild?.members.has(data.interaction.user.id)) { + this.interaction.member = this.guild.members.get(data.interaction.user.id) + } + } + + if (this.guild) { + if (data.member) { + data.member.user = data.author + this.member = new Member(data.member as DiscordMemberWithUser, this.guild, client) + this.guild.members.set(this.member.id, this.member) + } else if (this.guild.members.has(this.author.id)) { + this.member = this.guild.members.get(this.author.id) + } + } + + this.update(data) + } + + /** + * @deprecated Use `.client` instead. + */ + get _client(): Client { + return this.client + } + + get guild(): Guild | undefined { + return this.guildID ? this.client.guilds.get(this.guildID) : undefined + } + + update(data: DiscordMessage) { + if (data.pinned !== undefined) this.pinned = !!data.pinned + + if (data.tts !== undefined) this.tts = data.tts + if (data.attachments !== undefined) this.attachments = data.attachments + if (data.embeds !== undefined) this.embeds = data.embeds + if (data.flags !== undefined) this.flags = data.flags + if (data.activity !== undefined) this.activity = data.activity + if (data.components !== undefined) this.components = data.components + if (data.application !== undefined) this.application = data.application + + if (data.edited_timestamp) this.editedTimestamp = Date.parse(data.edited_timestamp) + if (data.application_id !== undefined) this.applicationID = data.application_id + if (data.sticker_items !== undefined) this.stickerItems = data.sticker_items + + if (data.content !== undefined) { + this.content = data.content || '' + this.mentionEveryone = !!data.mention_everyone + this.mentions = [] + + for (const mention of data.mentions ?? []) { + const user = new User(mention, this.client) + this.client.users.set(user.id, user) + + if (mention.member && this.guild) { + mention.member.user = mention + this.guild.members.set(mention.id, new Member(mention.member as DiscordMemberWithUser, this.guild, this.client)) + } + } + + if (data.mention_roles) this.roleMentions = data.mention_roles + } + + if (data.reactions) { + for (const reaction of data.reactions ?? []) { + this.reactions[reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name!] = { + count: reaction.count, + me: reaction.me, + } + } + } + } + + get channelMentions(): string[] { + return (this.content.match(/<#[0-9]+>/g) ?? []).map((mention) => mention.substring(2, mention.length - 1)) + } + + get cleanContent() { + let cleanContent = this.content?.replace(//g, '$1') || '' + + let authorName = this.author.username + if (this.guild) { + const member = this.guild.members.get(this.author.id) + if (member?.nick) { + authorName = member.nick + } + } + cleanContent = cleanContent.replace(new RegExp(`<@!?${this.author.id}>`, 'g'), '@\u200b' + authorName) + + if (this.mentions) { + this.mentions.forEach((mention) => { + if (this.guild) { + const member = this.guild.members.get(mention.id) + if (member?.nick) { + cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, 'g'), '@\u200b' + member.nick) + } + } + cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, 'g'), '@\u200b' + mention.username) + }) + } + + if (this.guild && this.roleMentions) { + for (const roleID of this.roleMentions) { + const role = this.guild.roles.get(roleID) + const roleName = role ? role.name : 'deleted-role' + cleanContent = cleanContent.replace(new RegExp(`<@&${roleID}>`, 'g'), '@\u200b' + roleName) + } + } + + this.channelMentions.forEach((id) => { + const channel = this.client.getChannel(id) as TextChannel + if (channel?.name && channel.mention) { + cleanContent = cleanContent.replace(channel.mention, '#' + channel.name) + } + }) + + return cleanContent.replace(/@everyone/g, '@\u200beveryone').replace(/@here/g, '@\u200bhere') + } + + get jumpLink() { + return `${this.client.CLIENT_URL}${MESSAGE_LINK(this.guildID ?? '@me', this.channel.id, this.id)}` + } + + /** Add a reaction to a message */ + async addReaction(reaction: string): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot have reactions') + } + return await this.client.addMessageReaction.call(this.client, this.channel.id, this.id, reaction) + } + + /** Create a thread with this message */ + async createThreadWithMessage(options: { + name: string + autoArchiveDuration: 60 | 1440 | 4320 | 10080 + }): Promise { + return await this.client.createThreadWithMessage.call(this.client, this.channel.id, this.id, options) + } + + /** Crosspost (publish) a message to subscribed channels (NewsChannel only) */ + async crosspost(): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot be crossposted') + } + + return await this.client.crosspostMessage.call(this.client, this.channel.id, this.id) + } + + /** Delete the message */ + async delete(reason?: string): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot be deleted') + } + + return await this.client.deleteMessage.call(this.client, this.channel.id, this.id, reason) + } + + /** Delete the message as a webhook */ + async deleteWebhook(token: string): Promise { + if (!this.webhookID) throw new Error('Message is not a webhook') + if (this.flags & MessageFlags.EPHEMERAL) throw new Error('Ephemeral messages cannot be deleted') + + return await this.client.deleteWebhookMessage.call(this.client, this.webhookID, token, this.id) + } + + /** Edit the message */ + async edit(content: MessageContentEdit): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot be edited via this method') + } + + return await this.client.editMessage.call(this.client, this.channel.id, this.id, content) + } + + /** Edit the message as a webhook */ + async editWebhook(token: string, options: MessageWebhookContent): Promise { + if (!this.webhookID) { + throw new Error('Message is not a webhook') + } + + return await this.client.editWebhookMessage.call(this.client, this.webhookID, token, this.id, options) + } + + /** Get a list of users who reacted with a specific reaction */ + async getReaction(reaction: string, options?: GetMessageReactionOptions): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot have reactions') + } + + return await this.client.getMessageReaction.call(this.client, this.channel.id, this.id, reaction, options) + } + + /** Pin the message */ + async pin(): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot be pinned') + } + + return await this.client.pinMessage.call(this.client, this.channel.id, this.id) + } + + /** Remove a reaction from a message */ + async removeReaction(reaction: string): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot have reactions') + } + + return await this.client.removeMessageReaction.call(this.client, this.channel.id, this.id, reaction) + } + + /** Remove all reactions from a message for a single emoji */ + async removeReactionEmoji(reaction: string): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot have reactions') + } + + return await this.client.removeMessageReactionEmoji.call(this.client, this.channel.id, this.id, reaction) + } + + /** Remove all reactions from a message */ + async removeReactions(): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot have reactions') + } + + return await this.client.removeMessageReactions.call(this.client, this.channel.id, this.id) + } + + /** Unpin the message */ + async unpin(): Promise { + if (this.flags & MessageFlags.EPHEMERAL) { + throw new Error('Ephemeral messages cannot be pinned') + } + + return await this.client.unpinMessage.call(this.client, this.channel.id, this.id) + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'activity', + 'application', + 'attachments', + 'author', + 'content', + 'editedTimestamp', + 'embeds', + 'flags', + 'guildID', + 'hit', + 'member', + 'mentionEveryone', + 'mentions', + 'messageReference', + 'pinned', + 'reactions', + 'referencedMesssage', + 'roleMentions', + 'stickers', + 'stickerItems', + 'timestamp', + 'tts', + 'type', + 'webhookID', + ...props, + ]) + } +} + +export default Message diff --git a/packages/client/src/Structures/Permission.ts b/packages/client/src/Structures/Permission.ts new file mode 100644 index 000000000..29fdf8404 --- /dev/null +++ b/packages/client/src/Structures/Permission.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { BitwisePermissionFlags } from '@discordeno/types' +import { Base } from '../Base.js' +import type { BigString } from '../Client.js' + +export class Permission { + allow: bigint + deny: bigint + _json?: Record + + constructor(allow: BigString | number = 0, deny: BigString | number = 0) { + this.allow = BigInt(allow) + this.deny = BigInt(deny) + } + + get isAdmin(): boolean { + return !!(this.allow & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) + } + + get json() { + if (!this._json) { + this._json = {} + for (const key of Object.keys(BitwisePermissionFlags)) { + if (typeof key === 'number') continue + + const perm = key as keyof typeof BitwisePermissionFlags + + if (this.allow & BigInt(BitwisePermissionFlags[perm])) { + this._json[perm] = true + } else if (this.deny & BigInt(BitwisePermissionFlags[perm])) { + this._json[perm] = false + } + } + } + return this._json + } + + /** Check if this permission allows a specific permission */ + has(permission: bigint | keyof typeof BitwisePermissionFlags): boolean { + if (this.isAdmin) return true + + if (typeof permission === 'bigint') { + return (this.allow & permission) === permission + } + return !!(this.allow & BigInt(BitwisePermissionFlags[permission])) + } + + toString() { + return `[${this.constructor.name} +${this.allow} -${this.deny}]` + } + + toJSON(props: string[] = []): Record { + return Base.prototype.toJSON.call(['allow', 'deny', ...props]) + } +} + +export default Permission diff --git a/packages/client/src/Structures/PermissionOverwrite.ts b/packages/client/src/Structures/PermissionOverwrite.ts new file mode 100644 index 000000000..8f613a3fa --- /dev/null +++ b/packages/client/src/Structures/PermissionOverwrite.ts @@ -0,0 +1,22 @@ +import type { DiscordOverwrite, OverwriteTypes } from '@discordeno/types' +import { Base } from '../Base.js' +import type { BigString } from '../Client.js' +import Permission from './Permission.js' + +export class PermissionOverwrite extends Permission { + id: BigString + type: OverwriteTypes + + constructor(data: DiscordOverwrite) { + super(data.allow, data.deny) + + this.id = data.id + this.type = data.type + } + + toJSON(props: string[] = []): Record { + return Base.prototype.toJSON.call(['id', 'type', ...props]) + } +} + +export default PermissionOverwrite diff --git a/packages/client/src/Structures/channels/Category.ts b/packages/client/src/Structures/channels/Category.ts new file mode 100644 index 000000000..af2fce989 --- /dev/null +++ b/packages/client/src/Structures/channels/Category.ts @@ -0,0 +1,12 @@ +import type { BigString } from '@discordeno/types' +import type Collection from '../../Collection.js' +import type { AnyGuildChannel } from '../../typings.js' +import GuildChannel from './Guild.js' + +export class CategoryChannel extends GuildChannel { + get channels(): Collection> { + return this.guild?.channels.filter((c) => c.parentID === this.id) as unknown as Collection> + } +} + +export default CategoryChannel diff --git a/packages/client/src/Structures/channels/Channel.ts b/packages/client/src/Structures/channels/Channel.ts new file mode 100644 index 000000000..f30c8ee27 --- /dev/null +++ b/packages/client/src/Structures/channels/Channel.ts @@ -0,0 +1,81 @@ +import { ChannelTypes, type DiscordChannel } from "@discordeno/types"; +import Base from "../../Base.js"; +import type Client from "../../Client.js"; +import type { AnyChannel } from "../../typings.js"; +import CategoryChannel from "./Category.js"; +import GuildChannel from "./Guild.js"; +import NewsChannel from "./News.js"; +import PrivateChannel from "./Private.js"; +import StageChannel from "./Stage.js"; +import TextChannel from "./Text.js"; +import TextVoiceChannel from "./TextVoice.js"; +import NewsThreadChannel from "./threads/NewsThread.js"; +import PrivateThreadChannel from "./threads/PrivateThread.js"; +import PublicThreadChannel from "./threads/PublicThread.js"; + + +export class Channel extends Base { + type: ChannelTypes; + client: Client; + + constructor(data: DiscordChannel | Pick, client: Client) { + super(data.id); + this.type = data.type; + this.client = client; + } + + get mention(): string { + return `<#${this.id}>`; + } + + static from(data: DiscordChannel, client: Client): AnyChannel { + switch (data.type) { + case ChannelTypes.GuildText: { + return new TextChannel(data, client); + } + case ChannelTypes.DM: { + return new PrivateChannel(data, client); + } + case ChannelTypes.GuildVoice: { + return new TextVoiceChannel(data, client); + } + case ChannelTypes.GuildCategory: { + return new CategoryChannel(data, client); + } + case ChannelTypes.GuildAnnouncement: { + return new NewsChannel(data, client); + } + case ChannelTypes.AnnouncementThread: { + return new NewsThreadChannel(data, client); + } + case ChannelTypes.PublicThread: { + return new PublicThreadChannel(data, client); + } + case ChannelTypes.PrivateThread: { + return new PrivateThreadChannel(data, client); + } + case ChannelTypes.GuildStageVoice: { + return new StageChannel(data, client); + } + } + if (data.guild_id) { + if (data.last_message_id !== undefined) { + client.emit("warn", new Error(`Unknown guild text channel type: ${data.type}\n${JSON.stringify(data)}`)); + return new TextChannel(data, client); + } + client.emit("warn", new Error(`Unknown guild channel type: ${data.type}\n${JSON.stringify(data)}`)); + return new GuildChannel(data, client); + } + client.emit("warn", new Error(`Unknown channel type: ${data.type}\n${JSON.stringify(data)}`)); + return new Channel(data, client); + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + "type", + ...props, + ]); + } +} + +export default Channel; \ No newline at end of file diff --git a/packages/client/src/Structures/channels/Guild.ts b/packages/client/src/Structures/channels/Guild.ts new file mode 100644 index 000000000..d1831ddba --- /dev/null +++ b/packages/client/src/Structures/channels/Guild.ts @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +import type { BigString, DiscordChannel, OverwriteTypes } from '@discordeno/types' +import { BitwisePermissionFlags } from '@discordeno/types' +import { Collection } from '@discordeno/utils' +import type Client from '../../Client.js' +import type { EditChannelOptions, EditChannelPositionOptions } from '../../typings.js' +import type Guild from '../guilds/Guild.js' +import Permission from '../Permission.js' +import PermissionOverwrite from '../PermissionOverwrite.js' +import Channel from './Channel.js' +import type Member from './threads/Member.js' +import ThreadChannel from './threads/Thread.js' + +export class GuildChannel extends Channel { + position: number + name: string + parentID?: BigString | null + guild: Guild + nsfw: boolean + permissionOverwrites = new Collection() + + constructor(data: DiscordChannel, client: Client) { + super(data, client) + + this.position = data.position ?? 0 + this.guild = client.guilds.get(data.guild_id!)! + this.name = data.name ?? '' + this.parentID = data.parent_id + this.nsfw = !!data.nsfw + } + + update(data: DiscordChannel): void { + if (data.type !== undefined) { + this.type = data.type + } + if (data.name !== undefined) { + this.name = data.name + } + if (data.position !== undefined) { + this.position = data.position + } + if (data.parent_id !== undefined) { + this.parentID = data.parent_id + } + this.nsfw = !!data.nsfw + if (data.permission_overwrites) { + data.permission_overwrites.forEach((overwrite) => { + const perms = new PermissionOverwrite(overwrite) + this.permissionOverwrites.set(perms.id, perms) + }) + } + } + + /** Delete the channel */ + async delete(reason?: string): Promise { + return await this.client.deleteChannel.call(this.client, this.id, reason) + } + + /** Delete a channel permission overwrite */ + async deletePermission(overwriteID: BigString, reason?: string): Promise { + return await this.client.deleteChannelPermission.call(this.client, this.id, overwriteID, reason) + } + + /** Edit the channel's properties */ + async edit(options: EditChannelOptions, reason?: string) { + return await this.client.editChannel.call(this.client, this.id, options, reason) + } + + /** Create a channel permission overwrite */ + async editPermission(overwriteID: BigString, allow: bigint | number, deny: bigint | number, type: OverwriteTypes, reason?: string): Promise { + return await this.client.editChannelPermission.call(this.client, this.id, overwriteID, allow, deny, type, reason) + } + + /** Edit the channel's position. Note that channel position numbers are lowest on top and highest at the bottom. */ + async editPosition(position: number, options?: EditChannelPositionOptions): Promise { + return await this.client.editChannelPosition.call(this.client, this.id, position, options) + } + + /** Get the channel-specific permissions of a member */ + permissionsOf(memberID: BigString | Member): Permission { + const member = ['string', 'bigint'].includes(typeof memberID) ? this.guild.members.get(memberID as BigString)! : (memberID as Member) + let permission = this.guild.permissionsOf(member).allow + if (permission & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) { + return new Permission(BitwisePermissionFlags.ADMINISTRATOR) + } + const channel = this instanceof ThreadChannel ? this.guild.channels.get(this.parentID) : this + let overwrite = channel?.permissionOverwrites.get(this.guild.id) + if (overwrite) { + permission = (permission & ~overwrite.deny) | overwrite.allow + } + let deny = 0n + let allow = 0n + for (const roleID of member.roles) { + if ((overwrite = channel?.permissionOverwrites.get(roleID))) { + deny |= overwrite.deny + allow |= overwrite.allow + } + } + permission = (permission & ~deny) | allow + overwrite = channel?.permissionOverwrites.get(member.id) + if (overwrite) { + permission = (permission & ~overwrite.deny) | overwrite.allow + } + return new Permission(permission) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['name', 'nsfw', 'parentID', 'permissionOverwrites', 'position', ...props]) + } +} + +export default GuildChannel diff --git a/packages/client/src/Structures/channels/News.ts b/packages/client/src/Structures/channels/News.ts new file mode 100644 index 000000000..844383136 --- /dev/null +++ b/packages/client/src/Structures/channels/News.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ +import type { BigString, DiscordChannel } from '@discordeno/types' +import type Client from '../../Client.js' +import type { ChannelFollow } from '../../typings.js' +import type Message from '../Message.js' +import TextChannel from './Text.js' + +export class NewsChannel extends TextChannel { + constructor(data: DiscordChannel, client: Client, messageLimit?: number) { + super(data, client, messageLimit) + + this.rateLimitPerUser = 0 + this.update(data) + } + + /** Crosspost (publish) a message to subscribed channels */ + async crosspostMessage(messageID: BigString): Promise { + return await this.client.crosspostMessage.call(this.client, this.id, messageID) + } + + /** Follow this channel in another channel. This creates a webhook in the target channel */ + async follow(webhookChannelID: BigString): Promise { + return await this.client.followChannel.call(this.client, this.id, webhookChannelID) + } +} + +export default NewsChannel diff --git a/packages/client/src/Structures/channels/Private.ts b/packages/client/src/Structures/channels/Private.ts new file mode 100644 index 000000000..1c15f3733 --- /dev/null +++ b/packages/client/src/Structures/channels/Private.ts @@ -0,0 +1,104 @@ +/* eslint-disable no-useless-call */ +import { ChannelTypes, type BigString, type DiscordChannel, type GetMessagesOptions } from '@discordeno/types' +import type Client from '../../Client.js' +import Collection from '../../Collection.js' +import type { FileContent, GetMessageReactionOptions, MessageContent, MessageContentEdit } from '../../typings.js' +import type Message from '../Message.js' +import User from '../users/User.js' +import Channel from './Channel.js' + +export class PrivateChannel extends Channel { + /** The ID of the last message in this channel */ + lastMessageID?: string | null + // TODO: THIS A THING IN DMS???? + /** The rate limit per user. */ + rateLimitPerUser?: number + /** Collection of Messages in this channel */ + messages: Collection + /** The recipient in this private channel */ + recipient?: User + + constructor(data: DiscordChannel, client: Client) { + super(data, client) + + this.lastMessageID = data.last_message_id + this.rateLimitPerUser = data.rate_limit_per_user + if (this.type === ChannelTypes.DM || this.type === undefined) { + if (data.recipients?.[0]) this.recipient = new User(data.recipients[0], client) + } + this.messages = new Collection() + this.messages.limit = client.options.messageLimit + } + + /** Add a reaction to a message */ + async addMessageReaction(messageID: BigString, reaction: string): Promise { + return await this.client.addMessageReaction.call(this.client, this.id, messageID, reaction) + } + + /** Create a message in a text channel */ + async createMessage(content: MessageContent, file: FileContent | FileContent[]): Promise { + return await this.client.createMessage.call(this.client, this.id, content, file) + } + + // TODO: REASONS ARE A THING FOR AUDIT LOGS IN DMS??? + /** Delete a message */ + async deleteMessage(messageID: BigString, reason?: string): Promise { + return await this.client.deleteMessage.call(this.client, this.id, messageID, reason) + } + + /** Edit a message */ + async editMessage(messageID: BigString, content: MessageContentEdit): Promise { + return await this.client.editMessage.call(this.client, this.id, messageID, content) + } + + /** Get a previous message in a text channel */ + async getMessage(messageID: BigString): Promise { + return await this.client.getMessage.call(this.client, this.id, messageID) + } + + /** Get a list of users who reacted with a specific reaction */ + async getMessageReaction(messageID: BigString, reaction: string, options: GetMessageReactionOptions): Promise { + return await this.client.getMessageReaction.call(this.client, this.id, messageID, reaction, options) + } + + /** Get a previous message in a text channel */ + async getMessages(options: GetMessagesOptions): Promise { + return await this.client.getMessages.call(this.client, this.id, options) + } + + /** Get all the pins in a text channel */ + async getPins(): Promise { + return await this.client.getPins.call(this.client, this.id) + } + + /** Leave the channel */ + async leave(): Promise { + return await this.client.deleteChannel.call(this.client, this.id) + } + + /** Pin a message */ + async pinMessage(messageID: BigString): Promise { + return await this.client.pinMessage.call(this.client, this.id, messageID) + } + + /** Remove a reaction from a message */ + async removeMessageReaction(messageID: BigString, reaction: string): Promise { + return await this.client.removeMessageReaction.call(this.client, this.id, messageID, reaction) + } + + /** Send typing status in a text channel */ + async sendTyping(): Promise { + return await this.client.sendChannelTyping.call(this.client, this.id) + } + + /** Unpin a message */ + async unpinMessage(messageID: BigString): Promise { + return await this.client.unpinMessage.call(this.client, this.id, messageID) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['call', 'lastCall', 'lastMessageID', 'messages', 'recipient', ...props]) + } +} + +export default PrivateChannel diff --git a/packages/client/src/Structures/channels/Stage.ts b/packages/client/src/Structures/channels/Stage.ts new file mode 100644 index 000000000..c07be32a9 --- /dev/null +++ b/packages/client/src/Structures/channels/Stage.ts @@ -0,0 +1,46 @@ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ +import type { DiscordChannel } from "@discordeno/types" +import type { StageInstanceOptions } from "../../typings.js" +import type StageInstance from "../guilds/StageInstance.js" +import VoiceChannel from "./Voice.js" + + +export class StageChannel extends VoiceChannel { + /** The topic of the channel */ + topic?: string | null + + update(data: DiscordChannel): void { + super.update(data) + + if (data.topic !== undefined) { + this.topic = data.topic + } + } + + /** Create a stage instance */ + async createInstance(options: StageInstanceOptions): Promise { + return await this.client.createStageInstance.call(this.client, this.id, options) + } + + /** Delete the stage instance for this channel */ + async deleteInstance(): Promise { + return await this.client.deleteStageInstance.call(this.client, this.id) + } + + /** Update the stage instance for this channel */ + async editInstance(options: StageInstanceOptions): Promise { + return await this.client.editStageInstance.call(this.client, this.id, options) + } + + /** Get the stage instance for this channel */ + async getInstance(): Promise { + return await this.client.getStageInstance.call(this.client, this.id) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['topic', ...props]) + } +} + +export default StageChannel diff --git a/packages/client/src/Structures/channels/Text.ts b/packages/client/src/Structures/channels/Text.ts new file mode 100644 index 000000000..3152df0e5 --- /dev/null +++ b/packages/client/src/Structures/channels/Text.ts @@ -0,0 +1,375 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ +import type { BigString, DiscordChannel, GetMessagesOptions } from '@discordeno/types' +import type Client from '../../Client.js' +import Collection from '../../Collection.js' +import type { + CreateChannelInviteOptions, + CreateThreadOptions, + CreateThreadWithoutMessageOptions, + FileContent, + GetArchivedThreadsOptions, + GetMessageReactionOptions, + ListedChannelThreads, + MessageContent, + PurgeChannelOptions, +} from '../../typings.js' +import type Invite from '../Invite.js' +import type Message from '../Message.js' +import GuildChannel from './Guild.js' +import type PrivateThreadChannel from './threads/PrivateThread.js' +import type PublicThreadChannel from './threads/PublicThread.js' + +export class TextChannel extends GuildChannel { + /** Collection of Messages in this channel */ + messages: Collection + /** The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled */ + rateLimitPerUser: number | null + /** The ID of the last message in this channel */ + lastMessageID?: string | null + /** The timestamp of the last pinned message */ + lastPinTimestamp?: number | null + /** Default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + defaultAutoArchiveDuration?: number + /** The channel topic (0-4096 characters for GUILD_FORUM channels, 0-1024 characters for all others) */ + topic?: string | null + + constructor(data: DiscordChannel, client: Client, messageLimit?: number) { + super(data, client) + + this.messages = new Collection() + if (messageLimit == null) this.messages.limit = client.options.messageLimit + else this.messages.limit = messageLimit + + this.rateLimitPerUser = data.rate_limit_per_user == null ? null : data.rate_limit_per_user + + this.lastMessageID = data.last_message_id ?? null + this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null + + this.update(data) + } + + update(data: DiscordChannel): void { + super.update(data) + + if (data.rate_limit_per_user !== undefined) this.rateLimitPerUser = data.rate_limit_per_user + if (data.topic !== undefined) this.topic = data.topic + if (data.default_auto_archive_duration !== undefined) this.defaultAutoArchiveDuration = data.default_auto_archive_duration + } + + /** Add a reaction to a message */ + async addMessageReaction(messageID: BigString, reaction: string): Promise { + return this.client.addMessageReaction.call(this.client, this.id, messageID, reaction) + } + + /** Create an invite for the channel */ + async createInvite(options?: CreateChannelInviteOptions, reason?: string): Promise { + return await this.client.createChannelInvite.call(this.client, this.id, options, reason) + } + + /** + * Create a message in the channel + * @arg {String | Object} content A string or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Boolean} [content.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object} [content.messageReference] The message reference, used when replying to messages + * @arg {String} [content.messageReference.channelID] The channel ID of the referenced message + * @arg {Boolean} [content.messageReference.failIfNotExists=true] Whether to throw an error if the message reference doesn't exist. If false, and the referenced message doesn't exist, the message is created without a referenced message + * @arg {String} [content.messageReference.guildID] The guild ID of the referenced message + * @arg {String} content.messageReference.messageID The message ID of the referenced message. This cannot reference a system message + * @arg {String} [content.messageReferenceID] [DEPRECATED] The ID of the message should be replied to. Use `messageReference` instead + * @arg {Array} [content.stickerIDs] An array of IDs corresponding to stickers to send + * @arg {Boolean} [content.tts] Set the message TTS flag + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async createMessage(content: MessageContent, file: FileContent | FileContent[]) { + return this.client.createMessage.call(this.client, this.id, content, file) + } + + /** + * Create a thread with an existing message + * @arg {String} messageID The ID of the message to create the thread from + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {String} options.name The thread channel name + * @returns {Promise} + */ + async createThreadWithMessage(messageID: string, options: CreateThreadOptions) { + return this.client.createThreadWithMessage.call(this.client, this.id, messageID, options) + } + + /** + * Create a thread without an existing message + * @arg {Object} options The thread options + * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) + * @arg {String} options.name The thread channel name + * @arg {Number} [options.type] The channel type of the thread to create. It is recommended to explicitly set this property as this will be a required property in API v10 + * @returns {Promise} + */ + async createThreadWithoutMessage(options: CreateThreadWithoutMessageOptions) { + return this.client.createThreadWithoutMessage.call(this.client, this.id, options) + } + + /** + * Create a channel webhook + * @arg {Object} options Webhook options + * @arg {String} [options.avatar] The default avatar as a base64 data URI. Note: base64 strings alone are not base64 data URI strings + * @arg {String} options.name The default name + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} Resolves with a webhook object + */ + async createWebhook(options: { name: string; avatar?: string | null }, reason: string) { + return this.client.createChannelWebhook.call(this.client, this.id, options, reason) + } + + /** + * Delete a message + * @arg {String} messageID The ID of the message + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + async deleteMessage(messageID: string, reason: string) { + return this.client.deleteMessage.call(this.client, this.id, messageID, reason) + } + + /** + * Bulk delete messages (bot accounts only) + * @arg {Array} messageIDs Array of message IDs to delete + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + async deleteMessages(messageIDs: string[], reason: string) { + return this.client.deleteMessages.call(this.client, this.id, messageIDs, reason) + } + + /** + * Edit a message + * @arg {String} messageID The ID of the message + * @arg {String | Array | Object} content A string, array of strings, or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [content.file] A file object (or an Array of them) + * @arg {Buffer} content.file[].file A buffer containing file data + * @arg {String} content.file[].name What to name the file + * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference + * @returns {Promise} + */ + async editMessage(messageID: string, content: string) { + return this.client.editMessage.call(this.client, this.id, messageID, content) + } + + /** Get all archived threads in this channel */ + async getArchivedThreads(type: 'private', options?: GetArchivedThreadsOptions): Promise> + async getArchivedThreads(type: 'public', options?: GetArchivedThreadsOptions): Promise> + async getArchivedThreads( + type: 'public' | 'private', + options?: GetArchivedThreadsOptions, + ): Promise> { + return await this.client.getArchivedThreads.call(this.client, this.id, type as 'public', options) + } + + /** + * Get all invites in the channel + * @returns {Promise>} + */ + async getInvites() { + return this.client.getChannelInvites.call(this.client, this.id) + } + + /** + * Get joined private archived threads in this channel + * @arg {Object} [options] Additional options when requesting archived threads + * @arg {Date} [options.before] List of threads to return before the timestamp + * @arg {Number} [options.limit] Maximum number of threads to return + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + async getJoinedPrivateArchivedThreads(options: GetArchivedThreadsOptions) { + return this.client.getJoinedPrivateArchivedThreads.call(this.client, this.id, options) + } + + /** + * Get a previous message in the channel + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async getMessage(messageID: string) { + return this.client.getMessage.call(this.client, this.id, messageID) + } + + /** + * Get a list of users who reacted with a specific reaction + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @arg {Object} [options] Options for the request. If this is a number, it is treated as `options.limit` ([DEPRECATED] behavior) + * @arg {Number} [options.limit=100] The maximum number of users to get + * @arg {String} [options.after] Get users after this user ID + * @returns {Promise>} + */ + async getMessageReaction(messageID: string, reaction: string, options: GetMessageReactionOptions) { + return this.client.getMessageReaction.call(this.client, this.id, messageID, reaction, options) + } + + /** + * Get previous messages in the channel + * @arg {Object} [options] Options for the request. If this is a number ([DEPRECATED] behavior), it is treated as `options.limit` + * @arg {String} [options.after] Get messages after this message ID + * @arg {String} [options.around] Get messages around this message ID (does not work with limit > 100) + * @arg {String} [options.before] Get messages before this message ID + * @arg {Number} [options.limit=50] The max number of messages to get + * @returns {Promise>} + */ + async getMessages(options: GetMessagesOptions) { + return this.client.getMessages.call(this.client, this.id, options) + } + + /** + * Get all the pins in the channel + * @returns {Promise>} + */ + async getPins() { + return this.client.getPins.call(this.client, this.id) + } + + /** + * Get all the webhooks in the channel + * @returns {Promise>} Resolves with an array of webhook objects + */ + async getWebhooks() { + return this.client.getChannelWebhooks.call(this.client, this.id) + } + + /** + * Pin a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async pinMessage(messageID: string) { + return this.client.pinMessage.call(this.client, this.id, messageID) + } + + /** + * Purge previous messages in the channel with an optional filter (bot accounts only) + * @arg {Object} options Options for the request. If this is a number ([DEPRECATED] behavior), it is treated as `options.limit` + * @arg {String} [options.after] Get messages after this message ID + * @arg {String} [options.before] Get messages before this message ID + * @arg {Function} [options.filter] Optional filter function that returns a boolean when passed a Message object + * @arg {Number} options.limit The max number of messages to search through, -1 for no limit + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @returns {Promise} Resolves with the number of messages deleted + */ + async purge(limit: PurgeChannelOptions) { + return this.client.purgeChannel.call(this.client, this.id, limit) + } + + /** + * Remove a reaction from a message + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @arg {String} [userID="@me"] The ID of the user to remove the reaction for + * @returns {Promise} + */ + async removeMessageReaction(messageID: string, reaction: string, userID: string) { + return this.client.removeMessageReaction.call(this.client, this.id, messageID, reaction, userID) + } + + /** + * Remove all reactions from a message for a single emoji + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @returns {Promise} + */ + async removeMessageReactionEmoji(messageID: string, reaction: string) { + return this.client.removeMessageReactionEmoji.call(this.client, this.id, messageID, reaction) + } + + /** + * Remove all reactions from a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async removeMessageReactions(messageID: string) { + return this.client.removeMessageReactions.call(this.client, this.id, messageID) + } + + /** + * Send typing status in the channel + * @returns {Promise} + */ + async sendTyping() { + return this.client.sendChannelTyping.call(this.client, this.id) + } + + /** + * Unpin a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async unpinMessage(messageID: string) { + return this.client.unpinMessage.call(this.client, this.id, messageID) + } + + /** + * Un-send a message. You're welcome Programmix + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async unsendMessage(messageID: string) { + return this.client.deleteMessage.call(this.client, this.id, messageID) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['lastMessageID', 'lastPinTimestamp', 'messages', 'rateLimitPerUser', 'topic', ...props]) + } +} + +export default TextChannel diff --git a/packages/client/src/Structures/channels/TextVoice.ts b/packages/client/src/Structures/channels/TextVoice.ts new file mode 100644 index 000000000..a79cbb434 --- /dev/null +++ b/packages/client/src/Structures/channels/TextVoice.ts @@ -0,0 +1,271 @@ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DiscordChannel, GetMessagesOptions } from '@discordeno/types' +import type Client from '../../Client.js' +import Collection from '../../Collection.js' +import type { + CreateInviteOptions, + FileContent, + GetMessageReactionOptions, + MessageContent, + MessageContentEdit, + PurgeChannelOptions, +} from '../../typings.js' +import type Message from '../Message.js' +import VoiceChannel from './Voice.js' + +/** + * Represents a Text-in-Voice channel. See VoiceChannel for more properties and methods. + * @extends VoiceChannel + * @prop {String} lastMessageID The ID of the last message in this channel + * @prop {Collection} messages Collection of Messages in this channel + * @prop {Number} rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled + */ +export class TextVoiceChannel extends VoiceChannel { + lastMessageID: string | null + messages: Collection + rateLimitPerUser: number | null + + constructor(data: DiscordChannel, client: Client, messageLimit?: number) { + super(data, client) + + this.messages = new Collection() + if (messageLimit == null) this.messages.limit = client.options.messageLimit + else this.messages.limit = messageLimit + + this.lastMessageID = data.last_message_id ?? null + this.rateLimitPerUser = data.rate_limit_per_user == null ? null : data.rate_limit_per_user + } + + update(data: DiscordChannel) { + super.update(data) + // "not yet, possibly TBD" + if (data.rate_limit_per_user !== undefined) { + this.rateLimitPerUser = data.rate_limit_per_user + } + } + + /** + * Add a reaction to a message + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @returns {Promise} + */ + async addMessageReaction(messageID: string, reaction: string) { + return await this.client.addMessageReaction.call(this.client, this.id, messageID, reaction) + } + + /** + * Create an invite for the channel + * @arg {Object} [options] Invite generation options + * @arg {Number} [options.maxAge] How long the invite should last in seconds + * @arg {Number} [options.maxUses] How many uses the invite should last for + * @arg {Boolean} [options.temporary] Whether the invite grants temporary membership or not + * @arg {Boolean} [options.unique] Whether the invite is unique or not + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + async createInvite(options: CreateInviteOptions, reason: string) { + return await this.client.createChannelInvite.call(this.client, this.id, options, reason) + } + + /** + * Create a message in the channel + * Note: If you want to DM someone, the user ID is **not** the DM channel ID. use Client.getDMChannel() to get the DM channel ID for a user + * @arg {String | Object} content A string or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Boolean} [options.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} content.content A content string + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object} [content.messageReference] The message reference, used when replying to messages + * @arg {String} [content.messageReference.channelID] The channel ID of the referenced message + * @arg {Boolean} [content.messageReference.failIfNotExists=true] Whether to throw an error if the message reference doesn't exist. If false, and the referenced message doesn't exist, the message is created without a referenced message + * @arg {String} [content.messageReference.guildID] The guild ID of the referenced message + * @arg {String} content.messageReference.messageID The message ID of the referenced message. This cannot reference a system message + * @arg {Array} [content.stickerIDs] An array of IDs corresponding to the stickers to send + * @arg {Boolean} [content.tts] Set the message TTS flag + * @arg {Object} [file] A file object + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async createMessage(content: MessageContent, file?: FileContent | FileContent[]) { + return await this.client.createMessage.call(this.client, this.id, content, file) + } + + /** + * Delete a message + * @arg {String} messageID The ID of the message + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + async deleteMessage(messageID: string, reason: string) { + return await this.client.deleteMessage.call(this.client, this.id, messageID, reason) + } + + /** + * Bulk delete messages (bot accounts only) + * @arg {Array} messageIDs Array of message IDs to delete + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + async deleteMessages(messageIDs: string[], reason: string) { + return await this.client.deleteMessages.call(this.client, this.id, messageIDs, reason) + } + + /** + * Edit a message + * @arg {String} messageID The ID of the message + * @arg {String | Array | Object} content A string, array of strings, or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} content.content A content string + * @arg {Boolean} [content.disableEveryone] Whether to filter @everyone/@here or not (overrides default) + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference + * @returns {Promise} + */ + async editMessage(messageID: string, content: MessageContentEdit) { + return await this.client.editMessage.call(this.client, this.id, messageID, content) + } + + /** + * Get all invites in the channel + * @returns {Promise>} + */ + async getInvites() { + return await this.client.getChannelInvites.call(this.client, this.id) + } + + /** + * Get a previous message in the channel + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async getMessage(messageID: string) { + return await this.client.getMessage.call(this.client, this.id, messageID) + } + + /** + * Get a list of users who reacted with a specific reaction + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @arg {Object} [options] Options for the request. If this is a number, it is treated as `options.limit` ([DEPRECATED] behavior) + * @arg {Number} [options.limit=100] The maximum number of users to get + * @arg {String} [options.after] Get users after this user ID + * @returns {Promise>} + */ + async getMessageReaction(messageID: string, reaction: string, options: GetMessageReactionOptions) { + return await this.client.getMessageReaction.call(this.client, this.id, messageID, reaction, options) + } + + /** + * Get previous messages in the channel + * @arg {Object} [options] Options for the request. If this is a number ([DEPRECATED] behavior), it is treated as `options.limit` + * @arg {String} [options.after] Get messages after this message ID + * @arg {String} [options.around] Get messages around this message ID (does not work with limit > 100) + * @arg {String} [options.before] Get messages before this message ID + * @arg {Number} [options.limit=50] The max number of messages to get + * @returns {Promise>} + */ + async getMessages(options: GetMessagesOptions) { + return await this.client.getMessages.call(this.client, this.id, options) + } + + /** + * Purge previous messages in the channel with an optional filter (bot accounts only) + * @arg {Object} options Options for the request. If this is a number ([DEPRECATED] behavior), it is treated as `options.limit` + * @arg {String} [options.after] Get messages after this message ID + * @arg {String} [options.before] Get messages before this message ID + * @arg {Function} [options.filter] Optional filter function that returns a boolean when passed a Message object + * @arg {Number} options.limit The max number of messages to search through, -1 for no limit + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @returns {Promise} Resolves with the number of messages deleted + */ + async purge(options: PurgeChannelOptions) { + return await this.client.purgeChannel.call(this.client, this.id, options) + } + + /** + * Remove a reaction from a message + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @arg {String} [userID="@me"] The ID of the user to remove the reaction for + * @returns {Promise} + */ + async removeMessageReaction(messageID: string, reaction: string, userID: string) { + return await this.client.removeMessageReaction.call(this.client, this.id, messageID, reaction, userID) + } + + /** + * Remove all reactions from a message for a single emoji + * @arg {String} messageID The ID of the message + * @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji) + * @returns {Promise} + */ + async removeMessageReactionEmoji(messageID: string, reaction: string) { + return await this.client.removeMessageReactionEmoji.call(this.client, this.id, messageID, reaction) + } + + /** + * Remove all reactions from a message + * @arg {String} messageID The ID of the message + * @returns {Promise} + */ + async removeMessageReactions(messageID: string) { + return await this.client.removeMessageReactions.call(this.client, this.id, messageID) + } + + /** + * Send typing status in the channel + * @returns {Promise} + */ + async sendTyping() { + return await this.client.sendChannelTyping.call(this.client, this.id) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['lastMessageID', 'messages', 'rateLimitPerUser', ...props]) + } +} + +export default TextVoiceChannel diff --git a/packages/client/src/Structures/channels/Voice.ts b/packages/client/src/Structures/channels/Voice.ts new file mode 100644 index 000000000..cb7040acd --- /dev/null +++ b/packages/client/src/Structures/channels/Voice.ts @@ -0,0 +1,91 @@ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DiscordChannel } from '@discordeno/types' +import type Client from '../../Client.js' +import Collection from '../../Collection.js' +import type { CreateInviteOptions, TextVoiceChannelTypes, VideoQualityMode } from '../../typings.js' +import type Member from '../guilds/Member.js' +import GuildChannel from './Guild.js' + +export class VoiceChannel extends GuildChannel { + bitrate: number = 0 + rtcRegion: string | null = null + type: TextVoiceChannelTypes = 0 + userLimit: number = 0 + videoQualityMode: VideoQualityMode = 0 + voiceMembers: Collection + + constructor(data: DiscordChannel, client: Client) { + super(data, client) + + this.voiceMembers = new Collection() + this.update(data) + } + + update(data: DiscordChannel): void { + super.update(data) + + if (data.bitrate !== undefined) { + this.bitrate = data.bitrate + } + if (data.rtc_region !== undefined) { + this.rtcRegion = data.rtc_region + } + if (data.user_limit !== undefined) { + this.userLimit = data.user_limit + } + if (data.video_quality_mode !== undefined) { + this.videoQualityMode = data.video_quality_mode + } + } + + /** + * Create an invite for the channel + * @arg {Object} [options] Invite generation options + * @arg {Number} [options.maxAge] How long the invite should last in seconds + * @arg {Number} [options.maxUses] How many uses the invite should last for + * @arg {Boolean} [options.temporary] Whether the invite grants temporary membership or not + * @arg {Boolean} [options.unique] Whether the invite is unique or not + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + async createInvite(options: CreateInviteOptions, reason: string) { + return await this.client.createChannelInvite.call(this.client, this.id, options, reason) + } + + /** + * Get all invites in the channel + * @returns {Promise>} + */ + async getInvites() { + return await this.client.getChannelInvites.call(this.client, this.id) + } + + // TODO: gateway + // /** + // * Joins the channel. + // * @arg {Object} [options] VoiceConnection constructor options + // * @arg {Object} [options.opusOnly] Skip opus encoder initialization. You should not enable this unless you know what you are doing + // * @arg {Object} [options.shared] Whether the VoiceConnection will be part of a SharedStream or not + // * @arg {Boolean} [options.selfMute] Whether the bot joins the channel muted or not + // * @arg {Boolean} [options.selfDeaf] Whether the bot joins the channel deafened or not + // * @returns {Promise} Resolves with a VoiceConnection + // */ + // join(options: JoinVoiceChannelOptions) { + // return this.client.joinVoiceChannel.call(this.client, this.id, options); + // } + + // TODO: gateway + // /** + // * Leaves the channel. + // */ + // leave() { + // return this.client.leaveVoiceChannel.call(this.client, this.id); + // } + + toJSON(props: string[] = []): Record { + return super.toJSON(['bitrate', 'rtcRegion', 'userLimit', 'videoQualityMode', 'voiceMembers', ...props]) + } +} + +export default VoiceChannel diff --git a/packages/client/src/Structures/channels/threads/Member.ts b/packages/client/src/Structures/channels/threads/Member.ts new file mode 100644 index 000000000..b4a2b1f0a --- /dev/null +++ b/packages/client/src/Structures/channels/threads/Member.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-useless-call */ +import type { BigString, DiscordThreadMember } from "@discordeno/types" +import Base from "../../../Base.js" +import type Client from "../../../Client.js" +import type Member from "../../guilds/Member.js" + + +export class ThreadMember extends Base { + client: Client + /** The user-thread settings of this member */ + flags: number + /** The ID of the thread this member is a part of */ + threadID: BigString + /** Timestamp of when the member joined the thread */ + joinTimestamp: number + /** The guild member that this thread member belongs to. This will never be present when fetching over REST */ + guildMember?: Member + + constructor(data: DiscordThreadMember, client: Client) { + super(data.user_id) + + this.client = client + this.flags = data.flags + this.threadID = data.id + this.joinTimestamp = Date.parse(data.join_timestamp) + } + + get _client(): Client { + return this.client + } + + /** Remove the member from the thread */ + async leave(): Promise { + return await this._client.leaveThread.call(this._client, this.threadID, this.id) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['threadID', 'joinTimestamp', ...props]) + } +} + +export default ThreadMember diff --git a/packages/client/src/Structures/channels/threads/NewsThread.ts b/packages/client/src/Structures/channels/threads/NewsThread.ts new file mode 100644 index 000000000..4ff199862 --- /dev/null +++ b/packages/client/src/Structures/channels/threads/NewsThread.ts @@ -0,0 +1,5 @@ +import ThreadChannel from './Thread.js' + +export class NewsThreadChannel extends ThreadChannel {} + +export default NewsThreadChannel diff --git a/packages/client/src/Structures/channels/threads/PrivateThread.ts b/packages/client/src/Structures/channels/threads/PrivateThread.ts new file mode 100644 index 000000000..2cac590b3 --- /dev/null +++ b/packages/client/src/Structures/channels/threads/PrivateThread.ts @@ -0,0 +1,26 @@ +import type { DiscordChannel } from "@discordeno/types"; +import type Client from "../../../Client.js"; +import ThreadChannel from "./Thread.js"; + + +export class PrivateThreadChannel extends ThreadChannel { + constructor(data: DiscordChannel, client: Client, messageLimit?: number) { + super(data, client, messageLimit); + + this.update(data); + } + + update(data: DiscordChannel): void { + if(data.thread_metadata !== undefined) { + this.threadMetadata = { + archiveTimestamp: Date.parse(data.thread_metadata.archive_timestamp), + archived: data.thread_metadata.archived, + autoArchiveDuration: data.thread_metadata.auto_archive_duration, + invitable: data.thread_metadata.invitable, + locked: data.thread_metadata.locked + }; + } + } +} + +export default PrivateThreadChannel; \ No newline at end of file diff --git a/packages/client/src/Structures/channels/threads/PublicThread.ts b/packages/client/src/Structures/channels/threads/PublicThread.ts new file mode 100644 index 000000000..dcc1ad9fc --- /dev/null +++ b/packages/client/src/Structures/channels/threads/PublicThread.ts @@ -0,0 +1,5 @@ +import ThreadChannel from './Thread.js' + +export class PublicThreadChannel extends ThreadChannel {} + +export default PublicThreadChannel diff --git a/packages/client/src/Structures/channels/threads/Thread.ts b/packages/client/src/Structures/channels/threads/Thread.ts new file mode 100644 index 000000000..31394be0b --- /dev/null +++ b/packages/client/src/Structures/channels/threads/Thread.ts @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +import type { BigString, DiscordChannel, GetMessagesOptions } from '@discordeno/types' +import type Client from '../../../Client.js' +import Collection from '../../../Collection.js' +import type { FileContent, GetMessageReactionOptions, MessageContent, MessageContentEdit, PurgeChannelOptions } from '../../../typings.js' +import type Message from '../../Message.js' +import type User from '../../users/User.js' +import GuildChannel from '../Guild.js' +import ThreadMember from './Member.js' + +export class ThreadChannel extends GuildChannel { + /** The cached messages that were sent in this channel. */ + messages: Collection + /** The cached thread members that are in this channel. */ + members: Collection + /** The id of the last message in this channel. */ + lastMessageID: BigString | null + /** The id of the user who created this thread. */ + ownerID: BigString + /** The approximate amount of members that have joined this thread. */ + memberCount?: number + /** The approximate amount of messages in this channel. */ + messageCount?: number + /** The rate limit that users can send messages in this channel. 0 means no rate limit has been enabled. */ + rateLimitPerUser?: number + /** The data relevant to this thread. */ + threadMetadata?: { + /** Timestamp when the thread's archive status was last changed, used for calculating recent activity */ + archiveTimestamp: number + /** Whether the thread is archived. */ + archived: boolean + /** Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 */ + autoArchiveDuration: number + /** Whether the thread is locked. */ + locked: boolean + /** Whether or not the thread is inviteable. */ + invitable?: boolean + } + + /** The bot's thread member object if it has joined the thread. */ + member?: ThreadMember + + constructor(data: DiscordChannel, client: Client, messageLimit?: number) { + super(data, client) + + this.members = new Collection() + this.messages = new Collection() + + this.messages.limit = messageLimit ?? client.options.messageLimit + this.lastMessageID = data.last_message_id ?? null + this.ownerID = data.owner_id! + + this.update(data) + } + + update(data: DiscordChannel): void { + super.update(data) + + if (data.member_count !== undefined) { + this.memberCount = data.member_count + } + if (data.message_count !== undefined) { + this.messageCount = data.message_count + } + if (data.rate_limit_per_user !== undefined) { + this.rateLimitPerUser = data.rate_limit_per_user + } + if (data.thread_metadata !== undefined) { + this.threadMetadata = { + archiveTimestamp: Date.parse(data.thread_metadata.archive_timestamp), + archived: data.thread_metadata.archived, + autoArchiveDuration: data.thread_metadata.auto_archive_duration, + locked: data.thread_metadata.locked, + } + } + if (data.member !== undefined) { + this.member = new ThreadMember(data.member, this.client) + } + } + + async addMessageReaction(messageID: BigString, reaction: string): Promise { + return await this.client.addMessageReaction.call(this.client, this.id, messageID, reaction) + } + + async createMessage(content: MessageContent, file?: FileContent | FileContent[]) { + return await this.client.createMessage.call(this.client, this.id, content, file) + } + + async deleteMessage(messageID: BigString, reason?: string): Promise { + return await this.client.deleteMessage.call(this.client, this.id, messageID, reason) + } + + async deleteMessages(messageIDs: BigString[], reason?: string): Promise { + return await this.client.deleteMessages.call(this.client, this.id, messageIDs, reason) + } + + async editMessage(messageID: BigString, content: MessageContentEdit) { + return await this.client.editMessage.call(this.client, this.id, messageID, content) + } + + async getMembers(): Promise { + return await this.client.getThreadMembers.call(this.client, this.id) + } + + async getMessage(messageID: BigString): Promise { + return await this.client.getMessage.call(this.client, this.id, messageID) + } + + async getMessageReaction(messageID: BigString, reaction: string, options?: GetMessageReactionOptions): Promise { + return await this.client.getMessageReaction.call(this.client, this.id, messageID, reaction, options) + } + + async getMessages(options: GetMessagesOptions) { + return await this.client.getMessages.call(this.client, this.id, options) + } + + async getPins(): Promise { + return await this.client.getPins.call(this.client, this.id) + } + + async join(userID: BigString = '@me'): Promise { + return await this.client.joinThread.call(this.client, this.id, userID) + } + + async leave(userID: BigString): Promise { + return await this.client.leaveThread.call(this.client, this.id, userID) + } + + async pinMessage(messageID: BigString): Promise { + return await this.client.pinMessage.call(this.client, this.id, messageID) + } + + async purge(options: PurgeChannelOptions): Promise { + return await this.client.purgeChannel.call(this.client, this.id, options) + } + + async removeMessageReaction(messageID: BigString, reaction: string, userID: BigString = '@me') { + return await this.client.removeMessageReaction.call(this.client, this.id, messageID, reaction, userID) + } + + async removeMessageReactionEmoji(messageID: BigString, reaction: string): Promise { + return await this.client.removeMessageReactionEmoji.call(this.client, this.id, messageID, reaction) + } + + async removeMessageReactions(messageID: BigString): Promise { + return await this.client.removeMessageReactions.call(this.client, this.id, messageID) + } + + async sendTyping(): Promise { + return await this.client.sendChannelTyping.call(this.client, this.id) + } + + async unpinMessage(messageID: BigString): Promise { + return await this.client.unpinMessage.call(this.client, this.id, messageID) + } + + /** + * @deprecated Use deleteMessage instead + */ + async unsendMessage(messageID: BigString): Promise { + return await this.client.deleteMessage.call(this.client, this.id, messageID) + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'lastMessageID', + 'memberCount', + 'messageCount', + 'messages', + 'ownerID', + 'rateLimitPerUser', + 'threadMetadata', + 'member', + ...props, + ]) + } +} + +export default ThreadChannel diff --git a/packages/client/src/Structures/guilds/AuditLogEntry.ts b/packages/client/src/Structures/guilds/AuditLogEntry.ts new file mode 100644 index 000000000..87a5da161 --- /dev/null +++ b/packages/client/src/Structures/guilds/AuditLogEntry.ts @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DiscordAuditLogChange, DiscordAuditLogEntry } from '@discordeno/types' +import { AuditLogEvents } from '@discordeno/types' +import Base from '../../Base.js' +import type GuildChannel from '../channels/Guild.js' +import type TextChannel from '../channels/Text.js' +import Invite from '../Invite.js' +import type Message from '../Message.js' +import type User from '../users/User.js' +import type Guild from './Guild.js' +import type Member from './Member.js' +import type Role from './Role.js' + +export class GuildAuditLogEntry extends Base { + /** The guild to which this entry belongs. */ + guild: Guild + /** The action type of the entry. */ + actionType: AuditLogEvents + /** The reason for the action. */ + reason: string | null + /** The user that performed the action. */ + user?: User + /** The properties of the targeted object before the action was taken. For example, if a channel was renamed from #general to #potato, this would be `{name: "general"}`` */ + before: Record + + /** The properties of the targeted object after the action was taken. For example, if a channel was renamed from #general to #potato, this would be `{name: "potato"}`` */ + after: Record + + /** The ID of the action target */ + targetID?: string + /** The number of entities targeted. For example, for action type 26 (MEMBER_MOVE), this is the number of members that were moved/disconnected from the voice channel */ + count?: number + /** The channel targeted in the entry, action types 26 (MEMBER_MOVE), 72/74/75 (MESSAGE_DELETE/PIN/UNPIN) and 83/84/85 (STAGE_INSTANCE_CREATE/UPDATE/DELETE) only */ + channel?: GuildChannel + /** The message that was (un)pinned, action types 74/75 (MESSAGE_PIN/UNPIN) only. If the message is not cached, this will be an object with an `id` key. No other property is guaranteed. */ + message?: Message | { id: string } + /** The number of days of inactivity to prune for, action type 21 (MEMBER_PRUNE) only */ + deleteMemberDays?: number + /** The number of members pruned from the server, action type 21 (MEMBER_PRUNE) only */ + membersRemoved?: number + /** The member described by the permission overwrite, action types 13-15 (CHANNEL\_OVERWRITE\_CREATE/UPDATE/DELETE) only. If the member is not cached, this could be {id: String} */ + member?: Member | { id: string } + /** The role described by the permission overwrite, action types 13-15 (CHANNEL\_OVERWRITE\_CREATE/UPDATE/DELETE) only. If the role is not cached, this could be {id: String, name: String} */ + role?: Role | { id: string; name: string } + + constructor(data: DiscordAuditLogEntry, guild: Guild) { + super(data.id) + + this.guild = guild + this.actionType = data.action_type + this.reason = data.reason ?? null + this.user = data.user_id ? guild.client.users.get(data.user_id) : undefined + this.before = {} as any + this.after = {} as any + if (data.changes) { + data.changes.forEach((change) => { + if (change.old_value !== undefined) { + this.before[change.key] = change.old_value + } + if (change.new_value !== undefined) { + this.after[change.key] = change.new_value + } + }) + } + + if (data.target_id) { + this.targetID = data.target_id + } + if (data.options) { + if (data.options.count) { + this.count = +data.options.count + } + if (data.options.channel_id) { + if (this.actionType >= 83) { + this.channel = guild.threads.get(data.options.channel_id) + } else { + this.channel = guild.channels.get(data.options.channel_id) + } + if (data.options.message_id) { + this.message = (this.channel && (this.channel as TextChannel).messages.get(data.options.message_id)) ?? { + id: data.options.message_id, + } + } + } + if (data.options.delete_member_days) { + this.deleteMemberDays = +data.options.delete_member_days + this.membersRemoved = +data.options.members_removed + } + if (data.options.type) { + if (data.options.type === '1') { + this.member = guild.members.get(data.options.id) ?? { + id: data.options.id, + } + } else if (data.options.type === '0') { + this.role = guild.roles.get(data.options.id) ?? { + id: data.options.id, + name: data.options.role_name, + } + } + } + } + } + + get target() { + // pay more, get less + if (this.actionType < 10) { + // Guild + return this.guild + } else if (this.actionType < 20) { + // Channel + return this.guild?.channels.get(this.targetID!) + } else if (this.actionType < 30) { + // Member + if (this.actionType === AuditLogEvents.MemberMove || this.actionType === AuditLogEvents.MemberDisconnect) { + // MEMBER_MOVE / MEMBER_DISCONNECT + return null + } + return this.guild?.members.get(this.targetID!) + } else if (this.actionType < 40) { + // Role + return this.guild?.roles.get(this.targetID!) + } else if (this.actionType < 50) { + // Invite + const changes = this.actionType === 42 ? this.before : this.after // Apparently the meaning of life is a deleted invite + return new Invite( + { + code: changes.code as string, + // @ts-expect-error idk why this is happening + channel: changes.channel, + guild: this.guild.toJSON(), + uses: changes.uses as number, + max_uses: changes.max_uses as number, + max_age: changes.max_age as number, + temporary: changes.temporary as boolean, + }, + this.guild?.client, + ) + } else if (this.actionType < 60) { + // Webhook + return null // Go get the webhook yourself + } else if (this.actionType < 70) { + // Emoji + return this.guild?.emojis?.find((emoji) => emoji.id === this.targetID) + } else if (this.actionType < 80) { + // Message + return this.guild?.client.users.get(this.targetID!) + } else if (this.actionType < 83) { + // Integrations + return null + } else if (this.actionType < 90) { + // Stage Instances + return this.guild?.threads.get(this.targetID!) + } else if (this.actionType < 100) { + // Sticker + return this.guild?.stickers?.find((sticker) => sticker.id === this.targetID) + } else { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + throw new Error('Unrecognized action type: ' + this.actionType) + } + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'actionType', + 'after', + 'before', + 'channel', + 'count', + 'deleteMemberDays', + 'member', + 'membersRemoved', + 'reason', + 'role', + 'targetID', + 'user', + ...props, + ]) + } +} + +export default GuildAuditLogEntry diff --git a/packages/client/src/Structures/guilds/Guild.ts b/packages/client/src/Structures/guilds/Guild.ts new file mode 100644 index 000000000..98e687b9a --- /dev/null +++ b/packages/client/src/Structures/guilds/Guild.ts @@ -0,0 +1,946 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +import { + BitwisePermissionFlags, + ChannelTypes, + type ApplicationCommandTypes, + type BigString, + type DefaultMessageNotificationLevels, + type DiscordEmoji, + type DiscordGuild, + type DiscordMemberWithUser, + type DiscordSticker, + type ExplicitContentFilterLevels, + type GuildFeatures, + type GuildNsfwLevel, + type MfaLevels, + type PremiumTiers, + type SystemChannelFlags, + type VerificationLevels, +} from '@discordeno/types' +import Base from '../../Base.js' +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 { + AnyGuildChannel, + AnyThreadChannel, + ApplicationCommand, + ApplicationCommandPermissions, + ApplicationCommandStructure, + ChannelPosition, + CreateChannelOptions, + CreateStickerOptions, + DiscoveryMetadata, + DiscoveryOptions, + DiscoverySubcategoryResponse, + EditStickerOptions, + Emoji, + EmojiOptions, + GetGuildAuditLogOptions, + GetGuildBansOptions, + GetPruneOptions, + GetRESTGuildMembersOptions, + GuildApplicationCommandPermissions, + GuildAuditLog, + GuildBan, + GuildOptions, + GuildTemplateOptions, + GuildVanity, + IntegrationOptions, + ListedGuildThreads, + MemberOptions, + PruneMemberOptions, + RoleOptions, + Sticker, + VoiceRegion, + VoiceStateOptions, + Webhook, + WelcomeScreen, + WelcomeScreenOptions, + Widget, + WidgetData, +} from '../../typings.js' +import type CategoryChannel from '../channels/Category.js' +import Channel from '../channels/Channel.js' +import type GuildChannel from '../channels/Guild.js' +import type StageChannel from '../channels/Stage.js' +import type TextChannel from '../channels/Text.js' +import type TextVoiceChannel from '../channels/TextVoice.js' +import type ThreadChannel from '../channels/threads/Thread.js' +import type VoiceChannel from '../channels/Voice.js' +import type Invite from '../Invite.js' +import Permission from '../Permission.js' +import User from '../users/User.js' +import type GuildIntegration from './Integration.js' +import Member from './Member.js' +import Role from './Role.js' +import StageInstance from './StageInstance.js' +import type GuildTemplate from './Template.js' +import type { VoiceState } from './VoiceState.js' + +export class Guild extends Base { + /** The client object */ + client: Client + /** The id of the guild owner. */ + ownerID: BigString + /** The id of the application. */ + applicationID?: BigString | null + /** The id of the widget channel. */ + widgetChannelID?: BigString | null + /** The afk channel id if one is set. */ + afkChannelID?: BigString | null + /** The system channel id if one is set. */ + systemChannelID?: BigString | null + /** The public updates channel id if one is set. */ + publicUpdatesChannelID?: BigString | null + /** The rules channel id if one is set. */ + rulesChannelID?: BigString | null + /** The name of the guild. */ + name?: string + /** The description of the guild. */ + description?: string | null + /** The vanity url if one is set. */ + vanityURL?: string | null + /** The preferred locale of the server. */ + preferredLocale?: string + /** The system channel flags. */ + systemChannelFlags?: SystemChannelFlags + /** The verification level of the guild. */ + verificationLevel?: VerificationLevels + /** The default notification level. */ + defaultNotifications?: DefaultMessageNotificationLevels + /** The explicit content filter setting for this guild. */ + explicitContentFilter?: ExplicitContentFilterLevels + /** Array of guild features */ + features: GuildFeatures[] = [] + /** The premium tier of the guild. */ + premiumTier?: PremiumTiers + /** The MFA level of the guild. */ + mfaLevel?: MfaLevels + /** The NSFW level of the guild. */ + nsfwLevel?: GuildNsfwLevel + /** The compressed form of the guild splash image. */ + _splash?: bigint + /** The compressed form of the guild's discovery splash image. */ + _discoverySplash?: bigint + /** The compressed form of the guild's banner image. */ + _banner?: bigint + /** The compressed form of the guild's icon image. */ + _icon?: bigint + /** The cached emojis in the guild. */ + emojis?: DiscordEmoji[] + /** The cached stickers in the guild. */ + stickers?: DiscordSticker[] + /** The afk timeout in seconds. */ + afkTimeout?: number + /** When this guild was joined at. */ + joinedAt: number + /** The amount of members in the guild. */ + memberCount?: number + /** The approximate member count in the guild. */ + approximateMemberCount?: number + /** The approximate presence count in the guild. */ + approximatePresenceCount?: number + /** The amount of subscribers to the server. */ + premiumSubscriptionCount?: number + /** The maximum amount of presences that can be in a guild. */ + maxPresences?: number | null + /** The maximum amount of members that can be in the guild. */ + maxMembers?: number + /** The maximum amount of members that can be in a video channel. */ + maxVideoChannelUsers?: number | null + /** Whether or not this guild is unavailable. */ + unavailable: boolean + /** Whether or not the widget is enabled in this guild. */ + widgetEnabled: boolean + /** Whether or not this guild is considered large. */ + large?: boolean + /** Whether or not the premium progress bar is enabled. */ + premiumProgressBarEnabled?: boolean + /** Whether or not this server is nsfw. */ + nsfw?: boolean + /** The welcome screen settings. */ + welcomeScreen?: { + description: string | null + welcomeChannels?: Array<{ + channelID: string + description: string + emojiID: string | null + emojiName: string | null + }> + } + + /** The cached members in this guild. */ + members = new Collection() + /** The cached roles in this guild. */ + roles = new Collection() + /** The cached channels in this guild. */ + channels = new Collection() + /** The cached threads in this guild. */ + threads = new Collection() + /** The cached voice states in this guild. */ + voiceStates = new Collection() + /** The cached stage instances in this guild. */ + stageInstances = new Collection() + + constructor(data: DiscordGuild, client: Client) { + super(data.id) + this.client = client + this.ownerID = data.owner_id + + this.unavailable = !!data.unavailable + this.joinedAt = Date.parse(data.joined_at!) + this.memberCount = data.member_count + this.applicationID = data.application_id + this.widgetEnabled = !!data.widget_enabled + + if (data.widget_channel_id !== undefined) { + this.widgetChannelID = data.widget_channel_id + } + + if (data.approximate_member_count !== undefined) { + this.approximateMemberCount = data.approximate_member_count + } + if (data.approximate_presence_count !== undefined) { + this.approximatePresenceCount = data.approximate_presence_count + } + + if (data.roles) { + for (const r of data.roles) { + const role = new Role(r, this) + this.roles.set(role.id, role) + } + } + + if (data.channels) { + for (const channelData of data.channels) { + channelData.guild_id = this.id.toString() + const channel = Channel.from(channelData, client) as GuildChannel + this.channels.set(channel.id, channel) + client._channelGuildMap.set(channel.id, this.id) + } + } + + if (data.threads) { + for (const threadData of data.threads) { + threadData.guild_id = this.id.toString() + const thread = Channel.from(threadData, client) as ThreadChannel + this.threads.set(thread.id, thread) + client._threadGuildMap.set(thread.id, this.id) + } + } + + if (data.members) { + for (const m of data.members) { + const member = new Member(m as DiscordMemberWithUser, this, client) + this.members.set(member.id, member) + } + } + + if (data.stage_instances) { + for (const stageInstance of data.stage_instances) { + stageInstance.guild_id = this.id + + const instance = new StageInstance(stageInstance, client) + this.stageInstances.set(instance.id, instance) + } + } + + if (data.presences) { + for (const presence of data.presences) { + if (presence.user?.id) { + const cached = this.client.users.get(presence.user.id) + if (cached) cached.update(presence.user) + else { + const user = new User(presence.user, this.client) + this.client.users.set(user.id, user) + } + } + } + } + + if (data.voice_states) { + for (const voiceState of data.voice_states) { + if (!this.members.get(voiceState.user_id)) continue + + if (voiceState.member) { + const member = new Member(voiceState.member, this, client) + this.members.set(member.id, member) + const user = new User(voiceState.member.user, client) + this.client.users.set(user.id, user) + + // TODO: check channel type maybe voice channel? + ;(this.channels.get(voiceState.channel_id!) as VoiceChannel)?.voiceMembers.set(member.id, member) + } + + // TODO: voice support + // if ( + // client.options.seedVoiceConnections && + // voiceState.user_id === client.id && + // !client.voiceConnections.get(this.id) + // ) { + // process.nextTick(() => + // this.client.joinVoiceChannel(voiceState.channel_id) + // ); + // } + } + } + this.update(data) + } + + /** + * @deprecated - please use .client + */ + get _client() { + return this.client + } + + update(data: DiscordGuild) { + if (data.name !== undefined) { + this.name = data.name + } + if (data.verification_level !== undefined) { + this.verificationLevel = data.verification_level + } + if (data.splash !== undefined) { + this._splash = data.splash ? this.client.iconHashToBigInt(data.splash) : undefined + } + + if (data.discovery_splash !== undefined) { + this._discoverySplash = data.discovery_splash ? this.client.iconHashToBigInt(data.discovery_splash) : undefined + } + + if (data.banner !== undefined) { + this._banner = data.banner ? this.client.iconHashToBigInt(data.banner) : undefined + } + + if (data.owner_id !== undefined) { + this.ownerID = data.owner_id + } + + if (data.icon !== undefined) { + this._icon = data.icon ? this.client.iconHashToBigInt(data.icon) : undefined + } + + // TODO: compress features. + if (data.features !== undefined) { + this.features = data.features + } + + if (data.emojis !== undefined) { + this.emojis = data.emojis + } + + if (data.stickers !== undefined) { + this.stickers = data.stickers + } + + if (data.afk_channel_id !== undefined) { + this.afkChannelID = data.afk_channel_id + } + + if (data.afk_timeout !== undefined) { + this.afkTimeout = data.afk_timeout + } + + if (data.default_message_notifications !== undefined) { + this.defaultNotifications = data.default_message_notifications + } + + if (data.mfa_level !== undefined) { + this.mfaLevel = data.mfa_level + } + + if (data.large !== undefined) { + this.large = data.large + } + + if (data.max_presences !== undefined) { + this.maxPresences = data.max_presences + } + + if (data.explicit_content_filter !== undefined) { + this.explicitContentFilter = data.explicit_content_filter + } + + if (data.system_channel_id !== undefined) { + this.systemChannelID = data.system_channel_id + } + + if (data.system_channel_flags !== undefined) { + this.systemChannelFlags = data.system_channel_flags + } + if (data.premium_progress_bar_enabled !== undefined) { + this.premiumProgressBarEnabled = data.premium_progress_bar_enabled + } + if (data.premium_tier !== undefined) { + this.premiumTier = data.premium_tier + } + if (data.premium_subscription_count !== undefined) { + this.premiumSubscriptionCount = data.premium_subscription_count + } + if (data.vanity_url_code !== undefined) { + this.vanityURL = data.vanity_url_code + } + if (data.preferred_locale !== undefined) { + this.preferredLocale = data.preferred_locale + } + if (data.description !== undefined) { + this.description = data.description + } + if (data.max_members !== undefined) { + this.maxMembers = data.max_members + } + if (data.public_updates_channel_id !== undefined) { + this.publicUpdatesChannelID = data.public_updates_channel_id + } + if (data.rules_channel_id !== undefined) { + this.rulesChannelID = data.rules_channel_id + } + if (data.max_video_channel_users !== undefined) { + this.maxVideoChannelUsers = data.max_video_channel_users + } + if (data.welcome_screen !== undefined) { + this.welcomeScreen = { + description: data.welcome_screen.description, + welcomeChannels: data.welcome_screen.welcome_channels?.map((c) => { + return { + channelID: c.channel_id, + description: c.description, + emojiID: c.emoji_id, + emojiName: c.emoji_name, + } + }), + } + } + // if (data.nsfw !== undefined) { + // this.nsfw = data.nsfw; + // } + if (data.nsfw_level !== undefined) { + this.nsfwLevel = data.nsfw_level + } + } + + get banner(): string | undefined { + return this._banner ? this.client.iconBigintToHash(this._banner) : undefined + } + + get bannerURL(): string | null { + return this.banner ? this.client._formatImage(BANNER(this.id, this.banner)) : null + } + + get icon(): string | undefined { + return this._icon ? this.client.iconBigintToHash(this._icon) : undefined + } + + get iconURL(): string | null { + return this.icon ? this.client._formatImage(GUILD_ICON(this.id, this.icon)) : null + } + + get splash(): string | undefined { + return this._splash ? this.client.iconBigintToHash(this._splash) : undefined + } + + get splashURL(): string | null { + return this.splash ? this.client._formatImage(GUILD_SPLASH(this.id, this.splash)) : null + } + + get discoverySplash(): string | undefined { + return this._discoverySplash ? this.client.iconBigintToHash(this._discoverySplash) : undefined + } + + get discoverySplashURL(): string | null { + return this.discoverySplash ? this.client._formatImage(GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash)) : null + } + + /** Add a discovery subcategory */ + async addDiscoverySubcategory(categoryID: BigString, reason?: string): Promise { + return await this.client.addGuildDiscoverySubcategory.call(this.client, this.id, categoryID, reason) + } + + /** Add a role to a guild member */ + async addMemberRole(memberID: BigString, roleID: BigString, reason?: string): Promise { + return await this.client.addGuildMemberRole.call(this.client, this.id, memberID, roleID, reason) + } + + /** Ban a user from the guild */ + async banMember(userID: BigString, deleteMessageDays = 0, reason?: string): Promise { + return await this.client.banGuildMember.call(this.client, this.id, userID, deleteMessageDays, reason) + } + + /** Bulk create/edit guild application commands */ + async bulkEditCommands(commands: Array>): Promise>> { + return await this.client.bulkEditGuildCommands.call(this.client, this.id, commands) + } + + /** Create a channel in the guild */ + async createChannel( + name: string, + type = ChannelTypes.GuildText, + options: CreateChannelOptions, + ): Promise { + return await this.client.createChannel.call(this.client, this.id, name, type as number, options) + } + + /** Create a guild application command */ + async createCommand(command: ApplicationCommandStructure): Promise> { + return await this.client.createGuildCommand.call(this.client, this.id, command) + } + + /** Create a emoji in the guild */ + async createEmoji(options: EmojiOptions, reason?: string): Promise { + return await this.client.createGuildEmoji.call(this.client, this.id, options, reason) + } + + /** Create a guild role */ + async createRole(options: Role | RoleOptions, reason?: string): Promise { + return await this.client.createRole.call(this.client, this.id, options, reason) + } + + /** Create a guild sticker */ + async createSticker(options: CreateStickerOptions, reason?: string): Promise { + return await this.client.createGuildSticker.call(this.client, this.id, options, reason) + } + + /** Create a template for this guild */ + async createTemplate(name: string, description?: string): Promise { + return await this.client.createGuildTemplate.call(this.client, this.id, name, description) + } + + /** Delete the guild (bot user must be owner) */ + async delete(): Promise { + if (this.ownerID !== this.client.id) throw new Error('To delete a guild, the bot must be the owner of the guild.') + + return await this.client.deleteGuild.call(this.client, this.id) + } + + /** Delete a guild application command */ + async deleteCommand(commandID: BigString): Promise { + return await this.client.deleteGuildCommand.call(this.client, this.id, commandID) + } + + /** Delete a discovery subcategory */ + async deleteDiscoverySubcategory(categoryID: BigString, reason?: string): Promise { + return await this.client.deleteGuildDiscoverySubcategory.call(this.client, this.id, categoryID, reason) + } + + /** Delete a emoji in the guild */ + async deleteEmoji(emojiID: BigString, reason?: string): Promise { + return await this.client.deleteGuildEmoji.call(this.client, this.id, emojiID, reason) + } + + /** Delete a guild integration */ + async deleteIntegration(integrationID: BigString): Promise { + return await this.client.deleteGuildIntegration.call(this.client, this.id, integrationID) + } + + /** Delete a role */ + async deleteRole(roleID: BigString, reason?: string): Promise { + return await this.client.deleteRole.call(this.client, this.id, roleID, reason) + } + + /** Delete a guild sticker */ + async deleteSticker(stickerID: BigString, reason?: string): Promise { + return await this.client.deleteGuildSticker.call(this.client, this.id, stickerID, reason) + } + + /** Delete a guild template */ + async deleteTemplate(code: string): Promise { + return await this.client.deleteGuildTemplate.call(this.client, this.id, code) + } + + /** Get the guild's banner with the given format and size */ + dynamicBannerURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.banner ? this.client._formatImage(BANNER(this.id, this.banner), format, size) : null + } + + /** Get the guild's discovery splash with the given format and size */ + dynamicDiscoverySplashURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.discoverySplash ? this.client._formatImage(GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash), format, size) : null + } + + /** Get the guild's icon with the given format and size */ + dynamicIconURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.icon ? this.client._formatImage(GUILD_ICON(this.id, this.icon), format, size) : null + } + + /** Get the guild's splash with the given format and size */ + dynamicSplashURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.splash ? this.client._formatImage(GUILD_SPLASH(this.id, this.splash), format, size) : null + } + + /** Edit the guild */ + async edit(options: GuildOptions, reason?: string): Promise { + return await this.client.editGuild.call(this.client, this.id, options, reason) + } + + /** Edit multiple channels' positions. Note that channel position numbers are grouped by type (category, text, voice), then sorted in ascending order (lowest number is on top). */ + async editChannelPositions(channelPositions: ChannelPosition[]): Promise { + return await this.client.editChannelPositions.call(this.client, this.id, channelPositions) + } + + /** Edit a guild application command */ + async editCommand(commandID: BigString, commands: ApplicationCommandStructure): Promise> { + return await this.client.editGuildCommand.call(this.client, this.id, commandID, commands) + } + + /** + * Edits command permissions for a specific command in a guild. + * Note: You can only add up to 10 permission overwrites for a command. + */ + async editCommandPermissions(commandID: BigString, permissions: ApplicationCommandPermissions[]): Promise { + return await this.client.editCommandPermissions.call(this.client, this.id, commandID, permissions) + } + + /** Edit the guild's discovery data */ + async editDiscovery(options: DiscoveryOptions): Promise { + return await this.client.editGuildDiscovery.call(this.client, this.id, options) + } + + /** + * Edit a emoji in the guild + * @arg {String} emojiID The ID of the emoji you want to modify + * @arg {Object} options Emoji options + * @arg {String} [options.name] The name of emoji + * @arg {Array} [options.roles] An array containing authorized role IDs + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} A guild emoji object + */ + async editEmoji( + emojiID: BigString, + options: { + name?: string | undefined + roles?: string[] | undefined + }, + reason?: string, + ): Promise { + return await this.client.editGuildEmoji.call(this.client, this.id, emojiID, options, reason) + } + + /** Edit a guild integration */ + async editIntegration(integrationID: BigString, options: IntegrationOptions): Promise { + return await this.client.editGuildIntegration.call(this.client, this.id, integrationID, options) + } + + /** Edit a guild member */ + async editMember(memberID: BigString, options: MemberOptions, reason?: string): Promise { + return await this.client.editGuildMember.call(this.client, this.id, memberID, options, reason) + } + + /** Edit the guild role */ + async editRole(roleID: BigString, options: RoleOptions, reason?: string): Promise { + return await this.client.editRole.call(this.client, this.id, roleID, options, reason) + } + + /** Edit a guild sticker */ + async editSticker(stickerID: BigString, options: EditStickerOptions, reason?: string): Promise { + return await this.client.editGuildSticker.call(this.client, this.id, stickerID, options, reason) + } + + /** Edit a guild template */ + async editTemplate(code: string, options: GuildTemplateOptions): Promise { + return await this.client.editGuildTemplate.call(this.client, this.id, code, options) + } + + /** Modify the guild's vanity code */ + async editVanity(code: string | null): Promise { + return await this.client.editGuildVanity.call(this.client, this.id, code) + } + + /** Update a user's voice state - See [caveats](https://discord.com/developers/docs/resources/guild#modify-user-voice-state-caveats) */ + async editVoiceState(options: VoiceStateOptions, userID: BigString = '@me'): Promise { + return await this.client.editGuildVoiceState.call(this.client, this.id, options, userID) + } + + /** Edit the guild welcome screen */ + async editWelcomeScreen(options: WelcomeScreenOptions): Promise { + return await this.client.editGuildWelcomeScreen.call(this.client, this.id, options) + } + + /** Modify a guild's widget */ + async editWidget(options: Widget): Promise { + return await this.client.editGuildWidget.call(this.client, this.id, options) + } + + /** Request all guild members from Discord */ + async fetchAllMembers(timeout?: number): Promise { + return await this.fetchMembers({ + guildId: this.id, + limit: 0, + }).then((m: any[]) => m.length) + } + + /** Request specific guild members through the gateway connection */ + async fetchMembers(options: RequestGuildMembers): Promise { + // TODO: Use gateway fetch + return await this.client.getRESTGuildMembers(this.id, options) + } + + /** Get all active threads in this guild */ + async getActiveThreads(): Promise> { + return await this.client.getActiveGuildThreads.call(this.client, this.id) + } + + /** Get the audit log for the guild */ + async getAuditLog(options: GetGuildAuditLogOptions): Promise { + return await this.client.getGuildAuditLog.call(this.client, this.id, options) + } + + /** Get a ban from the ban list of a guild */ + async getBan(userID: BigString): Promise { + return await this.client.getGuildBan.call(this.client, this.id, userID) + } + + /** Get the ban list of the guild */ + async getBans(options: GetGuildBansOptions): Promise { + return await this.client.getGuildBans.call(this.client, this.id, options) + } + + /** Get a guild application command */ + async getCommand(commandID: BigString): Promise> { + return await this.client.getGuildCommand.call(this.client, this.id, commandID) + } + + /** Get the a guild's application command permissions */ + async getCommandPermissions(commandID: BigString): Promise { + return await this.client.getCommandPermissions.call(this.client, this.id, commandID) + } + + /** Get the guild's application commands */ + async getCommands(): Promise> { + return await this.client.getGuildCommands.call(this.client, this.id) + } + + /** Get the guild's discovery object */ + async getDiscovery(): Promise { + return await this.client.getGuildDiscovery.call(this.client, this.id) + } + + /** Get the all of a guild's application command permissions */ + async getGuildCommandPermissions(): Promise { + return await this.client.getGuildCommandPermissions.call(this.client, this.id) + } + + /** Get a list of integrations for the guild */ + async getIntegrations(): Promise { + return await this.client.getGuildIntegrations.call(this.client, this.id) + } + + /** Get all invites in the guild */ + async getInvites(): Promise { + return await this.client.getGuildInvites.call(this.client, this.id) + } + + /** Get the prune count for the guild */ + async getPruneCount(options: GetPruneOptions): Promise { + return await this.client.getPruneCount.call(this.client, this.id, options) + } + + /** Get a guild's channels via the REST API. REST mode is required to use this endpoint. */ + async getRESTChannels(): Promise { + return await this.client.getRESTGuildChannels.call(this.client, this.id) + } + + /** Get a guild emoji via the REST API. REST mode is required to use this endpoint. */ + async getRESTEmoji(emojiID: BigString): Promise { + return await this.client.getRESTGuildEmoji.call(this.client, this.id, emojiID) + } + + /** Get a guild's emojis via the REST API. REST mode is required to use this endpoint. */ + async getRESTEmojis(): Promise { + return await this.client.getRESTGuildEmojis.call(this.client, this.id) + } + + /** Get a guild's members via the REST API. REST mode is required to use this endpoint. */ + async getRESTMember(memberID: BigString): Promise { + return await this.client.getRESTGuildMember.call(this.client, this.id, memberID) + } + + /** Get a guild's members via the REST API. REST mode is required to use this endpoint. */ + async getRESTMembers(options?: GetRESTGuildMembersOptions): Promise { + return await this.client.getRESTGuildMembers.call(this.client, this.id, options) + } + + /** Get a guild's roles via the REST API. REST mode is required to use this endpoint. */ + async getRESTRoles(): Promise { + return await this.client.getRESTGuildRoles.call(this.client, this.id) + } + + /** Get a guild sticker via the REST API. REST mode is required to use this endpoint. */ + async getRESTSticker(stickerID: BigString): Promise { + return await this.client.getRESTGuildSticker.call(this.client, this.id, stickerID) + } + + /** Get a guild's stickers via the REST API. REST mode is required to use this endpoint. */ + async getRESTStickers(): Promise { + return await this.client.getRESTGuildStickers.call(this.client, this.id) + } + + /** Get the guild's templates */ + async getTemplates(): Promise { + return await this.client.getGuildTemplates.call(this.client, this.id) + } + + /** Returns the vanity url of the guild */ + async getVanity(): Promise { + return await this.client.getGuildVanity.call(this.client, this.id) + } + + /** Get possible voice regions for a guild */ + async getVoiceRegions(): Promise { + return await this.client.getVoiceRegions.call(this.client, this.id) + } + + /** Get all the webhooks in the guild */ + async getWebhooks(): Promise { + return await this.client.getGuildWebhooks.call(this.client, this.id) + } + + /** Get the welcome screen of the Community guild, shown to new members */ + async getWelcomeScreen(): Promise { + return await this.client.getGuildWelcomeScreen.call(this.client, this.id) + } + + /** Get a guild's widget object */ + async getWidget(): Promise { + return await this.client.getGuildWidget.call(this.client, this.id) + } + + /** Get a guild's widget settings object */ + async getWidgetSettings(): Promise { + return await this.client.getGuildWidgetSettings.call(this.client, this.id) + } + + /** Kick a member from the guild */ + async kickMember(userID: BigString, reason?: string): Promise { + return await this.client.kickGuildMember.call(this.client, this.id, userID, reason) + } + + /** Leave the guild */ + async leave(): Promise { + return await this.client.leaveGuild.call(this.client, this.id) + } + + // TODO: gateway voice + // /** Leaves the voice channel in this guild */ + // async leaveVoiceChannel(): Promise { + // return await this.client.closeVoiceConnection.call(this.client, this.id); + // } + + /** Get the guild permissions of a member */ + permissionsOf(memberID: BigString | Member): Permission { + const member = ['string', 'bigint'].includes(typeof memberID) ? this.members.get(memberID as BigString)! : (memberID as Member) + if (member.id === this.ownerID) { + return new Permission(BitwisePermissionFlags.ADMINISTRATOR) + } else { + let permissions = this.roles.get(this.id)!.permissions.allow + if (permissions & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) { + return new Permission(BitwisePermissionFlags.ADMINISTRATOR) + } + for (const id of member.roles) { + const role = this.roles.get(id) + if (!role) { + continue + } + + const { allow: perm } = role.permissions + if (perm & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) { + permissions = BigInt(BitwisePermissionFlags.ADMINISTRATOR) + break + } else { + permissions |= perm + } + } + return new Permission(permissions) + } + } + + /** Begin pruning the guild */ + async pruneMembers(options?: PruneMemberOptions): Promise { + return await this.client.pruneMembers.call(this.client, this.id, options) + } + + /** Remove a role from a guild member */ + async removeMemberRole(memberID: BigString, roleID: BigString, reason?: string): Promise { + return await this.client.removeGuildMemberRole.call(this.client, this.id, memberID, roleID, reason) + } + + /** Search for guild members by partial nickname/username */ + async searchMembers(query: string, limit = 1): Promise { + return await this.client.searchGuildMembers.call(this.client, this.id, query, limit) + } + + /** Force a guild integration to sync */ + async syncIntegration(integrationID: BigString): Promise { + return await this.client.syncGuildIntegration.call(this.client, this.id, integrationID) + } + + /** Force a guild template to sync */ + async syncTemplate(code: string): Promise { + return await this.client.syncGuildTemplate.call(this.client, this.id, code) + } + + /** Unban a user from the guild */ + async unbanMember(userID: BigString, reason?: string): Promise { + return await this.client.unbanGuildMember.call(this.client, this.id, userID, reason) + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'afkChannelID', + 'afkTimeout', + 'applicationID', + 'approximateMemberCount', + 'approximatePresenceCount', + 'autoRemoved', + 'banner', + 'categories', + 'channels', + 'defaultNotifications', + 'description', + 'discoverySplash', + 'emojiCount', + 'emojis', + 'explicitContentFilter', + 'features', + 'icon', + 'joinedAt', + 'keywords', + 'large', + 'maxMembers', + 'maxPresences', + 'maxVideoChannelUsers', + 'memberCount', + 'members', + 'mfaLevel', + 'name', + 'ownerID', + 'pendingVoiceStates', + 'preferredLocale', + 'premiumProgressBarEnabled', + 'premiumSubscriptionCount', + 'premiumTier', + 'primaryCategory', + 'primaryCategoryID', + 'publicUpdatesChannelID', + 'roles', + 'rulesChannelID', + 'splash', + 'stickers', + 'systemChannelFlags', + 'systemChannelID', + 'unavailable', + 'vanityURL', + 'verificationLevel', + 'voiceStates', + 'welcomeScreen', + 'widgetChannelID', + 'widgetEnabled', + ...props, + ]) + } +} + +export default Guild diff --git a/packages/client/src/Structures/guilds/Integration.ts b/packages/client/src/Structures/guilds/Integration.ts new file mode 100644 index 000000000..1eed1873d --- /dev/null +++ b/packages/client/src/Structures/guilds/Integration.ts @@ -0,0 +1,131 @@ +/* eslint-disable no-useless-call */ +import type { DiscordIntegration, DiscordIntegrationApplication } from '@discordeno/types' +import Base from '../../Base.js' +import type { IntegrationOptions } from '../../typings.js' +import User from '../users/User.js' +import type Guild from './Guild.js' + +export class GuildIntegration extends Base { + /** The guild where this integration exists. */ + guild: Guild + /** The name of the integration. */ + name: string + /** The type of integration. */ + type: string + /** The user connected to the integration. */ + user?: User + /** Whether the integration is syncing or not. */ + syncing?: boolean + /** THe Unix timestamp of last integration sync. */ + syncedAt?: number + /** The number of subscribers. */ + subscriberCount?: number + /** The role id of the role connected to the integration. */ + roleID?: string + /** WHether or not the application was revoked. */ + revoked?: boolean + /** Whether integration emoticons are enabled or not. */ + enableEmoticons?: boolean + /** Behavior of expired subscriptions */ + expireBehavior?: number + /** Grace period for expired subscriptions. */ + expireGracePeriod?: number + /** Whether the integration is enabled or not. */ + enabled?: boolean + /** The bot/oauth2 application for integration. */ + application?: DiscordIntegrationApplication + + /** Info on the integration account */ + account: { + /** The id of the integration account. */ + id: string + /** The name of the integration account. */ + name: string + } + + constructor(data: DiscordIntegration, guild: Guild) { + super(data.id) + + this.guild = guild + this.name = data.name + this.type = data.type + + if (data.role_id !== undefined) { + this.roleID = data.role_id + } + + if (data.user) { + this.user = new User(data.user, guild.client) + guild.client.users.set(this.user.id, this.user) + } + + this.account = data.account // not worth making a class for + + this.update(data) + } + + update(data: DiscordIntegration): void { + this.enabled = data.enabled + if (data.syncing !== undefined) { + this.syncing = data.syncing + } + if (data.expire_behavior !== undefined) { + this.expireBehavior = data.expire_behavior + } + if (data.expire_behavior !== undefined) { + this.expireGracePeriod = data.expire_grace_period + } + if (data.enable_emoticons) { + this.enableEmoticons = data.enable_emoticons + } + if (data.subscriber_count !== undefined) { + this.subscriberCount = data.subscriber_count + } + if (data.synced_at !== undefined) { + this.syncedAt = Date.parse(data.synced_at) + } + if (data.revoked !== undefined) { + this.revoked = data.revoked + } + if (data.application !== undefined) { + this.application = data.application + } + } + + /** Delete the guild integration */ + async delete(): Promise { + return await this.guild.client.deleteGuildIntegration.call(this.guild.client, this.guild.id, this.id) + } + + /** Edit the guild integration */ + async edit(options: IntegrationOptions): Promise { + return await this.guild.client.editGuildIntegration.call(this.guild.client, this.guild.id, this.id, options) + } + + /** Force the guild integration to sync */ + async sync(): Promise { + return await this.guild.client.syncGuildIntegration.call(this.guild.client, this.guild.id, this.id) + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'account', + 'application', + 'enabled', + 'enableEmoticons', + 'expireBehavior', + 'expireGracePeriod', + 'name', + 'revoked', + 'roleID', + 'subscriberCount', + 'syncedAt', + 'syncing', + 'type', + 'user', + ...props, + ]) + } +} + +export default GuildIntegration diff --git a/packages/client/src/Structures/guilds/Member.ts b/packages/client/src/Structures/guilds/Member.ts new file mode 100644 index 000000000..892db7a34 --- /dev/null +++ b/packages/client/src/Structures/guilds/Member.ts @@ -0,0 +1,220 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ + +import type { BigString, DiscordMember, DiscordMemberWithUser } from '@discordeno/types' +import Base from '../../Base.js' +import type Client from '../../Client.js' +import type { ImageFormat, ImageSize } from '../../Client.js' +import { GUILD_AVATAR } from '../../Endpoints.js' +import type { MemberOptions } from '../../typings.js' +import User from '../users/User.js' +import type Guild from './Guild.js' + +export class Member extends Base { + /** The client manager */ + client: Client + /** An array of role IDs this member is a part of */ + roles: BigString[] + /** The guild the member is in */ + guild: Guild + /** The user object of the member */ + user: User + /** The server nickname of the member. */ + nick: string | null + /** The timestamp when this member joined the server. */ + joinedAt?: number + /** Timestamp of when the member boosted the guild */ + premiumSince?: number + /** Whether the user has not yet passed the guild's Membership Screening requirements */ + pending?: boolean + /** Timestamp of timeout expiry. If `null`, the member is not timed out */ + communicationDisabledUntil?: number | null + + /** The compressed form of the members avatar. */ + _avatar?: bigint + + constructor(data: (DiscordMember & { id: BigString }) | DiscordMemberWithUser, guild: Guild, client: Client) { + super(client.isDiscordMemberWithUser(data) ? data.user.id : data.id) + this.client = client + this.guild = guild + this.nick = null + this.roles = data.roles ?? [] + + const userID = client.isDiscordMemberWithUser(data) ? data.user.id : data.id + + this.user = client.users.get(userID)! + if (data.user) { + this.user = new User(data.user, client) + client.users.set(this.user.id, this.user) + } + + if (!this.user) { + throw new Error('User associated with Member not found: ' + userID) + } + + this.update(data) + } + + update(data: (DiscordMember & { id: BigString }) | DiscordMemberWithUser) { + if (data.joined_at !== undefined) { + this.joinedAt = data.joined_at ? Date.parse(data.joined_at) : undefined + } + + if (data.premium_since !== undefined) { + this.premiumSince = data.premium_since === null ? undefined : Date.parse(data.premium_since) + } + + // eslint-disable-next-line no-prototype-builtins + if (data.hasOwnProperty('mute') && this.guild) { + // TODO: voice stuff + // const state = this.guild.voiceStates.get(this.id); + // if ( + // data.channel_id === null && + // !data.mute && + // !data.deaf && + // !data.suppress + // ) { + // this.guild.voiceStates.delete(this.id); + // } else if (state) { + // state.update(data); + // } else if (data.channel_id || data.mute || data.deaf || data.suppress) { + // this.guild.voiceStates.update(data); + // } + } + + if (data.nick !== undefined) this.nick = data.nick + if (data.roles !== undefined) this.roles = data.roles + if (data.pending !== undefined) this.pending = data.pending + if (data.avatar !== undefined) this._avatar = this.client.iconHashToBigInt(data.avatar) + + if (data.communication_disabled_until !== undefined) { + if (data.communication_disabled_until !== null) { + this.communicationDisabledUntil = Date.parse(data.communication_disabled_until) + } else { + this.communicationDisabledUntil = data.communication_disabled_until + } + } + } + + get avatar(): string | undefined { + return this._avatar ? this.client.iconBigintToHash(this._avatar) : undefined + } + + get accentColor() { + return this.user.accentColor + } + + get avatarURL() { + return this.avatar ? this.client._formatImage(GUILD_AVATAR(this.guild.id, this.id, this.avatar)) : this.user.avatarURL + } + + get banner() { + return this.user.banner + } + + get bannerURL() { + return this.user.bannerURL + } + + get bot() { + return this.user.bot + } + + get createdAt() { + return this.user.createdAt + } + + get defaultAvatar() { + return this.user.defaultAvatar + } + + get defaultAvatarURL() { + return this.user.defaultAvatarURL + } + + get discriminator() { + return this.user.discriminator + } + + get mention() { + return `<@!${this.id}>` + } + + get permissions() { + return this.guild.permissionsOf(this) + } + + get staticAvatarURL() { + return this.user.staticAvatarURL + } + + get username() { + return this.user.username + } + + get voiceState() { + if (this.guild?.voiceStates.has(this.id)) { + return this.guild.voiceStates.get(this.id) + } else { + // @ts-expect-error some eris magic at play here + return new VoiceState({ id: this.id }) + } + } + + /** Add a role to the guild member */ + async addRole(roleID: BigString, reason?: string): Promise { + return await this.client.addGuildMemberRole.call(this.client, this.guild.id, this.id, roleID, reason) + } + + /** Ban the user from the guild */ + async ban(deleteMessageDays = 0, reason?: string): Promise { + return await this.client.banGuildMember.call(this.client, this.guild.id, this.id, deleteMessageDays, reason) + } + + /** Edit the guild member */ + async edit(options: MemberOptions, reason?: string): Promise { + return await this.client.editGuildMember.call(this.client, this.guild.id, this.id, options, reason) + } + + /** Get the member's avatar with the given format and size */ + dynamicAvatarURL(format?: ImageFormat, size?: ImageSize): string { + return this.avatar + ? this.client._formatImage(GUILD_AVATAR(this.guild.id, this.id, this.avatar), format, size) + : this.user.dynamicAvatarURL(format, size) + } + + /** Kick the member from the guild */ + async kick(reason?: string): Promise { + return await this.client.kickGuildMember.call(this.client, this.guild.id, this.id, reason) + } + + /** Remove a role from the guild member */ + async removeRole(roleID: BigString, reason?: string): Promise { + return await this.client.removeGuildMemberRole.call(this.client, this.guild.id, this.id, roleID, reason) + } + + /** Unban the user from the guild */ + async unban(reason?: string): Promise { + return await this.client.unbanGuildMember.call(this.client, this.guild.id, this.id, reason) + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'activities', + 'communicationDisabledUntil', + 'joinedAt', + 'nick', + 'pending', + 'premiumSince', + 'roles', + 'status', + 'user', + 'voiceState', + ...props, + ]) + } +} + +export default Member diff --git a/packages/client/src/Structures/guilds/Preview.ts b/packages/client/src/Structures/guilds/Preview.ts new file mode 100644 index 000000000..0c278901e --- /dev/null +++ b/packages/client/src/Structures/guilds/Preview.ts @@ -0,0 +1,113 @@ +import type { DiscordEmoji, DiscordGuildPreview } from '@discordeno/types' +import Base from '../../Base.js' +import type Client from '../../Client.js' +import type { ImageFormat, ImageSize } from '../../Client.js' +import { GUILD_DISCOVERY_SPLASH, GUILD_ICON, GUILD_SPLASH } from '../../Endpoints.js' + +export class GuildPreview extends Base { + /** The client object */ + client: Client + /** The name of the guild. */ + name: string + /** The description of the guild. */ + description: string | null + /** An array of guild emojis. */ + emojis: DiscordEmoji[] + /** The approximate number of members in the guild. */ + approximateMemberCount: number + /** The approximate number of presences in the guild. */ + approximatePresenceCount: number + + /** The guild's icon image url. */ + _icon: bigint | null + /** The guild's splash image url. */ + _splash: bigint | null + /** The guild's discovery splash image url. */ + _discoverySplash: bigint | null + /** The guild's features. */ + _features: GuildToggles + + constructor(data: DiscordGuildPreview, client: Client) { + super(data.id) + + this.client = client + this.name = data.name + this.description = data.description + this._icon = data.icon ? client.iconHashToBigInt(data.icon) : null + this._splash = data.splash ? client.iconHashToBigInt(data.splash) : null + this._discoverySplash = data.discovery_splash ? client.iconHashToBigInt(data.discovery_splash) : null + this.approximateMemberCount = data.approximate_member_count + this.approximatePresenceCount = data.approximate_presence_count + this.emojis = data.emojis + // TODO: make dd version accept a specific subset of discord guild here + // @ts-expect-error this should not cause an issue + this._features = new GuildToggles(data) + } + + /** + * @deprecated Use .client + */ + get _client(): Client { + return this.client + } + + get icon(): string | undefined { + return this._icon ? this.client.iconBigintToHash(this._icon) : undefined + } + + get iconURL(): string | null { + return this.icon ? this.client._formatImage(GUILD_ICON(this.id, this.icon)) : null + } + + get splash(): string | undefined { + return this._splash ? this.client.iconBigintToHash(this._splash) : undefined + } + + get splashURL(): string | null { + return this.splash ? this.client._formatImage(GUILD_SPLASH(this.id, this.splash)) : null + } + + get discoverySplash(): string | undefined { + return this._discoverySplash ? this.client.iconBigintToHash(this._discoverySplash) : undefined + } + + get discoverySplashURL(): string | null { + return this.discoverySplash ? this.client._formatImage(GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash)) : null + } + + get features(): string[] { + return this._features.features.map((feature) => feature.replace(/([a-z])([A-Z])/, '$1_$2').toUpperCase()) + } + + /** Get the guild's splash with the given format and size */ + dynamicDiscoverySplashURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.discoverySplash ? this.client._formatImage(GUILD_DISCOVERY_SPLASH(this.id, this.discoverySplash), format, size) : null + } + + /** Get the guild's icon with the given format and size */ + dynamicIconURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.icon ? this.client._formatImage(GUILD_ICON(this.id, this.icon), format, size) : null + } + + /** Get the guild's splash with the given format and size */ + dynamicSplashURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.splash ? this.client._formatImage(GUILD_SPLASH(this.id, this.splash), format, size) : null + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'approximateMemberCount', + 'approximatePresenceCount', + 'description', + 'discoverySplash', + 'emojis', + 'features', + 'icon', + 'name', + 'splash', + ...props, + ]) + } +} + +export default GuildPreview diff --git a/packages/client/src/Structures/guilds/Role.ts b/packages/client/src/Structures/guilds/Role.ts new file mode 100644 index 000000000..200a6e80a --- /dev/null +++ b/packages/client/src/Structures/guilds/Role.ts @@ -0,0 +1,116 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ + +import type { DiscordRole, DiscordRoleTags } from '@discordeno/types' +import Base from '../../Base.js' +import { ROLE_ICON } from '../../Endpoints.js' +import type { RoleOptions } from '../../typings.js' +import Permission from '../Permission.js' +import type Guild from './Guild.js' + +export class Role extends Base { + permissions: Permission + name: string + color: number + hoist: boolean + mentionable: boolean + managed: boolean + icon?: string + unicodeEmoji?: string + position: number + guild: Guild + tags?: Omit & { premium_subscriber?: boolean } + + constructor(data: DiscordRole, guild: Guild) { + super(data.id) + this.guild = guild + + this.name = data.name + this.permissions = new Permission(data.permissions) + this.color = data.color + this.hoist = !!data.hoist + this.mentionable = !!data.mentionable + this.managed = !!data.managed + this.icon = data.icon + this.unicodeEmoji = data.unicode_emoji + this.position = data.position + this.tags = data.tags + ? { + bot_id: data.tags.bot_id, + integration_id: data.tags.integration_id, + premium_subscriber: data.tags.premium_subscriber === null, + } + : undefined + } + + update(data: DiscordRole) { + if (data.name !== undefined) { + this.name = data.name + } + if (data.mentionable !== undefined) { + this.mentionable = data.mentionable + } + if (data.managed !== undefined) { + this.managed = data.managed + } + if (data.hoist !== undefined) { + this.hoist = data.hoist + } + if (data.color !== undefined) { + this.color = data.color + } + if (data.position !== undefined) { + this.position = data.position + } + if (data.permissions !== undefined) { + this.permissions = new Permission(data.permissions) + } + if (data.tags !== undefined) { + this.tags = { + bot_id: data.tags.bot_id, + integration_id: data.tags.integration_id, + premium_subscriber: data.tags.premium_subscriber === null, + } + } + if (data.icon !== undefined) { + this.icon = data.icon + } + if (data.unicode_emoji !== undefined) { + this.unicodeEmoji = data.unicode_emoji + } + } + + get iconURL() { + return this.icon ? this.guild.client._formatImage(ROLE_ICON(this.id, this.icon)) : null + } + + get json() { + return this.permissions.json + } + + get mention() { + return `<@&${this.id}>` + } + + /** Delete the role */ + async delete(reason: string): Promise { + return await this.guild.client.deleteRole.call(this.guild.client, this.guild.id, this.id, reason) + } + + /** Edit the guild role */ + async edit(options: RoleOptions, reason?: string): Promise { + return await this.guild.client.editRole.call(this.guild.client, this.guild.id, this.id, options, reason) + } + + /** Edit the role's position. Note that role position numbers are highest on top and lowest at the bottom. */ + async editPosition(position: number): Promise { + return await this.guild.client.editRolePosition.call(this.guild.client, this.guild.id, this.id, position) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['color', 'hoist', 'icon', 'managed', 'mentionable', 'name', 'permissions', 'position', 'tags', 'unicodeEmoji', ...props]) + } +} + +export default Role diff --git a/packages/client/src/Structures/guilds/StageInstance.ts b/packages/client/src/Structures/guilds/StageInstance.ts new file mode 100644 index 000000000..4eb5b3a65 --- /dev/null +++ b/packages/client/src/Structures/guilds/StageInstance.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +import type { DiscordStageInstance } from "@discordeno/types"; +import Base from "../../Base.js"; +import type Client from "../../Client.js"; +import type { StageInstanceOptions } from "../../typings.js"; +import type StageChannel from "../channels/Stage.js"; +import type Guild from "./Guild.js"; + + +export class StageInstance extends Base { + /** The client manager. */ + client: Client; + /** The associated stage channel */ + channel: StageChannel | { id: string }; + /** The guild of the associated stage channel */ + guild: Guild | { id: string }; + /** The stage channel topic */ + topic?: string | null; + + constructor(data: DiscordStageInstance, client: Client) { + super(data.id); + + this.client = client; + this.channel = client.getChannel(data.channel_id) || { + id: data.channel_id, + }; + this.guild = client.guilds.get(data.guild_id) ?? { id: data.guild_id }; + this.update(data); + } + + /** + * @deprecated Use `.client` instead. + */ + get _client(): Client { + return this.client; + } + + update(data: DiscordStageInstance) { + if (data.topic !== undefined) { + this.topic = data.topic; + } + } + + /** Delete this stage instance */ + async delete(): Promise { + return await this.client.deleteStageInstance.call( + this.client, + this.channel.id + ); + } + + /** Update this stage instance */ + async edit(options: StageInstanceOptions): Promise { + return await this.client.editStageInstance.call( + this.client, + this.channel.id, + options + ); + } +} + +export default StageInstance; \ No newline at end of file diff --git a/packages/client/src/Structures/guilds/Template.ts b/packages/client/src/Structures/guilds/Template.ts new file mode 100644 index 000000000..cb2f00a4f --- /dev/null +++ b/packages/client/src/Structures/guilds/Template.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-useless-call */ +import type { DiscordTemplate } from '@discordeno/types' +import Base from '../../Base.js' +import type Client from '../../Client.js' +import type { GuildTemplateOptions } from '../../typings.js' +import User from '../users/User.js' +import Guild from './Guild.js' + +export class GuildTemplate { + /** The client class itself. */ + client: Client + /** The template code (unique Id) */ + code: string + /** Template name */ + name: string + /** The description for the template */ + description: string | null + /** Number of times this template has been used */ + usageCount: number + /** The user who created the template */ + creator: User + /** When this template was created */ + createdAt: number + /** When this template was last synced to the source guild */ + updatedAt: number + /** The guild snapshot this template contains */ + serializedSourceGuild: Guild + /** The guild this template is based on. If the guild is not cached, this will be an object with `id` key. No other property is guaranteed */ + sourceGuild: Guild | { id: string } + /** Whether the template has un-synced changes */ + isDirty: boolean | null + + constructor(data: DiscordTemplate, client: Client) { + this.client = client + this.code = data.code + this.createdAt = Date.parse(data.created_at) + this.creator = new User(data.creator, client) + this.client.users.set(this.creator.id, this.creator) + this.description = data.description + this.isDirty = data.is_dirty + this.name = data.name + this.sourceGuild = client.guilds.get(data.source_guild_id) ?? { id: data.source_guild_id } + this.updatedAt = Date.parse(data.updated_at) + this.usageCount = data.usage_count + + data.serialized_source_guild.features = [] + // @ts-expect-error Hacks to get this to not error + this.serializedSourceGuild = new Guild(data.serialized_source_guild, client) + } + + /** + * @deprecated Use .client instead. + */ + get _client(): Client { + return this.client + } + + /** Create a guild based on this template. Only for bots in less than 10 guilds */ + async createGuild(name: string, icon?: string): Promise { + return await this.client.createGuildFromTemplate.call(this.client, this.code, name, icon) + } + + /** Delete this template */ + async delete(): Promise { + return await this.client.deleteGuildTemplate.call(this.client, this.sourceGuild.id, this.code) + } + + /** Edit this template */ + async edit(options: GuildTemplateOptions): Promise { + return await this.client.editGuildTemplate.call(this.client, this.sourceGuild.id, this.code, options) + } + + /** Force this template to sync to the guild's current state */ + async sync(): Promise { + return await this.client.syncGuildTemplate.call(this.client, this.sourceGuild.id, this.code) + } + + toJSON(props: string[] = []): Record { + return Base.prototype.toJSON.call(this, [ + 'code', + 'createdAt', + 'creator', + 'description', + 'isDirty', + 'name', + 'serializedSourceGuild', + 'sourceGuild', + 'updatedAt', + 'usageCount', + ...props, + ]) + } +} + +export default GuildTemplate diff --git a/packages/client/src/Structures/guilds/Unavailable.ts b/packages/client/src/Structures/guilds/Unavailable.ts new file mode 100644 index 000000000..f05060ab1 --- /dev/null +++ b/packages/client/src/Structures/guilds/Unavailable.ts @@ -0,0 +1,23 @@ +import type { DiscordUnavailableGuild } from "@discordeno/types" +import Base from "../../Base.js" +import type Client from "../../Client.js" + + +export class UnavailableGuild extends Base { + /** Whether or not the guild is unavailable. */ + unavailable: boolean + + constructor(data: DiscordUnavailableGuild, client: Client) { + super(data.id) + + // TODO: gateway + // this.shard = client.shards.get(client.guildShardMap[this.id]); + this.unavailable = !!data.unavailable + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['unavailable', ...props]) + } +} + +export default UnavailableGuild diff --git a/packages/client/src/Structures/guilds/VoiceState.ts b/packages/client/src/Structures/guilds/VoiceState.ts new file mode 100644 index 000000000..202fe1e98 --- /dev/null +++ b/packages/client/src/Structures/guilds/VoiceState.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DiscordVoiceState } from "@discordeno/types" +import Base from "../../Base.js" + + +export class VoiceState extends Base { + channelID: string | null = null + requestToSpeakTimestamp: number | null + sessionID!: string | null + + bitfield: VoiceStateToggles + + constructor(data: DiscordVoiceState & { id: string }) { + super(data.id) + + this.requestToSpeakTimestamp = null + this.bitfield = new VoiceStateToggles(data) + + this.update(data) + } + + /** Whether or not the user is deafened by the server. */ + get deaf(): boolean { + return this.bitfield.deaf + } + + /** Set whether or not the user is deafened by the server. */ + set deaf(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.deaf) + else this.bitfield.remove(VoiceStateToggle.deaf) + } + + /** Whether or not the user is muted by the server. */ + get mute(): boolean { + return this.bitfield.mute + } + + /** Set whether or not the user is muted by the server. */ + set mute(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.mute) + else this.bitfield.remove(VoiceStateToggle.mute) + } + + /** Whether or not the user has deafened themself. */ + get selfDeaf(): boolean { + return this.bitfield.selfDeaf + } + + /** Set whether or not the user has deafened themself. */ + set selfDeaf(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.selfDeaf) + else this.bitfield.remove(VoiceStateToggle.selfDeaf) + } + + /** Whether or not the user has muted themself. */ + get selfMute(): boolean { + return this.bitfield.selfMute + } + + /** Set whether or not the user has muted themself. */ + set selfMute(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.selfMute) + else this.bitfield.remove(VoiceStateToggle.selfMute) + } + + /** Whether or not the user is streaming. */ + get selfStream(): boolean { + return this.bitfield.selfStream + } + + /** Set whether or not the user is streaming. */ + set selfStream(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.selfStream) + else this.bitfield.remove(VoiceStateToggle.selfStream) + } + + /** Whether or not the user is video calling. */ + get selfVideo(): boolean { + return this.bitfield.selfVideo + } + + /** Set whether or not the user is video calling. */ + set selfVideo(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.selfVideo) + else this.bitfield.remove(VoiceStateToggle.selfVideo) + } + + /** Whether or not the user is suppressed from speaking. */ + get suppress(): boolean { + return this.bitfield.suppress + } + + /** Set whether or not the user is suppressed from speaking. */ + set suppress(value: boolean) { + if (value) this.bitfield.add(VoiceStateToggle.suppress) + else this.bitfield.remove(VoiceStateToggle.suppress) + } + + update(data: DiscordVoiceState) { + if (data.channel_id !== undefined) { + this.channelID = data.channel_id + this.sessionID = data.channel_id === null ? null : data.session_id + } else if (this.channelID === undefined) { + this.channelID = this.sessionID = null + } + + if (data.mute !== undefined) this.mute = data.mute + if (data.deaf !== undefined) this.deaf = data.deaf + if (data.request_to_speak_timestamp) this.requestToSpeakTimestamp = Date.parse(data.request_to_speak_timestamp) + if (data.self_mute !== undefined) this.selfMute = data.self_mute + if (data.self_deaf !== undefined) this.selfDeaf = data.self_deaf + if (data.self_video !== undefined) this.selfVideo = data.self_video + if (data.self_stream !== undefined) this.selfStream = data.self_stream + if (data.suppress !== undefined) this.suppress = data.suppress + } + + toJSON(props: string[] = []): Record { + return super.toJSON([ + 'channelID', + 'deaf', + 'mute', + 'requestToSpeakTimestamp', + 'selfDeaf', + 'selfMute', + 'selfStream', + 'selfVideo', + 'sessionID', + 'suppress', + ...props, + ]) + } +} diff --git a/packages/client/src/Structures/interactions/Autocomplete.ts b/packages/client/src/Structures/interactions/Autocomplete.ts new file mode 100644 index 000000000..d27c28051 --- /dev/null +++ b/packages/client/src/Structures/interactions/Autocomplete.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/return-await */ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { BigString, DiscordInteraction, DiscordInteractionData } from '@discordeno/types' +import { InteractionResponseTypes } from '@discordeno/types' +import type Client from '../../Client.js' +import type { ApplicationCommandOptionChoice } from '../../typings.js' +import type NewsChannel from '../channels/News.js' +import type PrivateChannel from '../channels/Private.js' +import type TextChannel from '../channels/Text.js' +import type Guild from '../guilds/Guild.js' +import Member from '../guilds/Member.js' +import Permission from '../Permission.js' +import User from '../users/User.js' +import Interaction from './Interaction.js' + +export class AutocompleteInteraction extends Interaction { + /** The guild id if this interaction occurred in a guild. */ + guildID?: BigString + /** The permissions the app or bot has within the channel, the interaction was sent from. */ + appPermissions?: Permission + /** The channel id where this interaction was created in. */ + channelID: BigString + /** The user who triggered the interaction. */ + user: User + /** The data attached to this interaction. */ + data?: DiscordInteractionData + /** The member who triggered the interaction. Sent when used in a guild. */ + member?: Member + + constructor(data: DiscordInteraction, client: Client) { + super(data, client) + + this.channelID = data.channel_id! + this.data = data.data + + if (data.guild_id !== undefined) { + this.guildID = data.guild_id + } + + if (data.member !== undefined && this.guild) { + this.member = new Member(data.member, this.guild, this.client) + this.guild.members.set(this.member.id, this.member) + } + + this.user = new User(data.user ?? data.member!.user, this.client) + this.client.users.set(this.user.id, this.user) + + if (data.app_permissions !== undefined) { + this.appPermissions = new Permission(data.app_permissions) + } + } + + /** The channel the interaction was created in. */ + get channel(): PrivateChannel | TextChannel | NewsChannel { + return this.client.getChannel(this.channelID) as PrivateChannel | TextChannel | NewsChannel + } + + /** The guild the interaction was created in. */ + get guild(): Guild | undefined { + return this.guildID ? this.client.guilds.get(this.guildID) : undefined + } + + async acknowledge(choices: ApplicationCommandOptionChoice[]) { + return await this.result(choices) + } + + async result(choices: ApplicationCommandOptionChoice[]) { + if (this.acknowledged) throw new Error('You have already acknowledged this interaction.') + + return this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + data: { choices }, + }) + .then(() => this.update()) + } +} + +export default AutocompleteInteraction diff --git a/packages/client/src/Structures/interactions/Command.ts b/packages/client/src/Structures/interactions/Command.ts new file mode 100644 index 000000000..e605d9edb --- /dev/null +++ b/packages/client/src/Structures/interactions/Command.ts @@ -0,0 +1,318 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ +import { + ApplicationCommandTypes, + InteractionResponseTypes, + type BigString, + type DiscordAttachment, + type DiscordInteraction, + type DiscordInteractionDataOption, + type DiscordMessageComponents, + type MessageComponentTypes, +} from '@discordeno/types' +import type Client from '../../Client.js' +import Collection from '../../Collection.js' +import type { AnyChannel, FileContent, InteractionContent, InteractionContentEdit } from '../../typings.js' +import Channel from '../channels/Channel.js' +import Member from '../guilds/Member.js' +import Role from '../guilds/Role.js' +import Message from '../Message.js' +import User from '../users/User.js' +import Interaction from './Interaction.js' + +export class CommandInteraction extends Interaction { + channel: AnyChannel + /** The type of component */ + componentType?: MessageComponentTypes + /** The custom id provided for this component. */ + customId?: string + /** The components if its a Modal Submit interaction. */ + components?: DiscordMessageComponents + /** The values chosen by the user. */ + values?: string[] + /** the type of the invoked command */ + commandType: ApplicationCommandTypes = ApplicationCommandTypes.ChatInput + /** The Ids and Message objects */ + messages = new Collection() + /** The Ids and User objects */ + users = new Collection() + /** The Ids and partial Member objects */ + members = new Collection() + /** The Ids and Role objects */ + roles = new Collection() + /** The Ids and partial Channel objects */ + channels = new Collection() + /** The ids and attachment objects */ + attachments = new Collection() + /** The params + values from the user */ + options?: DiscordInteractionDataOption[] + /** The target id if this is a context menu command. */ + targetID?: string + /** the id of the guild the command is registered to */ + guildID?: string + + member?: Member + user: User + + constructor(info: DiscordInteraction, client: Client) { + super(info, client) + + this.channel = this.client.getChannel(info.channel_id!) || { + id: info.channel_id, + } + + // this.data = info.data!; + const guild = this.client.guilds.get(info.guild_id!) + + if (info.data?.resolved !== undefined) { + // Users + if (info.data.resolved.users !== undefined) { + for (const u of Object.values(info.data.resolved.users)) { + const user = new User(u, this.client) + this.users.set(user.id, user) + } + } + + // Members + if (info.data.resolved.members !== undefined) { + for (const [, m] of Object.entries(info.data.resolved.members)) { + // @ts-expect-error some eris magic at play here + m.id = m + // @ts-expect-error some eris magic at play here + const member = new Member(m, guild, this.client) + this.members.set(member.id, member) + } + } + + // Roles + if (info.data.resolved.roles !== undefined) { + for (const r of Object.values(info.data.resolved.roles)) { + // @ts-expect-error some eris magic at play here + const role = new Role(r, guild) + this.roles.set(role.id, role) + } + } + + // Channels + if (info.data.resolved.channels !== undefined) { + for (const c of Object.values(info.data.resolved.channels)) { + const channel = new Channel(c, this.client) + this.channels.set(channel.id, channel) + } + } + + // Messages + if (info.data.resolved.messages !== undefined) { + for (const m of Object.values(info.data.resolved.messages)) { + const message = new Message(m, this.client) + this.messages.set(message.id, message) + } + } + + // Attachments + if (info.data.resolved.attachments !== undefined) { + for (const attachment of Object.values(info.data.resolved.attachments)) { + this.attachments.set(attachment.id, attachment) + } + } + } + + this.guildID = info.guild_id + + if (info.member !== undefined) { + // @ts-expect-error some eris magic at play here + this.member = new Member(info.member, guild, this.client) + guild?.members.set(this.member.id, this.member) + } + + this.user = new User(info.user ?? info.member!.user, this.client) + this.client.users.set(this.user.id, this.user) + + if (info.data) { + this.componentType = info.data.component_type + this.customId = info.data.custom_id + this.components = info.data.components + this.values = info.data.values + this.commandType = info.data.type + this.options = info.data.options + this.targetID = info.data.target_id + } + } + + get data() { + return { + component_type: this.componentType, + custom_id: this.customId, + components: this.components, + values: this.values, + type: this.commandType, + resolved: { + messages: this.messages.toRecord(), + users: this.users.toRecord(), + members: this.members.toRecord(), + roles: this.roles.toRecord(), + channels: this.channels.toRecord(), + attachments: this.attachments.toRecord(), + }, + options: this.options, + target_id: this.targetID, + guild_id: this.guildID, + } + } + + /** + * Acknowledges the interaction with a defer response + * Note: You can **not** use more than 1 initial interaction response per interaction. + */ + async acknowledge(flags?: number): Promise { + return this.defer(flags) + } + + /** Respond to the interaction with a followup message */ + async createFollowup(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise { + if (!this.acknowledged) { + throw new Error('createFollowup cannot be used to acknowledge an interaction, please use acknowledge, createMessage, or defer first.') + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + return await this.client.executeWebhook.call(this.client, this.applicationID, this.token, { + ...content, + wait: true, + file, + }) + } + + /** + * Acknowledges the interaction with a message. If already acknowledged runs createFollowup + * Note: You can **not** use more than 1 initial interaction response per interaction, use createFollowup if you have already responded with a different interaction response. + */ + async createMessage(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise { + if (this.acknowledged) { + // @ts-expect-error some eris magic at play here + return await this.createFollowup(content, file) + } + + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + + return this.client.createInteractionResponse + .call( + this.client, + this.id, + this.token, + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: content, + }, + file, + ) + .then(() => this.update()) + } + + /** + * Acknowledges the interaction with a defer response + * Note: You can **not** use more than 1 initial interaction response per interaction. + */ + async defer(flags?: number): Promise { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + return this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.DeferredChannelMessageWithSource, + data: { + flags: flags ?? 0, + }, + }) + .then(() => this.update()) + } + + /** Delete a message */ + async deleteMessage(messageID: BigString): Promise { + if (!this.acknowledged) { + throw new Error('deleteMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, or defer first.') + } + return this.client.deleteWebhookMessage.call(this.client, this.applicationID, this.token, messageID) + } + + /** + * Delete the Original message + * Warning: Will error with ephemeral messages. + */ + async deleteOriginalMessage(): Promise { + if (!this.acknowledged) { + throw new Error('deleteOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, or defer first.') + } + return this.client.deleteWebhookMessage.call(this.client, this.applicationID, this.token, '@original') + } + + /** Edit a message */ + async editMessage(messageID: BigString, content: string | InteractionContentEdit, file?: FileContent | FileContent[]): Promise { + if (!this.acknowledged) { + throw new Error('editMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, or defer first.') + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + return await this.client.editWebhookMessage.call(this.client, this.applicationID, this.token, messageID, { + ...content, + file, + }) + } + + /** Edit the Original response message */ + async editOriginalMessage(content: string | InteractionContentEdit, file?: FileContent | FileContent[]): Promise { + if (!this.acknowledged) { + throw new Error('editOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, or defer first.') + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + return this.client.editWebhookMessage.call(this.client, this.applicationID, this.token, '@original', { + ...content, + file, + }) + } + + /** + * Get the Original response message + * Warning: Will error with ephemeral messages. + */ + async getOriginalMessage(): Promise { + if (!this.acknowledged) { + throw new Error('getOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, or defer first.') + } + return this.client.getWebhookMessage.call(this.client, this.applicationID, this.token, '@original') + } +} + +export default CommandInteraction diff --git a/packages/client/src/Structures/interactions/Component.ts b/packages/client/src/Structures/interactions/Component.ts new file mode 100644 index 000000000..f8987244c --- /dev/null +++ b/packages/client/src/Structures/interactions/Component.ts @@ -0,0 +1,296 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ + +import type { BigString, DiscordInteraction, MessageComponentTypes } from '@discordeno/types' +import { InteractionResponseTypes } from '@discordeno/types' +import type Client from '../../Client.js' +import type { AnyChannel, FileContent, InteractionApplicationCommandCallbackData, MessageWebhookContent, WebhookPayload } from '../../typings.js' +import type Guild from '../guilds/Guild.js' +import Member from '../guilds/Member.js' +import Message from '../Message.js' +import Permission from '../Permission.js' +import User from '../users/User.js' +import Interaction from './Interaction.js' + +export class ComponentInteraction extends Interaction { + /** The channel id where this interaction occurred in. */ + channelID: BigString + /** The guild id where this interaction occurred in. */ + guildID?: BigString + /** The member object if this interaction occurred in a guild. */ + member?: Member + /** The user object for the user that created this interaction. */ + user: User + /** The permissions the app or bot has within the channel, the interaction was sent from. */ + appPermissions?: Permission + /** The message object, if this interaction occurred on a message. */ + message?: Message + /** The custom id of the component. */ + customID: string + /** The type of component. */ + componentType: MessageComponentTypes + /** The values from a selector component. */ + values?: string[] + + constructor(data: DiscordInteraction, client: Client) { + super(data, client) + + this.channelID = data.channel_id! + // this.data = data.data; + this.guildID = data.guild_id + // Required to make a component + this.customID = data.data!.custom_id! + this.componentType = data.data!.component_type! + this.values = data.data!.values + + if (data.member !== undefined && this.guild) { + this.member = new Member(data.member, this.guild, this.client) + this.guild.members.set(this.member.id, this.member) + } + + if (data.message !== undefined) { + this.message = new Message(data.message, this.client) + } + + this.user = new User(data.user ?? data.member!.user, this.client) + this.client.users.set(this.user.id, this.user) + + if (data.app_permissions !== undefined) { + this.appPermissions = new Permission(data.app_permissions) + } + } + + /** The channel if cached, where this interaction occurred. */ + get channel(): AnyChannel | undefined { + return this.channelID ? this.client.getChannel(this.channelID) : undefined + } + + /** The guild if cached, where this interaction occurred. */ + get guild(): Guild | undefined { + return this.guildID ? this.client.guilds.get(this.guildID) : undefined + } + + /** Acknowledges the interaction with a defer message update response */ + async acknowledge(): Promise { + return await this.deferUpdate() + } + + /** Respond to the interaction with a followup message. */ + async createFollowup(content: WebhookPayload, file?: FileContent | FileContent[]): Promise { + if (!this.acknowledged) { + throw new Error( + 'createFollowup cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, or editParent first.', + ) + } + + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + if (file) { + content.file = file + } + + return await this.client.executeWebhook.call(this.client, this.applicationID, this.token, { ...content, wait: true }) + } + + /** + * Acknowledges the interaction with a message. If already acknowledged runs createFollowup + * Note: You can **not** use more than 1 initial interaction response per interaction, use createFollowup if you have already responded with a different interaction response. + */ + async createMessage(content: InteractionApplicationCommandCallbackData, file?: FileContent | FileContent[]): Promise { + if (this.acknowledged) { + return await this.createFollowup(content, file) + } + + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + await this.client.createInteractionResponse + .call( + this.client, + this.id, + this.token, + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: content, + }, + file, + ) + .then(() => this.update()) + } + + /** + * Acknowledges the interaction with a defer response + * Note: You can **not** use more than 1 initial interaction response per interaction. + */ + async defer(flags: number): Promise { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.DeferredChannelMessageWithSource, + data: { + flags: flags || 0, + }, + }) + .then(() => this.update()) + } + + /** + * Acknowledges the interaction with a defer message update response + * Note: You can **not** use more than 1 initial interaction response per interaction. + */ + async deferUpdate(): Promise { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.DeferredUpdateMessage, + }) + .then(() => this.update()) + } + + /** Delete a message */ + async deleteMessage(messageID: BigString): Promise { + if (!this.acknowledged) { + throw new Error( + 'deleteMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, or editParent first.', + ) + } + + return await this.client.deleteWebhookMessage.call(this.client, this.applicationID, this.token, messageID) + } + + /** + * Delete the parent message + * Warning: Will error with ephemeral messages. + */ + async deleteOriginalMessage(): Promise { + if (!this.acknowledged) { + throw new Error( + 'deleteOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, or editParent first.', + ) + } + + return await this.client.deleteWebhookMessage.call(this.client, this.applicationID, this.token, '@original') + } + + /** Edit a message */ + async editMessage(messageID: BigString, content: MessageWebhookContent, file?: FileContent | FileContent[]): Promise { + if (!this.acknowledged) { + throw new Error( + 'editMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, or editParent first.', + ) + } + + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + if (file) { + content.file = file + } + + return await this.client.editWebhookMessage.call(this.client, this.applicationID, this.token, messageID, content) + } + + /** Edit the parent message */ + async editOriginalMessage(content: MessageWebhookContent, file?: FileContent | FileContent[]): Promise { + if (!this.acknowledged) { + throw new Error( + 'editOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, or editParent first.', + ) + } + + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + if (file) { + content.file = file + } + + return await this.client.editWebhookMessage.call(this.client, this.applicationID, this.token, '@original', content) + } + + /** + * Acknowledges the interaction by editing the parent message. If already acknowledged runs editOriginalMessage + * Note: You can **not** use more than 1 initial interaction response per interaction, use edit if you have already responded with a different interaction response. + * Warning: Will error with ephemeral messages. + */ + async editParent(content: InteractionApplicationCommandCallbackData, file?: FileContent | FileContent[]): Promise { + if (this.acknowledged) { + return this.editOriginalMessage(content) + } + + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + await this.client.createInteractionResponse + .call( + this.client, + this.id, + this.token, + { + type: InteractionResponseTypes.UpdateMessage, + data: content, + }, + file, + ) + .then(() => this.update()) + } + + /** + * Get the parent message + * Warning: Will error with ephemeral messages. + */ + async getOriginalMessage(): Promise { + if (!this.acknowledged) { + throw new Error( + 'getOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, or editParent first.', + ) + } + + return this.client.getWebhookMessage.call(this.client, this.applicationID, this.token, '@original') + } +} + +export default ComponentInteraction diff --git a/packages/client/src/Structures/interactions/Interaction.ts b/packages/client/src/Structures/interactions/Interaction.ts new file mode 100644 index 000000000..8d2372fa0 --- /dev/null +++ b/packages/client/src/Structures/interactions/Interaction.ts @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { InteractionTypes, type BigString, type DiscordInteraction } from '@discordeno/types' +import Base from '../../Base.js' +import type Client from '../../Client.js' +import AutocompleteInteraction from './Autocomplete.js' +import CommandInteraction from './Command.js' +import ComponentInteraction from './Component.js' +import PingInteraction from './Ping.js' +import UnknownInteraction from './Unknown.js' + +export class Interaction extends Base { + client: Client + applicationID: BigString + token: string + type: InteractionTypes + version: 1 + acknowledged: boolean + + constructor(data: DiscordInteraction, client: Client) { + super(data.id) + this.client = client + + this.applicationID = data.application_id + this.token = data.token + this.type = data.type + this.version = data.version + this.acknowledged = false + } + + /** + * @deprecated Use `.client` + */ + get _client(): Client { + return this.client + } + + update() { + this.acknowledged = true + } + + static from(data: DiscordInteraction, client: Client) { + switch (data.type) { + case InteractionTypes.Ping: { + return new PingInteraction(data, client) + } + case InteractionTypes.ApplicationCommand: { + return new CommandInteraction(data, client) + } + case InteractionTypes.MessageComponent: { + return new ComponentInteraction(data, client) + } + case InteractionTypes.ApplicationCommandAutocomplete: { + return new AutocompleteInteraction(data, client) + } + } + + client.emit('warn', new Error(`Unknown interaction type: ${data.type}\n${JSON.stringify(data)}`)) + return new UnknownInteraction(data, client) + } +} + +export default Interaction diff --git a/packages/client/src/Structures/interactions/Ping.ts b/packages/client/src/Structures/interactions/Ping.ts new file mode 100644 index 000000000..2dd1a6b90 --- /dev/null +++ b/packages/client/src/Structures/interactions/Ping.ts @@ -0,0 +1,33 @@ +/* eslint-disable no-useless-call */ +/* eslint-disable @typescript-eslint/return-await */ + +import { InteractionResponseTypes } from '@discordeno/types' +import Interaction from './Interaction.js' + +export class PingInteraction extends Interaction { + /** + * Acknowledges the ping interaction with a pong response. + * Note: You can **not** use more than 1 initial interaction response per interaction. + */ + async acknowledge(): Promise { + return this.pong() + } + + /** + * Acknowledges the ping interaction with a pong response. + * Note: You can **not** use more than 1 initial interaction response per interaction. + */ + async pong(): Promise { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.Pong, + }) + .then(() => this.update()) + } +} + +export default PingInteraction diff --git a/packages/client/src/Structures/interactions/Unknown.ts b/packages/client/src/Structures/interactions/Unknown.ts new file mode 100644 index 000000000..04a1c7e39 --- /dev/null +++ b/packages/client/src/Structures/interactions/Unknown.ts @@ -0,0 +1,493 @@ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable no-useless-call */ + +import type { DiscordInteraction } from '@discordeno/types' +import { InteractionResponseTypes } from '@discordeno/types' +import type Client from '../../Client.js' +import type { + ApplicationCommandOptionChoice, + FileContent, + InteractionContent, + InteractionContentEdit, + InteractionResponse, + PossiblyUncachedTextable, + TextableChannel, +} from '../../typings.js' +import Member from '../guilds/Member.js' +import Message from '../Message.js' +import Permission from '../Permission.js' +import User from '../users/User.js' +import Interaction from './Interaction.js' + +export class UnknownInteraction extends Interaction { + channel?: T + data?: unknown + guildID?: string + member?: Member + message?: Message + type: number = 0 + user?: User + appPermissions?: Permission + + constructor(data: DiscordInteraction, client: Client) { + super(data, client) + + if (data.channel_id !== undefined) { + this.channel = (this.client.getChannel(data.channel_id) as unknown as T) || { + id: data.channel_id, + } + } + + if (data.data !== undefined) { + this.data = data.data + } + + if (data.guild_id !== undefined) { + this.guildID = data.guild_id + } + + if (data.member !== undefined) { + const guild = this.client.guilds.get(data.guild_id ?? '')! + this.member = new Member(data.member, guild, this.client) + guild.members.set(this.member.id, this.member) + } + + if (data.message !== undefined) { + this.message = new Message(data.message, this.client) + } + + if (data.user !== undefined) { + this.user = new User(data.user, this.client) + this.client.users.set(this.user.id, this.user) + } + + if (data.app_permissions !== undefined) { + this.appPermissions = new Permission(data.app_permissions) + } + } + + /** + * Acknowledges the autocomplete interaction with a result of choices. + * Note: You can **not** use more than 1 initial interaction response per interaction. + * @arg {Object} data The data object + * @arg {Number} data.type The type of [interaction response](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type) to send + * @arg {Object} data.data The data to return to discord + * @returns {Promise} + */ + async acknowledge(data: InteractionResponse) { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + return await this.client.createInteractionResponse.call(this.client, this.id, this.token, data).then(() => this.update()) + } + + /** + * Respond to the interaction with a followup message + * @arg {String | Object} content A string or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [options.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Number} [content.flags] 64 for Ephemeral + * @arg {Boolean} [content.tts] Set the message TTS flag + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async createFollowup(content: string | InteractionContent, file?: FileContent | FileContent[]) { + if (!this.acknowledged) { + throw new Error( + 'createFollowup cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, editParent, pong, or result first.', + ) + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + if (file) { + content.file = file + } + return await this.client.executeWebhook.call(this.client, this.applicationID, this.token, Object.assign({ wait: true as true }, content)) + } + + /** + * Acknowledges the interaction with a message. If already acknowledged runs createFollowup + * Note: You can **not** use more than 1 initial interaction response per interaction, use createFollowup if you have already responded with a different interaction response. + * @arg {String | Object} content A string or object. If an object is passed: + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Boolean} [content.flags] 64 for Ephemeral + * @arg {Boolean} [content.tts] Set the message TTS flag + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async createMessage(content: string | InteractionContent, file?: FileContent | FileContent[]) { + if (this.acknowledged) { + return await this.createFollowup(content, file) + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + return await this.client.createInteractionResponse + .call( + this.client, + this.id, + this.token, + { + type: InteractionResponseTypes.ChannelMessageWithSource, + data: content, + }, + file, + ) + .then(() => this.update()) + } + + /** + * Acknowledges the interaction with a defer response + * Note: You can **not** use more than 1 initial interaction response per interaction. + * @arg {Number} [flags] 64 for Ephemeral + * @returns {Promise} + */ + async defer(flags: number) { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.DeferredChannelMessageWithSource, + data: { + flags: flags || 0, + }, + }) + .then(() => this.update()) + } + + /** + * Acknowledges the interaction with a defer message update response (Message Component only) + * Note: You can **not** use more than 1 initial interaction response per interaction. + * @returns {Promise} + */ + async deferUpdate() { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.DeferredUpdateMessage, + }) + .then(() => this.update()) + } + + /** + * Delete a message + * @arg {String} messageID the id of the message to delete, or "@original" for the original response. + * @returns {Promise} + */ + async deleteMessage(messageID: string) { + if (!this.acknowledged) { + throw new Error( + 'deleteMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, editParent, or pong first.', + ) + } + return await this.client.deleteWebhookMessage.call(this.client, this.applicationID, this.token, messageID) + } + + /** + * Delete the Original message (or the parent message for components) + * Warning: Will error with ephemeral messages. + * @returns {Promise} + */ + async deleteOriginalMessage() { + if (!this.acknowledged) { + throw new Error( + 'deleteOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, editParent, or pong first.', + ) + } + return await this.client.deleteWebhookMessage.call(this.client, this.applicationID, this.token, '@original') + } + + /** + * Edit a message + * @arg {String} messageID the id of the message to edit, or "@original" for the original response. + * @arg {Object} content Interaction message edit options + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean} [content.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async editMessage(messageID: string, content: string | InteractionContentEdit, file?: FileContent | FileContent[]) { + if (!this.acknowledged) { + throw new Error( + 'editMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, editParent, pong, or result first.', + ) + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + if (file) { + content.file = file + } + return await this.client.editWebhookMessage.call(this.client, this.applicationID, this.token, messageID, content) + } + + /** + * Edit the Original response message + * @arg {Object} content Interaction message edit options (or the parent message for components) + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean} [content.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async editOriginalMessage(content: string | InteractionContentEdit, file?: FileContent | FileContent[]) { + if (!this.acknowledged) { + throw new Error( + 'editOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, editParent, pong, or result first.', + ) + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + if (file) { + content.file = file + } + + return await this.client.editWebhookMessage.call(this.client, this.applicationID, this.token, '@original', content) + } + + /** + * Acknowledges the interaction by editing the parent message. If already acknowledged runs editOriginalMessage (Message Component only) + * Note: You can **not** use more than 1 initial interaction response per interaction, use edit if you have already responded with a different interaction response. + * Warning: Will error with ephemeral messages. + * @arg {String | Object} content What to edit the message with + * @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here. + * @arg {Boolean} [content.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to. + * @arg {Boolean | Array} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow. + * @arg {Boolean | Array} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow. + * @arg {Array} [content.components] An array of component objects + * @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [content.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [content.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [content.components[].options[].description] The description for this option + * @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} content.components[].options[].label The label for this option + * @arg {Number | String} content.components[].options[].value The value for this option + * @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [content.content] A content string + * @arg {Object} [content.embed] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Boolean} [content.flags] 64 for Ephemeral + * @arg {Boolean} [content.tts] Set the message TTS flag + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + async editParent(content: InteractionContentEdit, file?: FileContent | FileContent[]) { + if (this.acknowledged) { + return await this.editOriginalMessage(content) + } + if (content !== undefined) { + if (typeof content !== 'object' || content === null) { + content = { + content: '' + content, + } + } else if (content.content !== undefined && typeof content.content !== 'string') { + content.content = '' + content.content + } + } + + return await this.client.createInteractionResponse + .call( + this.client, + this.id, + this.token, + { + type: InteractionResponseTypes.UpdateMessage, + data: content, + }, + file, + ) + .then(() => this.update()) + } + + /** + * Get the Original response message (or the parent message for components) + * Warning: Will error with ephemeral messages. + * @returns {Promise} + */ + async getOriginalMessage() { + if (!this.acknowledged) { + throw new Error( + 'getOriginalMessage cannot be used to acknowledge an interaction, please use acknowledge, createMessage, defer, deferUpdate, editParent, or pong first.', + ) + } + return await this.client.getWebhookMessage.call(this.client, this.applicationID, this.token, '@original') + } + + /** + * Acknowledges the ping interaction with a pong response (Ping Only) + * Note: You can **not** use more than 1 initial interaction response per interaction. + * @returns {Promise} + */ + async pong() { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.Pong, + }) + .then(() => this.update()) + } + + /** + * Acknowledges the autocomplete interaction with a result of choices. + * Note: You can **not** use more than 1 initial interaction response per interaction. + * @arg {Array} choices The autocomplete choices to return to the user + * @arg {String | Number} choices[].name The choice display name + * @arg {String} choices[].value The choice value to return to the bot + * @returns {Promise} + */ + async result(choices: ApplicationCommandOptionChoice[]) { + if (this.acknowledged) { + throw new Error('You have already acknowledged this interaction.') + } + return await this.client.createInteractionResponse + .call(this.client, this.id, this.token, { + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + data: { choices }, + }) + .then(() => this.update()) + } +} + +export default UnknownInteraction diff --git a/packages/client/src/Structures/users/Extended.ts b/packages/client/src/Structures/users/Extended.ts new file mode 100644 index 000000000..7a55cb90b --- /dev/null +++ b/packages/client/src/Structures/users/Extended.ts @@ -0,0 +1,38 @@ +import type { PremiumTypes, DiscordUser } from "@discordeno/types" +import type Client from "../../Client.js" +import User from "./User.js" + + +export class ExtendedUser extends User { + email?: string | null + verified?: boolean + mfaEnabled?: boolean + premiumType?: PremiumTypes + + constructor(data: DiscordUser, client: Client) { + super(data, client) + this.update(data) + } + + update(data: DiscordUser): void { + super.update(data) + if (data.email !== undefined) { + this.email = data.email + } + if (data.verified !== undefined) { + this.verified = data.verified + } + if (data.mfa_enabled !== undefined) { + this.mfaEnabled = data.mfa_enabled + } + if (data.premium_type !== undefined) { + this.premiumType = data.premium_type + } + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['email', 'mfaEnabled', 'premium', 'verified', ...props]) + } +} + +export default ExtendedUser diff --git a/packages/client/src/Structures/users/User.ts b/packages/client/src/Structures/users/User.ts new file mode 100644 index 000000000..376080353 --- /dev/null +++ b/packages/client/src/Structures/users/User.ts @@ -0,0 +1,128 @@ +/* eslint-disable no-useless-call */ +import type { BigString, DiscordUser, UserFlags } from '@discordeno/types' +import Base from '../../Base.js' +import type Client from '../../Client.js' +import type { ImageFormat, ImageSize } from '../../Client.js' +import { BANNER, DEFAULT_USER_AVATAR, USER_AVATAR } from '../../Endpoints.js' +import type PrivateChannel from '../channels/Private.js' + +export class User extends Base { + client: Client + bot: boolean + system: boolean + _avatar!: bigint | null + username!: string + discriminator!: string + publicFlags?: UserFlags + _banner!: bigint | null + accentColor?: number + + constructor(data: DiscordUser, client: Client) { + super(data.id) + + this.client = client + this.bot = !!data.bot + this.system = !!data.system + + this.update(data) + } + + /** @deprecated Use User.client Supported for Eris api compatibility. */ + get _client(): Client { + return this.client + } + + get avatar(): string | null { + if (!this._avatar) return null + + return this.client.iconBigintToHash(this._avatar) + } + + set avatar(value: BigString | null) { + this._avatar = typeof value === 'string' ? this.client.iconHashToBigInt(value) : value + } + + get banner(): string | null { + if (!this._banner) return null + + return this.client.iconBigintToHash(this._banner) + } + + set banner(value: BigString | null) { + this._banner = typeof value === 'string' ? this.client.iconHashToBigInt(value) : value + } + + get avatarURL(): string | null { + return this.avatar ? this.client._formatImage(USER_AVATAR(this.id, this.avatar)) : this.defaultAvatarURL + } + + get bannerURL(): string | null { + if (!this.banner) { + return null + } + + return this.client._formatImage(BANNER(this.id, this.banner)) + } + + get defaultAvatar(): string { + // @ts-expect-error some eris magic at play here + return (this.discriminator % 5).toString() + } + + get defaultAvatarURL(): string { + return `${this.client.CDN_URL}${DEFAULT_USER_AVATAR(this.defaultAvatar)}.png` + } + + get mention(): string { + return `<@${this.id}>` + } + + get staticAvatarURL(): string { + return this.avatar ? this.client._formatImage(USER_AVATAR(this.id, this.avatar), 'jpg') : this.defaultAvatarURL + } + + update(data: DiscordUser): void { + if (data.avatar !== undefined) { + this.avatar = data.avatar + } + if (data.username !== undefined) { + this.username = data.username + } + if (data.discriminator !== undefined) { + this.discriminator = data.discriminator + } + if (data.public_flags !== undefined) { + this.publicFlags = data.public_flags + } + if (data.banner !== undefined) { + this.banner = data.banner + } + if (data.accent_color !== undefined) { + this.accentColor = data.accent_color + } + } + + /** Get the user's avatar with the given format and size */ + dynamicAvatarURL(format?: ImageFormat, size?: ImageSize): string { + return this.avatar ? this.client._formatImage(USER_AVATAR(this.id, this.avatar), format, size) : this.defaultAvatarURL + } + + /** Get the user's banner with the given format and size */ + dynamicBannerURL(format?: ImageFormat, size?: ImageSize): string | null { + return this.banner ? this.client._formatImage(BANNER(this.id, this.banner), format, size) : null + } + + /** + * Get a DM channel with the user, or create one if it does not exist + * @returns {Promise} + */ + async getDMChannel(): Promise { + return await this.client.getDMChannel.call(this.client, this.id) + } + + toJSON(props: string[] = []): Record { + return super.toJSON(['accentColor', 'avatar', 'banner', 'bot', 'discriminator', 'publicFlags', 'system', 'username', ...props]) + } +} + +export default User diff --git a/packages/client/src/gateway/Shard.ts b/packages/client/src/gateway/Shard.ts new file mode 100644 index 000000000..417146521 --- /dev/null +++ b/packages/client/src/gateway/Shard.ts @@ -0,0 +1,2030 @@ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +/* eslint-disable @typescript-eslint/restrict-plus-operands */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { + ChannelTypes, + GatewayOpcodes, + type DiscordChannel, + type DiscordChannelPinsUpdate, + type DiscordGatewayPayload, + type DiscordGuild, + type DiscordGuildBanAddRemove, + type DiscordGuildEmojisUpdate, + type DiscordGuildMemberAdd, + type DiscordGuildMemberRemove, + type DiscordGuildMembersChunk, + type DiscordGuildMemberUpdate, + type DiscordGuildRoleCreate, + type DiscordGuildRoleDelete, + type DiscordGuildRoleUpdate, + type DiscordHello, + type DiscordInteraction, + type DiscordInviteCreate, + type DiscordInviteDelete, + type DiscordMessage, + type DiscordMessageDelete, + type DiscordMessageDeleteBulk, + type DiscordMessageReactionAdd, + type DiscordMessageReactionRemove, + type DiscordMessageReactionRemoveAll, + type DiscordMessageReactionRemoveEmoji, + type DiscordPresenceUpdate, + type DiscordReady, + type DiscordStageInstance, + type DiscordThreadListSync, + type DiscordThreadMembersUpdate, + type DiscordTypingStart, + type DiscordUnavailableGuild, + type DiscordUser, + type DiscordVoiceServerUpdate, + type DiscordVoiceState, + type DiscordWebhookUpdate, +} from '@discordeno/types' +import EventEmitter from 'events' +import Base from '../Base.js' +import type Client from '../Client.js' +import Channel from '../Structures/channels/Channel.js' +import GuildChannel from '../Structures/channels/Guild.js' +import PrivateChannel from '../Structures/channels/Private.js' +import type StageChannel from '../Structures/channels/Stage.js' +import type TextChannel from '../Structures/channels/Text.js' +import type TextVoiceChannel from '../Structures/channels/TextVoice.js' +import ThreadChannel from '../Structures/channels/threads/Thread.js' +import type VoiceChannel from '../Structures/channels/Voice.js' +import Guild from '../Structures/guilds/Guild.js' +import Member from '../Structures/guilds/Member.js' +import Role from '../Structures/guilds/Role.js' +import StageInstance from '../Structures/guilds/StageInstance.js' +import UnavailableGuild from '../Structures/guilds/Unavailable.js' +import Interaction from '../Structures/interactions/Interaction.js' +import Invite from '../Structures/Invite.js' +import Message from '../Structures/Message.js' +import ExtendedUser from '../Structures/users/Extended.js' +import User from '../Structures/users/User.js' +import type { + ActivityPartial, + BotActivityType, + ClientPresence, + RequestGuildMembersOptions, + RequestMembersPromise, + SelfStatus, + TextableChannel, +} from '../typings.js' +import type BrowserWebSocket from '../utils/BrowserWebSocket.js' +import Bucket from '../utils/Bucket.js' + +export class Shard extends EventEmitter { + client: Client + connectAttempts: number = 0 + connecting = false + connectTimeout: number | null = null + discordServerTrace?: string[] + getAllUsersCount: { [guildID: string]: boolean } = {} + getAllUsersLength: number = 0 + getAllUsersQueue: unknown[] = [] + globalBucket!: Bucket + guildCreateTimeout: number | null = null + guildSyncQueue: string[] = [] + guildSyncQueueLength: number = 0 + heartbeatInterval: number | null = null + id: number + lastHeartbeatAck = false + lastHeartbeatReceived: number | null = null + lastHeartbeatSent: number | null = null + latency: number = 0 + preReady = false + presence!: ClientPresence + presenceUpdateBucket!: Bucket + ready = false + reconnectInterval: number = 0 + requestMembersPromise: { [s: string]: RequestMembersPromise } = {} + seq: number = 0 + sessionID: string | null = null + status: 'connecting' | 'disconnected' | 'handshaking' | 'identifying' | 'ready' | 'resuming' = 'disconnected' + + unsyncedGuilds: number = 0 + ws: WebSocket | BrowserWebSocket | null = null + + constructor(id: number, client: Client) { + super() + + this.id = id + this.client = client + + this.onPacket = this.onPacket.bind(this) + this._onWSOpen = this._onWSOpen.bind(this) + this._onWSMessage = this._onWSMessage.bind(this) + this._onWSError = this._onWSError.bind(this) + this._onWSClose = this._onWSClose.bind(this) + + this.hardReset() + } + + /** + * @deprecated Use .token instead. + */ + get _token(): string { + return this.token + } + + get token(): string { + return this.client.token + } + + checkReady() { + if (!this.ready) { + this.ready = true + super.emit('ready') + } + } + + /** Tells the shard to connect */ + connect() { + if (this.ws && this.ws.readyState != WebSocket.CLOSED) { + this.emit('error', new Error('Existing connection detected'), this.id) + return + } + ++this.connectAttempts + this.connecting = true + return this.initializeWS() + } + + createGuild(guild: Guild) { + this.client.guildShardMap[guild.id] = this.id + this.client.guilds.set(guild.id, guild) + + return guild + } + + /** Disconnects the shard */ + disconnect(options: { reconnect?: boolean | 'auto' } = {}, error?: Error) { + if (!this.ws) { + return + } + + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null + } + + if (this.ws.readyState !== WebSocket.CLOSED) { + this.ws.removeEventListener('message', this._onWSMessage) + this.ws.removeEventListener('close', this._onWSClose) + try { + if (options.reconnect && this.sessionID) { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.close(4901, 'Eris: reconnect') + } else { + this.emit('debug', `Terminating websocket (state: ${this.ws.readyState})`, this.id) + this.ws.close(1000, `Terminating websocket (state: ${this.ws.readyState})`) + } + } else { + this.ws.close(1000, 'Eris: normal') + } + } catch (err) { + this.emit('error', err, this.id) + } + } + this.ws = null + this.reset() + + if (error) { + this.emit('error', error, this.id) + } + + super.emit('disconnect', error) + + if (this.sessionID && this.connectAttempts >= this.client.options.maxResumeAttempts) { + this.emit('debug', `Automatically invalidating session due to excessive resume attempts | Attempt ${this.connectAttempts}`, this.id) + this.sessionID = null + } + + if (options.reconnect === 'auto' && this.client.options.autoreconnect) { + if (this.sessionID) { + this.emit('debug', `Immediately reconnecting for potential resume | Attempt ${this.connectAttempts}`, this.id) + this.client.shards.connect(this) + } else { + this.emit('debug', `Queueing reconnect in ${this.reconnectInterval}ms | Attempt ${this.connectAttempts}`, this.id) + setTimeout(() => { + this.client.shards.connect(this) + }, this.reconnectInterval) + this.reconnectInterval = Math.min(Math.round(this.reconnectInterval * (Math.random() * 2 + 1)), 30000) + } + } else if (!options.reconnect) { + this.hardReset() + } + } + + /** + * Update the bot's AFK status. + */ + editAFK(afk: boolean) { + this.presence.afk = !!afk + + this.sendStatusUpdate() + } + + /** + * Updates the bot's status on all guilds the shard is in + */ + editStatus(status: SelfStatus, activities?: Array> | ActivityPartial) { + if (activities === undefined && typeof status === 'object') { + activities = status + // @ts-expect-error + status = undefined + } + if (status) { + this.presence.status = status + } + if (activities === null) { + activities = [] + } else if (activities && !Array.isArray(activities)) { + activities = [activities] + } + if (activities !== undefined) { + if (activities.length > 0 && !activities[0].hasOwnProperty('type')) { + activities[0].type = activities[0].url ? 1 : 0 + } + this.presence.activities = activities + } + + this.sendStatusUpdate() + } + + emit(event: string, ...args: any[]) { + this.client.emit.call(this.client, event, ...args) + if (event !== 'error' || this.listeners('error').length > 0) { + super.emit.call(this, event, ...args) + } + } + + getGuildMembers(guildID: string, timeout: number) { + if (this.getAllUsersCount.hasOwnProperty(guildID)) { + throw new Error('Cannot request all members while an existing request is processing') + } + this.getAllUsersCount[guildID] = true + // Using intents, request one guild at a time + if (this.client.options.intents) { + if (!(this.client.options.intents & Intents.GuildMembers)) { + throw new Error('Cannot request all members without guildMembers intent') + } + this.requestGuildMembers([guildID], timeout) + } else { + if (this.getAllUsersLength + 3 + guildID.length > 4048) { + // 4096 - "{\"op\":8,\"d\":{\"guild_id\":[],\"query\":\"\",\"limit\":0}}".length + 1 for lazy comma offset + this.requestGuildMembers(this.getAllUsersQueue) + this.getAllUsersQueue = [guildID] + this.getAllUsersLength = 1 + guildID.length + 3 + } else { + this.getAllUsersQueue.push(guildID) + this.getAllUsersLength += guildID.length + 3 + } + } + } + + hardReset() { + this.reset() + this.seq = 0 + this.sessionID = null + this.reconnectInterval = 1000 + this.connectAttempts = 0 + this.ws = null + this.heartbeatInterval = null + this.guildCreateTimeout = null + this.globalBucket = new Bucket(120, 60000, { reservedTokens: 5 }) + this.presenceUpdateBucket = new Bucket(5, 20000) + this.presence = JSON.parse(JSON.stringify(this.client.presence)) + } + + heartbeat(normal?: boolean) { + // Can only heartbeat after identify/resume succeeds, session will be killed otherwise, discord/discord-api-docs#1619 + if (this.status === 'resuming' || this.status === 'identifying') { + return + } + if (normal) { + if (!this.lastHeartbeatAck) { + this.emit( + 'debug', + 'Heartbeat timeout; ' + + JSON.stringify({ + lastReceived: this.lastHeartbeatReceived, + lastSent: this.lastHeartbeatSent, + interval: this.heartbeatInterval, + status: this.status, + timestamp: Date.now(), + }), + ) + return this.disconnect( + { + reconnect: 'auto', + }, + new Error("Server didn't acknowledge previous heartbeat, possible lost connection"), + ) + } + this.lastHeartbeatAck = false + } + this.lastHeartbeatSent = Date.now() + this.sendWS(GatewayOpcodes.Heartbeat, this.seq, true) + } + + identify() { + this.status = 'identifying' + const identify = { + token: this.client.token, + v: API_VERSION, + compress: !!this.client.options.compress, + large_threshold: this.client.options.largeThreshold, + intents: this.client.options.intents, + properties: { + os: process.platform, + browser: 'Eris', + device: 'Eris', + }, + shard: this.client.options.maxShards > 1 ? [this.id, this.client.options.maxShards] : undefined, + presence: this.presence.status ? this.presence : undefined, + } + this.sendWS(GatewayOpcodes.Identify, identify) + } + + initializeWS() { + if (!this.token) { + return this.disconnect(null, new Error('Token not specified')) + } + + this.status = 'connecting' + if (this.client.options.compress) { + this.emit('debug', 'Initializing zlib-sync-based compression') + } + this.ws = new WebSocket(this.client.gatewayURL, this.client.options.ws) + this.ws.addEventListener('open', this._onWSOpen) + this.ws.addEventListener('message', this._onWSMessage) + this.ws.addEventListener('error', this._onWSError) + this.ws.addEventListener('close', this._onWSClose) + + this.connectTimeout = setTimeout(() => { + if (this.connecting) { + this.disconnect( + { + reconnect: 'auto', + }, + new Error('Connection timeout'), + ) + } + }, this.client.options.connectionTimeout) + } + + onPacket(packet: DiscordGatewayPayload) { + if (this.listeners('rawWS').length > 0 || this.client.listeners('rawWS').length) { + this.emit('rawWS', packet, this.id) + } + + if (packet.s) { + if (packet.s > this.seq + 1 && this.ws && this.status !== 'resuming') { + this.emit('warn', `Non-consecutive sequence (${this.seq} -> ${packet.s})`, this.id) + } + this.seq = packet.s + } + + switch (packet.op) { + case GatewayOpcodes.Dispatch: { + if (!this.client.options.disableEvents[packet.t]) { + this.wsEvent(packet) + } + break + } + case GatewayOpcodes.Heartbeat: { + this.heartbeat() + break + } + case GatewayOpcodes.InvalidSession: { + this.seq = 0 + this.sessionID = null + this.emit('warn', 'Invalid session, reidentifying!', this.id) + this.identify() + break + } + case GatewayOpcodes.Reconnect: { + this.emit('debug', 'Reconnecting due to server request', this.id) + this.disconnect({ + reconnect: 'auto', + }) + break + } + case GatewayOpcodes.Hello: { + if ((packet.d as DiscordHello).heartbeat_interval > 0) { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + } + this.heartbeatInterval = setInterval(() => this.heartbeat(true), (packet.d as DiscordHello).heartbeat_interval) + } + + this.discordServerTrace = packet.d._trace + this.connecting = false + if (this.connectTimeout) { + clearTimeout(this.connectTimeout) + } + this.connectTimeout = null + + if (this.sessionID) { + this.resume() + } else { + this.identify() + // Cannot heartbeat when resuming, discord/discord-api-docs#1619 + this.heartbeat() + } + + this.emit('hello', packet.d._trace, this.id) + break /* eslint-enable no-unreachable */ + } + case GatewayOpcodes.HeartbeatACK: { + this.lastHeartbeatAck = true + this.lastHeartbeatReceived = Date.now() + this.latency = this.lastHeartbeatReceived - (this.lastHeartbeatSent ?? 0) + break + } + default: { + this.emit('unknown', packet, this.id) + break + } + } + } + + async requestGuildMembers(guildID: string, options?: RequestGuildMembersOptions) { + const opts = { + guild_id: guildID, + limit: (options && options.limit) ?? 0, + user_ids: options && options.user_ids, + query: options && options.query, + nonce: Date.now().toString() + Math.random().toString(36), + presences: options && options.presences, + } + if (!opts.user_ids && !opts.query) { + opts.query = '' + } + if (!opts.query && !opts.user_ids && this.client.options.intents && !(this.client.options.intents & Intents.GuildMembers)) { + throw new Error('Cannot request all members without guildMembers intent') + } + if (opts.presences && this.client.options.intents && !(this.client.options.intents & Intents.GuildPresences)) { + throw new Error('Cannot request members presences without guildPresences intent') + } + if (opts.user_ids && opts.user_ids.length > 100) { + throw new Error('Cannot request more than 100 users by their ID') + } + this.sendWS(GatewayOpcodes.RequestGuildMembers, opts) + return await new Promise( + (res) => + (this.requestMembersPromise[opts.nonce] = { + res, + received: 0, + members: [], + timeout: setTimeout(() => { + res(this.requestMembersPromise[opts.nonce].members) + delete this.requestMembersPromise[opts.nonce] + }, (options && options.timeout) ?? this.client.options.requestTimeout), + }), + ) + } + + reset() { + this.connecting = false + this.ready = false + this.preReady = false + if (this.requestMembersPromise !== undefined) { + for (const guildID in this.requestMembersPromise) { + if (!this.requestMembersPromise.hasOwnProperty(guildID)) { + continue + } + clearTimeout(this.requestMembersPromise[guildID].timeout) + this.requestMembersPromise[guildID].res(this.requestMembersPromise[guildID].received) + } + } + this.requestMembersPromise = {} + this.getAllUsersCount = {} + this.getAllUsersQueue = [] + this.getAllUsersLength = 1 + this.guildSyncQueue = [] + this.guildSyncQueueLength = 1 + this.unsyncedGuilds = 0 + this.latency = Infinity + this.lastHeartbeatAck = true + this.lastHeartbeatReceived = null + this.lastHeartbeatSent = null + this.status = 'disconnected' + if (this.connectTimeout) { + clearTimeout(this.connectTimeout) + } + this.connectTimeout = null + } + + restartGuildCreateTimeout() { + if (this.guildCreateTimeout) { + clearTimeout(this.guildCreateTimeout) + this.guildCreateTimeout = null + } + if (!this.ready) { + if (this.client.unavailableGuilds.size === 0 && this.unsyncedGuilds === 0) { + return this.checkReady() + } + this.guildCreateTimeout = setTimeout(() => { + this.checkReady() + }, this.client.options.guildCreateTimeout) + } + } + + resume() { + this.status = 'resuming' + this.sendWS(GatewayOpcodes.Resume, { + token: this.client.token, + session_id: this.sessionID, + seq: this.seq, + }) + } + + sendStatusUpdate() { + this.sendWS(GatewayOpcodes.PresenceUpdate, { + activities: this.presence.activities, + afk: false, + since: this.presence.status === 'idle' ? Date.now() : 0, + status: this.presence.status, + }) + } + + sendWS(op: number, _data: Record, priority = false) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + let i = 0 + let waitFor = 1 + const func = () => { + if (++i >= waitFor && this.ws && this.ws.readyState === WebSocket.OPEN) { + const data = JSON.stringify({ op, d: _data }) + this.ws.send(data) + if (_data.token) { + delete _data.token + } + this.emit('debug', JSON.stringify({ op, d: _data }), this.id) + } + } + if (op === GatewayOpcodes.PresenceUpdate) { + ++waitFor + this.presenceUpdateBucket.queue(func, priority) + } + this.globalBucket.queue(func, priority) + } + } + + wsEvent(pkt: Required) { + switch ( + pkt.t /* eslint-disable no-redeclare */ // (╯°□°)╯︵ ┻━┻ + ) { + case 'PRESENCE_UPDATE': { + const packet = pkt as Omit & { + d: DiscordPresenceUpdate + } + + if (packet.d.user.username !== undefined) { + let user = this.client.users.get(packet.d.user.id) + let oldUser = null + if ( + user && + (user.username !== packet.d.user.username || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar) + ) { + oldUser = { + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + } + } + if (!user || oldUser) { + user = this.client.users.update(new User(packet.d.user, this.client), this.client) + this.emit('userUpdate', user, oldUser) + } + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', 'Rogue presence update: ' + JSON.stringify(packet), this.id) + break + } + let member = guild.members.get((packet.d.id = packet.d.user.id)) + let oldPresence = null + if (member) { + oldPresence = { + activities: member.activities, + clientStatus: member.clientStatus, + status: member.status, + } + } + if ((!member && packet.d.user.username) || oldPresence) { + member = guild.members.update(packet.d, guild) + this.emit('presenceUpdate', member, oldPresence) + } + break + } + case 'VOICE_STATE_UPDATE': { + const packet = pkt as Omit & { + d: DiscordVoiceState + } + + // (╯°□°)╯︵ ┻━┻ + if (packet.d.guild_id && packet.d.user_id === this.client.id) { + const voiceConnection = this.client.voiceConnections.get(packet.d.guild_id) + if (voiceConnection) { + if (packet.d.channel_id === null) { + this.client.voiceConnections.leave(packet.d.guild_id) + } else if (voiceConnection.channelID !== packet.d.channel_id) { + voiceConnection.switchChannel(packet.d.channel_id, true) + } + } + } + if (packet.d.self_stream === undefined) { + packet.d.self_stream = false + } + if (packet.d.guild_id === undefined) { + packet.d.id = packet.d.user_id + if (packet.d.channel_id !== null) { + const channel = this.client.getChannel(packet.d.channel_id) + if (!channel.call && !channel.lastCall) { + this.emit('debug', new Error('VOICE_STATE_UPDATE for untracked call')) + break + } + ;(channel.call || channel.lastCall).voiceStates.update(packet.d) + } + break + } + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + break + } + if (guild.pendingVoiceStates) { + guild.pendingVoiceStates.push(packet.d) + break + } + let member = guild.members.get((packet.d.id = packet.d.user_id)) + if (!member) { + if (!packet.d.member) { + this.emit( + 'voiceStateUpdate', + { + id: packet.d.user_id, + voiceState: { + deaf: packet.d.deaf, + mute: packet.d.mute, + selfDeaf: packet.d.self_deaf, + selfMute: packet.d.self_mute, + selfStream: packet.d.self_stream, + selfVideo: packet.d.self_video, + }, + }, + null, + ) + break + } + // Updates the member cache with this member for future events. + packet.d.member.id = packet.d.user_id + member = new Member(packet.d.member, guild, this.client) + guild.members.set(packet.d.user_id, member) + + const channel = guild.channels.find( + (channel) => + (channel.type === ChannelTypes.GuildVoice || channel.type === ChannelTypes.GuildStageVoice) && channel.voiceMembers.get(packet.d.id), + ) + if (channel) { + channel.voiceMembers.remove(packet.d) + this.emit('debug', 'VOICE_STATE_UPDATE member null but in channel: ' + packet.d.id, this.id) + } + } + const oldState = { + deaf: member.voiceState?.deaf, + mute: member.voiceState?.mute, + selfDeaf: member.voiceState?.selfDeaf, + selfMute: member.voiceState?.selfMute, + selfStream: member.voiceState?.selfStream, + selfVideo: member.voiceState?.selfVideo, + } + const oldChannelID = member.voiceState?.channelID + member.update(packet.d) + if (oldChannelID != packet.d.channel_id) { + let oldChannel: TextVoiceChannel | StageChannel | null, newChannel: TextVoiceChannel | StageChannel | null + if (oldChannelID) { + oldChannel = guild.channels.get(oldChannelID) as TextVoiceChannel | StageChannel + if (oldChannel && oldChannel.type !== ChannelTypes.GuildVoice && oldChannel.type !== ChannelTypes.GuildStageVoice) { + this.emit('warn', 'Old channel not a recognized voice channel: ' + oldChannelID, this.id) + oldChannel = null + } + } + if ( + packet.d.channel_id && + (newChannel = guild.channels.get(packet.d.channel_id) as TextVoiceChannel | StageChannel) && + (newChannel.type === ChannelTypes.GuildVoice || newChannel.type === ChannelTypes.GuildStageVoice) + ) { + // Welcome to Discord, where one can "join" text channels + if (oldChannel!) { + oldChannel.voiceMembers.remove(member) + this.emit('voiceChannelSwitch', newChannel.voiceMembers.add(member, guild), newChannel, oldChannel) + } else { + this.emit('voiceChannelJoin', newChannel.voiceMembers.add(member, guild), newChannel) + } + } else if (oldChannel!) { + oldChannel.voiceMembers.remove(member) + this.emit('voiceChannelLeave', member, oldChannel) + } + } + if ( + oldState.mute !== member.voiceState?.mute || + oldState.deaf !== member.voiceState?.deaf || + oldState.selfMute !== member.voiceState?.selfMute || + oldState.selfDeaf !== member.voiceState?.selfDeaf || + oldState.selfStream !== member.voiceState?.selfStream || + oldState.selfVideo !== member.voiceState?.selfVideo + ) { + this.emit('voiceStateUpdate', member, oldState) + } + break + } + case 'TYPING_START': { + const packet = pkt as Omit & { + d: DiscordTypingStart + } + + let member = null + const guild = this.client.guilds.get(packet.d.guild_id ?? '') + if (guild) { + member = guild.members.update(new Member({ ...packet.d.member!, id: packet.d.user_id }, guild, this.client)) + } + if (this.client.listeners('typingStart').length > 0) { + this.emit( + 'typingStart', + this.client.getChannel(packet.d.channel_id) ?? { + id: packet.d.channel_id, + }, + this.client.users.get(packet.d.user_id) ?? { id: packet.d.user_id }, + member, + ) + } + break + } + case 'MESSAGE_CREATE': { + const packet = pkt as Omit & { + d: DiscordMessage + } + + const channel = this.client.getChannel(packet.d.channel_id) + if (channel) { + // MESSAGE_CREATE just when deleting o.o + channel.lastMessageID = packet.d.id + + this.emit('messageCreate', channel.messages.add(new Message(packet.d, this.client))) + } else { + this.emit('messageCreate', new Message(packet.d, this.client)) + } + break + } + case 'MESSAGE_UPDATE': { + const packet = pkt as Omit & { + d: DiscordMessage + } + + const channel = this.client.getChannel(packet.d.channel_id) + if (!channel) { + packet.d.channel = { + id: packet.d.channel_id, + } + this.emit('messageUpdate', packet.d, null) + break + } + const message = channel.messages.get(packet.d.id) + let oldMessage = null + if (message) { + oldMessage = { + attachments: message.attachments, + channelMentions: message.channelMentions, + content: message.content, + editedTimestamp: message.editedTimestamp, + embeds: message.embeds, + flags: message.flags, + mentionedBy: message.mentionedBy, + mentions: message.mentions, + pinned: message.pinned, + roleMentions: message.roleMentions, + tts: message.tts, + } + } else if (!packet.d.timestamp) { + packet.d.channel = channel + this.emit('messageUpdate', packet.d, null) + break + } + this.emit('messageUpdate', channel.messages.update(new Message(packet.d, this.client)), oldMessage) + break + } + case 'MESSAGE_DELETE': { + const packet = pkt as Omit & { + d: DiscordMessageDelete + } + + const channel = this.client.getChannel(packet.d.channel_id) + + this.emit( + 'messageDelete', + (channel && channel.messages.remove(new Message(packet.d, this.client))) || { + id: packet.d.id, + channel: channel ?? { + id: packet.d.channel_id, + guild: packet.d.guild_id ? { id: packet.d.guild_id } : undefined, + }, + guildID: packet.d.guild_id, + }, + ) + break + } + case 'MESSAGE_DELETE_BULK': { + const packet = pkt as Omit & { + d: DiscordMessageDeleteBulk + } + + const channel = this.client.getChannel(packet.d.channel_id) + + this.emit( + 'messageDeleteBulk', + packet.d.ids.map( + (id) => + (channel && + channel.messages.remove({ + id, + })) || { + id, + channel: { + id: packet.d.channel_id, + guild: packet.d.guild_id ? { id: packet.d.guild_id } : undefined, + }, + guildID: packet.d.guild_id, + }, + ), + ) + break + } + case 'MESSAGE_REACTION_ADD': { + const packet = pkt as Omit & { + d: DiscordMessageReactionAdd + } + + const channel = this.client.getChannel(packet.d.channel_id) + let message: + | Message + | { + id: string + channel: TextableChannel | { id: string } + guildID?: string + } + | undefined + let member + if (channel) { + message = channel.messages.get(packet.d.message_id) + if (channel.guild) { + if (packet.d.member) { + // Updates the member cache with this member for future events. + packet.d.member.id = packet.d.user_id + member = channel.guild.members.update(packet.d.member, channel.guild) + } + } + } + if (message instanceof Message) { + const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name! + if (message.reactions[reaction]) { + ++message.reactions[reaction].count + if (packet.d.user_id === this.client.id) { + message.reactions[reaction].me = true + } + } else { + message.reactions[reaction] = { + count: 1, + me: packet.d.user_id === this.client.id, + } + } + } else { + message = { + id: packet.d.message_id, + channel: channel ?? { id: packet.d.channel_id }, + } + + if (packet.d.guild_id) { + message.guildID = packet.d.guild_id + if (!message.channel.guild) { + message.channel.guild = { id: packet.d.guild_id } + } + } + } + this.emit('messageReactionAdd', message, packet.d.emoji, member || { id: packet.d.user_id }) + break + } + case 'MESSAGE_REACTION_REMOVE': { + const packet = pkt as Omit & { + d: DiscordMessageReactionRemove + } + + const channel = this.client.getChannel(packet.d.channel_id) + let message: + | Message + | { + id: string + channel: TextableChannel | { id: string } + guildID?: string + } + | undefined + if (channel) { + message = channel.messages.get(packet.d.message_id) + } + if (message instanceof Message) { + const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name! + const reactionObj = message.reactions[reaction] + if (reactionObj) { + --reactionObj.count + if (reactionObj.count === 0) { + delete message.reactions[reaction] + } else if (packet.d.user_id === this.client.id) { + reactionObj.me = false + } + } + } else { + message = { + id: packet.d.message_id, + channel: channel ?? { id: packet.d.channel_id }, + } + + if (packet.d.guild_id) { + message.guildID = packet.d.guild_id + if (!message.channel.guild) { + message.channel.guild = { id: packet.d.guild_id } + } + } + } + + this.emit('messageReactionRemove', message, packet.d.emoji, packet.d.user_id) + break + } + case 'MESSAGE_REACTION_REMOVE_ALL': { + const packet = pkt as Omit & { + d: DiscordMessageReactionRemoveAll + } + + const channel = this.client.getChannel(packet.d.channel_id) + let message + if (channel) { + message = channel.messages.get(packet.d.message_id) + if (message) { + message.reactions = {} + } + } + if (!message) { + message = { + id: packet.d.message_id, + channel: channel ?? { id: packet.d.channel_id }, + } + if (packet.d.guild_id) { + message.guildID = packet.d.guild_id + if (!message.channel.guild) { + message.channel.guild = { id: packet.d.guild_id } + } + } + } + + this.emit('messageReactionRemoveAll', message) + break + } + case 'MESSAGE_REACTION_REMOVE_EMOJI': { + const packet = pkt as Omit & { + d: DiscordMessageReactionRemoveEmoji + } + + const channel = this.client.getChannel(packet.d.channel_id) + let message + if (channel) { + message = channel.messages.get(packet.d.message_id) + if (message) { + const reaction = packet.d.emoji.id ? `${packet.d.emoji.name}:${packet.d.emoji.id}` : packet.d.emoji.name! + delete message.reactions[reaction] + } + } + if (!message) { + message = { + id: packet.d.message_id, + channel: channel ?? { id: packet.d.channel_id }, + } + if (packet.d.guild_id) { + message.guildID = packet.d.guild_id + if (!message.channel.guild) { + message.channel.guild = { id: packet.d.guild_id } + } + } + } + + this.emit('messageReactionRemoveEmoji', message, packet.d.emoji) + break + } + case 'GUILD_MEMBER_ADD': { + const packet = pkt as Omit & { + d: DiscordGuildMemberAdd + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + // Eventual Consistency™ (╯°□°)╯︵ ┻━┻ + this.emit('debug', `Missing guild ${packet.d.guild_id} in GUILD_MEMBER_ADD`) + break + } + packet.d.id = packet.d.user.id + let x: number | undefined + guild.memberCount = (guild.memberCount ?? 0) + 1 + + this.emit('guildMemberAdd', guild, guild.members.add(new Member(packet.d, guild, this.client))) + break + } + case 'GUILD_MEMBER_UPDATE': { + const packet = pkt as Omit & { + d: DiscordGuildMemberUpdate + } + + // Check for member update if GuildPresences intent isn't set, to prevent emitting twice + if (!(this.client.options.intents & Intents.GuildPresences) && packet.d.user.username !== undefined) { + let user = this.client.users.get(packet.d.user.id) + let oldUser = null + if ( + user && + (user.username !== packet.d.user.username || user.discriminator !== packet.d.user.discriminator || user.avatar !== packet.d.user.avatar) + ) { + oldUser = { + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + } + } + if (!user || oldUser) { + user = this.client.users.update(new User(packet.d.user, this.client)) + this.emit('userUpdate', user, oldUser) + } + } + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in GUILD_MEMBER_UPDATE`) + break + } + let member = guild.members.get((packet.d.id = packet.d.user.id)) + let oldMember = null + if (member) { + oldMember = { + avatar: member.avatar, + communicationDisabledUntil: member.communicationDisabledUntil, + roles: member.roles, + nick: member.nick, + premiumSince: member.premiumSince, + pending: member.pending, + } + } + member = guild.members.update(new Member(packet.d, guild, this.client)) + + this.emit('guildMemberUpdate', guild, member, oldMember) + break + } + case 'GUILD_MEMBER_REMOVE': { + const packet = pkt as Omit & { + d: DiscordGuildMemberRemove + } + + if (packet.d.user.id === this.client.id) { + // The bot is probably leaving + break + } + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + break + } + guild.memberCount = (guild.memberCount ?? 0) - 1 + packet.d.id = packet.d.user.id + + this.emit( + 'guildMemberRemove', + guild, + guild.members.remove(new Member(packet.d, guild, this.client)) || { + id: packet.d.id, + user: new User(packet.d.user, this.client), + }, + ) + break + } + case 'GUILD_CREATE': { + const packet = pkt as Omit & { + d: DiscordGuild + } + + if (!packet.d.unavailable) { + const guild = this.createGuild(new Guild(packet.d, this.client)) + if (this.ready) { + if (this.client.unavailableGuilds.remove(new Guild(packet.d, this.client))) { + this.emit('guildAvailable', guild) + } else { + this.emit('guildCreate', guild) + } + } else { + this.client.unavailableGuilds.remove(new Guild(packet.d, this.client)) + this.restartGuildCreateTimeout() + } + } else { + this.client.guilds.remove(new Guild(packet.d, this.client)) + + this.emit('unavailableGuildCreate', this.client.unavailableGuilds.add(new UnavailableGuild(packet.d, this.client))) + } + break + } + case 'GUILD_UPDATE': { + const packet = pkt as Omit & { + d: DiscordGuild + } + + const guild = this.client.guilds.get(packet.d.id) + if (!guild) { + this.emit('debug', `Guild ${packet.d.id} undefined in GUILD_UPDATE`) + break + } + const oldGuild = { + afkChannelID: guild.afkChannelID, + afkTimeout: guild.afkTimeout, + banner: guild.banner, + defaultNotifications: guild.defaultNotifications, + description: guild.description, + discoverySplash: guild.discoverySplash, + emojis: guild.emojis, + explicitContentFilter: guild.explicitContentFilter, + features: guild.features, + icon: guild.icon, + large: guild.large, + maxMembers: guild.maxMembers, + maxVideoChannelUsers: guild.maxVideoChannelUsers, + mfaLevel: guild.mfaLevel, + name: guild.name, + nsfw: guild.nsfw, + nsfwLevel: guild.nsfwLevel, + ownerID: guild.ownerID, + preferredLocale: guild.preferredLocale, + premiumSubscriptionCount: guild.premiumSubscriptionCount, + premiumTier: guild.premiumTier, + publicUpdatesChannelID: guild.publicUpdatesChannelID, + rulesChannelID: guild.rulesChannelID, + splash: guild.splash, + stickers: guild.stickers, + systemChannelFlags: guild.systemChannelFlags, + systemChannelID: guild.systemChannelID, + vanityURL: guild.vanityURL, + verificationLevel: guild.verificationLevel, + } + + this.emit('guildUpdate', this.client.guilds.update(new Guild(packet.d, this.client)), oldGuild) + break + } + case 'GUILD_DELETE': { + const packet = pkt as Omit & { + d: DiscordUnavailableGuild + } + + const voiceConnection = this.client.voiceConnections.get(packet.d.id) + if (voiceConnection) { + if (voiceConnection.channelID) { + this.client.leaveVoiceChannel(voiceConnection.channelID) + } else { + this.client.voiceConnections.leave(packet.d.id) + } + } + + delete this.client.guildShardMap[packet.d.id] + const guild = this.client.guilds.remove(packet.d) + if (guild) { + // Discord sends GUILD_DELETE for guilds that were previously unavailable in READY + guild.channels.forEach((channel) => { + delete this.client.channelGuildMap[channel.id] + }) + } + if (packet.d.unavailable) { + this.emit('guildUnavailable', this.client.unavailableGuilds.add(new UnavailableGuild(packet.d, this.client))) + } else { + this.emit( + 'guildDelete', + guild ?? { + id: packet.d.id, + }, + ) + } + break + } + case 'GUILD_BAN_ADD': { + const packet = pkt as Omit & { + d: DiscordGuildBanAddRemove + } + + this.emit('guildBanAdd', this.client.guilds.get(packet.d.guild_id), this.client.users.update(new User(packet.d.user, this.client))) + break + } + case 'GUILD_BAN_REMOVE': { + const packet = pkt as Omit & { + d: DiscordGuildBanAddRemove + } + + this.emit('guildBanRemove', this.client.guilds.get(packet.d.guild_id), this.client.users.update(new User(packet.d.user, this.client))) + break + } + case 'GUILD_ROLE_CREATE': { + const packet = pkt as Omit & { + d: DiscordGuildRoleCreate + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in GUILD_ROLE_CREATE`) + break + } + this.emit('guildRoleCreate', guild, guild.roles.add(new Role(packet.d.role, guild))) + break + } + case 'GUILD_ROLE_UPDATE': { + const packet = pkt as Omit & { + d: DiscordGuildRoleUpdate + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Guild ${packet.d.guild_id} undefined in GUILD_ROLE_UPDATE`) + break + } + const role = guild.roles.add(new Role(packet.d.role, guild)) + if (!role) { + this.emit('debug', `Role ${packet.d.role} in guild ${packet.d.guild_id} undefined in GUILD_ROLE_UPDATE`) + break + } + const oldRole = { + color: role.color, + hoist: role.hoist, + icon: role.icon, + managed: role.managed, + mentionable: role.mentionable, + name: role.name, + permissions: role.permissions, + position: role.position, + tags: role.tags, + unicodeEmoji: role.unicodeEmoji, + } + + this.emit('guildRoleUpdate', guild, guild.roles.update(new Role(packet.d.role, guild)), oldRole) + break + } + case 'GUILD_ROLE_DELETE': { + const packet = pkt as Omit & { + d: DiscordGuildRoleDelete + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in GUILD_ROLE_DELETE`) + break + } + if (!guild.roles.has(packet.d.role_id)) { + this.emit('debug', `Missing role ${packet.d.role_id} in GUILD_ROLE_DELETE`) + break + } + this.emit('guildRoleDelete', guild, guild.roles.remove({ id: packet.d.role_id })) + break + } + case 'INVITE_CREATE': { + const packet = pkt as Omit & { + d: DiscordInviteCreate + } + + const guild = this.client.guilds.get(packet.d.guild_id ?? '') + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in INVITE_CREATE`) + break + } + const channel = this.client.getChannel(packet.d.channel_id) + if (!channel) { + this.emit('debug', `Missing channel ${packet.d.channel_id} in INVITE_CREATE`) + break + } + + this.emit( + 'inviteCreate', + guild, + new Invite( + { + ...packet.d, + guild, + channel, + }, + this.client, + ), + ) + break + } + case 'INVITE_DELETE': { + const packet = pkt as Omit & { + d: DiscordInviteDelete + } + + const guild = this.client.guilds.get(packet.d.guild_id ?? '') + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in INVITE_DELETE`) + break + } + const channel = this.client.getChannel(packet.d.channel_id) + if (!channel) { + this.emit('debug', `Missing channel ${packet.d.channel_id} in INVITE_DELETE`) + break + } + + this.emit( + 'inviteDelete', + guild, + new Invite( + { + ...packet.d, + guild, + channel, + }, + this.client, + ), + ) + break + } + case 'CHANNEL_CREATE': { + const packet = pkt as Omit & { + d: DiscordChannel + } + + const channel = Channel.from(packet.d, this.client) + if (packet.d.guild_id) { + if (!channel.guild) { + channel.guild = this.client.guilds.get(packet.d.guild_id) + if (!channel.guild) { + this.emit('debug', `Received CHANNEL_CREATE for channel in missing guild ${packet.d.guild_id}`) + break + } + } + channel.guild.channels.add(channel, this.client) + this.client.channelGuildMap[packet.d.id] = packet.d.guild_id + + this.emit('channelCreate', channel) + } else { + this.emit('warn', new Error('Unhandled CHANNEL_CREATE type: ' + JSON.stringify(packet, null, 2))) + break + } + break + } + case 'CHANNEL_UPDATE': { + const packet = pkt as Omit & { + d: DiscordChannel + } + + let channel = this.client.getChannel(packet.d.id) as GuildChannel + if (!channel) { + break + } + let oldChannel + const oldType = channel.type + + if (channel instanceof GuildChannel) { + oldChannel = { + bitrate: (channel as VoiceChannel).bitrate, + name: channel.name, + nsfw: channel.nsfw, + parentID: channel.parentID, + permissionOverwrites: channel.permissionOverwrites, + position: channel.position, + rateLimitPerUser: (channel as TextChannel).rateLimitPerUser, + rtcRegion: (channel as VoiceChannel).rtcRegion, + topic: (channel as TextChannel).topic, + type: channel.type, + userLimit: (channel as VoiceChannel).userLimit, + videoQualityMode: (channel as VoiceChannel).videoQualityMode, + } + } else { + this.emit('warn', `Unexpected CHANNEL_UPDATE for channel ${packet.d.id} with type ${oldType}`) + } + if (oldType === packet.d.type) { + channel.update(packet.d) + } else { + this.emit('debug', `Channel ${packet.d.id} changed from type ${oldType} to ${packet.d.type}`) + const newChannel = Channel.from(packet.d, this.client) as GuildChannel + if (packet.d.guild_id) { + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Received CHANNEL_UPDATE for channel in missing guild ${packet.d.guild_id}`) + break + } + guild.channels.remove(channel) + guild.channels.add(newChannel, this.client) + } else if (channel instanceof PrivateChannel) { + this.client.privateChannels.remove(channel) + this.client.privateChannels.add(newChannel as unknown as PrivateChannel, this.client) + } else { + this.emit('warn', new Error('Unhandled CHANNEL_UPDATE type: ' + JSON.stringify(packet, null, 2))) + break + } + channel = newChannel + } + + this.emit('channelUpdate', channel, oldChannel) + break + } + case 'CHANNEL_DELETE': { + const packet = pkt as Omit & { + d: DiscordChannel + } + + if (packet.d.type === ChannelTypes.DM || packet.d.type === undefined) { + if (this.id === 0) { + const channel = this.client.privateChannels.remove(new PrivateChannel(packet.d, this.client)) + if (channel) { + delete this.client.privateChannelMap[channel.recipient?.id ?? ''] + + this.emit('channelDelete', channel) + } + } + } else if (packet.d.guild_id) { + delete this.client.channelGuildMap[packet.d.id] + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in CHANNEL_DELETE`) + break + } + const channel = guild.channels.remove(new GuildChannel(packet.d, this.client)) + if (!channel) { + break + } + if (channel.type === ChannelTypes.GuildVoice || channel.type === ChannelTypes.GuildStageVoice) { + channel.voiceMembers.forEach((member) => { + channel.voiceMembers.remove(member) + this.emit('voiceChannelLeave', member, channel) + }) + } + this.emit('channelDelete', channel) + } else { + this.emit('warn', new Error('Unhandled CHANNEL_DELETE type: ' + JSON.stringify(packet, null, 2))) + } + break + } + case 'GUILD_MEMBERS_CHUNK': { + const packet = pkt as Omit & { + d: DiscordGuildMembersChunk + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit( + 'debug', + `Received GUILD_MEMBERS_CHUNK, but guild ${packet.d.guild_id} is ` + + (this.client.unavailableGuilds.has(packet.d.guild_id) ? 'unavailable' : 'missing'), + this.id, + ) + break + } + + const members = packet.d.members.map((member) => { + member.id = member.user.id + return guild.members.add(new Member(member, guild, this.client)) + }) + + if (packet.d.presences) { + packet.d.presences.forEach((presence) => { + const member = guild.members.get(presence.user.id) + if (member) { + member.update(presence) + } + }) + } + + if (this.requestMembersPromise.hasOwnProperty(packet.d.nonce ?? '')) { + this.requestMembersPromise[packet.d.nonce ?? ''].members.push(...members) + } + + if (packet.d.chunk_index >= packet.d.chunk_count - 1) { + if (this.requestMembersPromise.hasOwnProperty(packet.d.nonce ?? '')) { + clearTimeout(this.requestMembersPromise[packet.d.nonce ?? ''].timeout) + this.requestMembersPromise[packet.d.nonce ?? ''].res(this.requestMembersPromise[packet.d.nonce ?? ''].members) + delete this.requestMembersPromise[packet.d.nonce ?? ''] + } + if (this.getAllUsersCount.hasOwnProperty(guild.id)) { + delete this.getAllUsersCount[guild.id] + this.checkReady() + } + } + + this.emit('guildMemberChunk', guild, members) + + this.lastHeartbeatAck = true + + break + } + case 'RESUMED': + case 'READY': { + const packet = pkt as Omit & { + d: DiscordReady + } + + this.connectAttempts = 0 + this.reconnectInterval = 1000 + + this.connecting = false + if (this.connectTimeout) { + clearTimeout(this.connectTimeout) + } + this.connectTimeout = null + this.status = 'ready' + this.presence.status = 'online' + this.client.shards._readyPacketCB(this.id) + + if (packet.t === 'RESUMED') { + // Can only heartbeat after resume succeeds, discord/discord-api-docs#1619 + this.heartbeat() + + this.preReady = true + this.ready = true + + /** + * Fired when a shard finishes resuming + * @event Shard#resume + */ + super.emit('resume') + break + } + + this.client.user = this.client.users.update(new ExtendedUser(packet.d.user, this.client), this.client) + + if (!this.client.token.startsWith('Bot ')) { + this.client.token = 'Bot ' + this.client.token + } + + if (packet.d._trace) { + this.discordServerTrace = packet.d._trace + } + + this.sessionID = packet.d.session_id + + packet.d.guilds.forEach((guild) => { + if (guild.unavailable) { + this.client.guilds.delete(guild.id) + this.client.unavailableGuilds.set(guild.id, new UnavailableGuild(guild, this.client)) + } else { + this.client.unavailableGuilds.remove(this.createGuild(guild)) + } + }) + + this.client.application = packet.d.application + + this.preReady = true + + this.emit('shardPreReady', this.id) + + if (this.client.unavailableGuilds.size > 0 && packet.d.guilds.length > 0) { + this.restartGuildCreateTimeout() + } else { + this.checkReady() + } + + break + } + case 'VOICE_SERVER_UPDATE': { + const packet = pkt as Omit & { + d: DiscordVoiceServerUpdate + } + + packet.d.session_id = this.sessionID + packet.d.user_id = this.client.id + packet.d.shard = this + + this.client.voiceConnections.voiceServerUpdate(packet.d) + + break + } + case 'USER_UPDATE': { + const packet = pkt as Omit & { + d: DiscordUser + } + + let user = this.client.users.get(packet.d.id) + let oldUser = null + if (user) { + oldUser = { + username: user.username, + discriminator: user.discriminator, + avatar: user.avatar, + } + } + user = this.client.users.update(new User(packet.d, this.client)) + this.emit('userUpdate', user, oldUser) + break + } + case 'GUILD_EMOJIS_UPDATE': { + const packet = pkt as Omit & { + d: DiscordGuildEmojisUpdate + } + + const guild = this.client.guilds.get(packet.d.guild_id) + let oldEmojis = null + let emojis = packet.d.emojis + if (guild) { + oldEmojis = guild.emojis + guild.update(packet.d) + emojis = guild.emojis + } + + this.emit('guildEmojisUpdate', guild ?? { id: packet.d.guild_id }, emojis, oldEmojis) + break + } + // TODO: Add this when dd has the support for this event + // case 'GUILD_STICKERS_UPDATE': { + // const packet = pkt as Omit & { d: {} }; + + // const guild = this.client.guilds.get(packet.d.guild_id); + // let oldStickers = null; + // let stickers = packet.d.stickers; + // if (guild) { + // oldStickers = guild.stickers; + // guild.update(packet.d); + // stickers = guild.stickers; + // } + // this.emit('guildStickersUpdate', guild || { id: packet.d.guild_id }, stickers, oldStickers); + // break; + // } + + case 'CHANNEL_PINS_UPDATE': { + const packet = pkt as Omit & { + d: DiscordChannelPinsUpdate + } + + const channel = this.client.getChannel(packet.d.channel_id) + if (!channel) { + this.emit('debug', `CHANNEL_PINS_UPDATE target channel ${packet.d.channel_id} not found`) + break + } + const oldTimestamp = channel.lastPinTimestamp + channel.lastPinTimestamp = Date.parse(packet.d.last_pin_timestamp ?? '') + + this.emit('channelPinUpdate', channel, channel.lastPinTimestamp, oldTimestamp) + break + } + case 'WEBHOOKS_UPDATE': { + const packet = pkt as Omit & { + d: DiscordWebhookUpdate + } + + this.emit('webhooksUpdate', { + channelID: packet.d.channel_id, + guildID: packet.d.guild_id, + }) + break + } + case 'THREAD_CREATE': { + const packet = pkt as Omit & { + d: DiscordChannel + } + + const channel = Channel.from(packet.d, this.client) as ThreadChannel + if (!channel.guild) { + channel.guild = this.client.guilds.get(packet.d.guild_id ?? '')! + if (!channel.guild) { + this.emit('debug', `Received THREAD_CREATE for channel in missing guild ${packet.d.guild_id}`) + break + } + } + channel.guild.threads.add(channel, this.client) + this.client.threadGuildMap[packet.d.id] = packet.d.guild_id ?? '' + + this.emit('threadCreate', channel) + break + } + case 'THREAD_UPDATE': { + const packet = pkt as Omit & { + d: DiscordChannel + } + + const channel = this.client.getChannel(packet.d.id) + if (!channel) { + const thread = Channel.from(packet.d, this.client) as ThreadChannel + this.emit('threadUpdate', this.client.guilds.get(packet.d.guild_id ?? '')?.threads.add(thread, this.client), null) + this.client.threadGuildMap[packet.d.id] = packet.d.guild_id ?? '' + break + } + if (!(channel instanceof ThreadChannel)) { + this.emit('warn', `Unexpected THREAD_UPDATE for channel ${packet.d.id} with type ${channel.type}`) + break + } + const oldChannel = { + name: channel.name, + rateLimitPerUser: channel.rateLimitPerUser, + threadMetadata: channel.threadMetadata, + } + channel.update(packet.d) + + this.emit('threadUpdate', channel, oldChannel) + break + } + case 'THREAD_DELETE': { + const packet = pkt as Omit & { + d: Pick + } + + delete this.client.threadGuildMap[packet.d.id] + const guild = this.client.guilds.get(packet.d.guild_id ?? '') + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in THREAD_DELETE`) + break + } + const channel = guild.threads.get(packet.d.id) + guild.threads.delete(packet.d.id) + if (!channel) { + break + } + + this.emit('threadDelete', channel) + break + } + case 'THREAD_LIST_SYNC': { + const packet = pkt as Omit & { + d: DiscordThreadListSync + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in THREAD_LIST_SYNC`) + break + } + const deletedThreads = (packet.d.channel_ids ?? guild.threads.map((c) => c.id)) // REVIEW Is this a good name? + .filter((c) => !packet.d.threads.some((t) => t.id === c)) + .map((id) => guild.threads.remove({ id }) || { id }) + const activeThreads = packet.d.threads.map((t) => guild.threads.update(t, this.client)) + const joinedThreadsMember = packet.d.members.map((m) => guild.threads.get(m.id)?.members.update(m, this.client)) + + this.emit('threadListSync', guild, deletedThreads, activeThreads, joinedThreadsMember) + break + } + // TODO: Add this when dd has the support for this event + // case 'THREAD_MEMBER_UPDATE': { + // const channel = this.client.getChannel(packet.d.id); + // if (!channel) { + // this.emit('debug', `Missing channel ${packet.d.id} in THREAD_MEMBER_UPDATE`); + // break; + // } + // let oldMember = null; + // // Thanks Discord + // packet.d.thread_id = packet.d.id; + // let member = channel.members.get((packet.d.id = packet.d.user_id)); + // if (member) { + // oldMember = { + // flags: member.flags, + // }; + // } + // member = channel.members.update(packet.d, this.client); + // this.emit('threadMemberUpdate', channel, member, oldMember); + // break; + // } + case 'THREAD_MEMBERS_UPDATE': { + const packet = pkt as Omit & { + d: DiscordThreadMembersUpdate + } + + const channel = this.client.getChannel(packet.d.id) as unknown as ThreadChannel + if (!channel) { + this.emit('debug', `Missing channel ${packet.d.id} in THREAD_MEMBERS_UPDATE`) + break + } + channel.update(packet.d) + let addedMembers + let removedMembers + if (packet.d.added_members) { + addedMembers = packet.d.added_members.map((m) => { + if (m.presence) { + m.presence.id = m.presence.user.id + this.client.users.update(m.presence.user, this.client) + } + + m.thread_id = m.id + m.id = m.user_id + m.member.id = m.member.user.id + const guild = this.client.guilds.get(packet.d.guild_id) + if (guild) { + if (m.presence) { + guild.members.update(m.presence, guild) + } + guild.members.update(m.member, guild) + } + return channel.members.update(m, this.client) + }) + } + if (packet.d.removed_member_ids) { + removedMembers = packet.d.removed_member_ids.map((id) => channel.members.remove({ id }) ?? { id }) + } + + this.emit('threadMembersUpdate', channel, addedMembers ?? [], removedMembers ?? []) + break + } + case 'STAGE_INSTANCE_CREATE': { + const packet = pkt as Omit & { + d: DiscordStageInstance + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('debug', `Missing guild ${packet.d.guild_id} in STAGE_INSTANCE_CREATE`) + break + } + + this.emit('stageInstanceCreate', guild.stageInstances.add(new StageInstance(packet.d, this.client))) + break + } + case 'STAGE_INSTANCE_UPDATE': { + const packet = pkt as Omit & { + d: DiscordStageInstance + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('stageInstanceUpdate', packet.d, null) + break + } + const stageInstance = guild.stageInstances.get(packet.d.id) + let oldStageInstance = null + if (stageInstance) { + oldStageInstance = { + topic: stageInstance.topic, + } + } + + this.emit('stageInstanceUpdate', guild.stageInstances.update(new StageInstance(packet.d, this.client)), oldStageInstance) + break + } + case 'STAGE_INSTANCE_DELETE': { + const packet = pkt as Omit & { + d: DiscordStageInstance + } + + const guild = this.client.guilds.get(packet.d.guild_id) + if (!guild) { + this.emit('stageInstanceDelete', new StageInstance(packet.d, this.client)) + break + } + + this.emit('stageInstanceDelete', guild.stageInstances.remove(packet.d) || new StageInstance(packet.d, this.client)) + break + } + case 'GUILD_INTEGRATIONS_UPDATE': { + // Ignore this + break + } + case 'INTERACTION_CREATE': { + const packet = pkt as Omit & { + d: DiscordInteraction + } + + this.emit('interactionCreate', Interaction.from(packet.d, this.client)) + break + } + default: { + this.emit('unknown', pkt, this.id) + break + } + } /* eslint-enable no-redeclare */ + } + + _onWSClose(event: { code: number; reason: string }) { + let { code, reason } = event + + reason = reason.toString() + this.emit( + 'debug', + 'WS disconnected: ' + + JSON.stringify({ + code, + reason, + status: this.status, + }), + ) + let err: (Error & { code?: number }) | null = !code || code === 1000 ? null : new Error(code + ': ' + reason) + let reconnect: 'auto' | boolean = 'auto' + if (code) { + this.emit('debug', `${code === 1000 ? 'Clean' : 'Unclean'} WS close: ${code}: ${reason}`, this.id) + if (code === 4001) { + err = new Error('Gateway received invalid OP code') + } else if (code === 4002) { + err = new Error('Gateway received invalid message') + } else if (code === 4003) { + err = new Error('Not authenticated') + this.sessionID = null + } else if (code === 4004) { + err = new Error('Authentication failed') + this.sessionID = null + reconnect = false + this.emit('error', new Error(`Invalid token: ${this.token}`)) + } else if (code === 4005) { + err = new Error('Already authenticated') + } else if (code === 4006 || code === 4009) { + err = new Error('Invalid session') + this.sessionID = null + } else if (code === 4007) { + err = new Error('Invalid sequence number: ' + this.seq) + this.seq = 0 + } else if (code === 4008) { + err = new Error('Gateway connection was ratelimited') + } else if (code === 4010) { + err = new Error('Invalid shard key') + this.sessionID = null + reconnect = false + } else if (code === 4011) { + err = new Error('Shard has too many guilds (>2500)') + this.sessionID = null + reconnect = false + } else if (code === 4013) { + err = new Error('Invalid intents specified') + this.sessionID = null + reconnect = false + } else if (code === 4014) { + err = new Error('Disallowed intents specified') + this.sessionID = null + reconnect = false + } else if (code === 1006) { + err = new Error('Connection reset by peer') + } else if (code !== 1000 && reason) { + err = new Error(code + ': ' + reason) + } + if (err) { + err.code = code + } + } else { + this.emit('debug', 'WS close: unknown code: ' + reason, this.id) + } + this.disconnect( + { + reconnect, + }, + err ?? undefined, + ) + } + + _onWSError(err: Error) { + this.emit('error', err, this.id) + } + + _onWSMessage(data) { + try { + if (data instanceof ArrayBuffer) { + if (this.client.options.compress) { + data = Buffer.from(data) + } + } else if (Array.isArray(data)) { + // Fragmented messages + data = Buffer.concat(data) // Copyfull concat is slow, but no alternative + } + if (this.client.options.compress) { + if (data.length >= 4 && data.readUInt32BE(data.length - 4) === 0xffff) { + return this.onPacket(JSON.parse(data.toString())) + } + } else { + return this.onPacket(JSON.parse(data.toString())) + } + } catch (err) { + this.emit('error', err, this.id) + } + } + + _onWSOpen() { + this.status = 'handshaking' + this.emit('connect', this.id) + this.lastHeartbeatAck = true + } + + toString() { + return Base.prototype.toString.call(this) + } + + toJSON(props: string[] = []) { + return Base.prototype.toJSON.call(this, [ + 'connecting', + 'ready', + 'discordServerTrace', + 'status', + 'lastHeartbeatReceived', + 'lastHeartbeatSent', + 'latency', + 'preReady', + 'getAllUsersCount', + 'getAllUsersQueue', + 'getAllUsersLength', + 'guildSyncQueue', + 'guildSyncQueueLength', + 'unsyncedGuilds', + 'lastHeartbeatAck', + 'seq', + 'sessionID', + 'reconnectInterval', + 'connectAttempts', + ...props, + ]) + } +} + +export default Shard diff --git a/packages/client/src/gateway/ShardManager.ts b/packages/client/src/gateway/ShardManager.ts new file mode 100644 index 000000000..d5612909e --- /dev/null +++ b/packages/client/src/gateway/ShardManager.ts @@ -0,0 +1,157 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import Base from '../Base.js' +import type Client from '../Client.js' +import Collection from '../Collection.js' +import type { ShardManagerOptions } from '../typings.js' +import Shard from './Shard.js' + +export class ShardManager extends Collection { + /** The client manager */ + client: Client + /** The options that were used to configure this manager. */ + options: ShardManagerOptions + /** The buckets that this manager is handling. */ + buckets: Map + /** The queue in which to connect a shard. */ + connectQueue: Shard[] + /** The timeout to use for connecting a shard. */ + connectTimeout: NodeJS.Timeout | null + + constructor(client: Client, options: ShardManagerOptions = {}) { + super() + this.client = client + + this.options = Object.assign( + { + concurrency: 1, + }, + options, + ) + + this.buckets = new Map() + this.connectQueue = [] + this.connectTimeout = null + } + + /** + * @deprecated Use `.client` instead. + */ + get _client(): Client { + return this.client + } + + connect(shard: Shard) { + this.connectQueue.push(shard) + this.tryConnect() + } + + get concurrency(): number { + return this.options.concurrency as number + } + + setConcurrency(concurrency: number) { + this.options.concurrency = concurrency + } + + spawn(id: number) { + let shard = this.get(id) + + if (!shard) { + shard = new Shard(id, this.client) + this.set(id, shard) + + shard + .on('ready', () => { + this.client.emit('shardReady', shard!.id) + if (this.client.ready) return + + for (const other of this.values()) if (!other.ready) return + + this.client.ready = true + this.client.startTime = Date.now() + + this.client.emit('ready') + }) + .on('resume', () => { + this.client.emit('shardResume', shard!.id) + if (this.client.ready) return + + for (const other of this.values()) if (!other.ready) return + + this.client.ready = true + this.client.startTime = Date.now() + this.client.emit('ready') + }) + .on('disconnect', (error) => { + this.client.emit('shardDisconnect', error, shard!.id) + for (const other of this.values()) if (other.ready) return + + this.client.ready = false + this.client.startTime = 0 + this.client.emit('disconnect') + }) + } + + if (shard.status === 'disconnected') { + return this.connect(shard) + } + } + + tryConnect() { + // nothing in queue + if (this.connectQueue.length === 0) { + return + } + + // loop over the connectQueue + for (const shard of this.connectQueue) { + // find the bucket for our shard + const rateLimitKey = shard.id % this.concurrency || 0 + const lastConnect = this.buckets.get(rateLimitKey) ?? 0 + + // has enough time passed since the last connect for this bucket (5s/bucket)? + // alternatively if we have a sessionID, we can skip this check + if (!shard.sessionID && Date.now() - lastConnect < 5000) { + continue + } + + // Are there any connecting shards in the same bucket we should wait on? + if (this.some((s) => s.connecting && (s.id % this.concurrency || 0) === rateLimitKey)) { + continue + } + + // connect the shard + shard.connect() + this.buckets.set(rateLimitKey, Date.now()) + + // remove the shard from the queue + const index = this.connectQueue.findIndex((s) => s.id === shard.id) + this.connectQueue.splice(index, 1) + } + + // set the next timeout if we have more shards to connect + if (!this.connectTimeout && this.connectQueue.length > 0) { + this.connectTimeout = setTimeout(() => { + this.connectTimeout = null + this.tryConnect() + }, 500) + } + } + + _readyPacketCB(shardID: number) { + const rateLimitKey = shardID % this.concurrency || 0 + this.buckets.set(rateLimitKey, Date.now()) + + this.tryConnect() + } + + toString() { + return `[ShardManager ${this.size}]` + } + + toJSON(props = []) { + return Base.prototype.toJSON.call(this, ['buckets', 'connectQueue', 'connectTimeout', 'options', ...props]) + } +} + +export default ShardManager diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts new file mode 100644 index 000000000..502966dfd --- /dev/null +++ b/packages/client/src/index.ts @@ -0,0 +1,48 @@ +import Client from './Client.js' +export default Client + +export * from './Base.js' +export * from './Client.js' +export * from './Collection.js' +export * from './Endpoints.js' +export * from './gateway/Shard.js' +export * from './gateway/ShardManager.js' +export * from './Structures/channels/Category.js' +export * from './Structures/channels/Channel.js' +export * from './Structures/channels/Guild.js' +export * from './Structures/channels/News.js' +export * from './Structures/channels/Private.js' +export * from './Structures/channels/Stage.js' +export * from './Structures/channels/Text.js' +export * from './Structures/channels/TextVoice.js' +export * from './Structures/channels/threads/Member.js' +export * from './Structures/channels/threads/NewsThread.js' +export * from './Structures/channels/threads/PrivateThread.js' +export * from './Structures/channels/threads/PublicThread.js' +export * from './Structures/channels/threads/Thread.js' +export * from './Structures/channels/Voice.js' +export * from './Structures/guilds/AuditLogEntry.js' +export * from './Structures/guilds/Guild.js' +export * from './Structures/guilds/Integration.js' +export * from './Structures/guilds/Member.js' +export * from './Structures/guilds/Preview.js' +export * from './Structures/guilds/Role.js' +export * from './Structures/guilds/StageInstance.js' +export * from './Structures/guilds/Template.js' +export * from './Structures/guilds/Unavailable.js' +export * from './Structures/guilds/VoiceState.js' +export * from './Structures/interactions/Autocomplete.js' +export * from './Structures/interactions/Command.js' +export * from './Structures/interactions/Component.js' +export * from './Structures/interactions/Interaction.js' +export * from './Structures/interactions/Ping.js' +export * from './Structures/interactions/Unknown.js' +export * from './Structures/Invite.js' +export * from './Structures/Message.js' +export * from './Structures/Permission.js' +export * from './Structures/PermissionOverwrite.js' +export * from './Structures/users/Extended.js' +export * from './Structures/users/User.js' +export * from './typings.js' +export * from './utils/BrowserWebSocket.js' +export * from './utils/Bucket.js' diff --git a/packages/client/src/typings.ts b/packages/client/src/typings.ts new file mode 100644 index 000000000..cdd39434b --- /dev/null +++ b/packages/client/src/typings.ts @@ -0,0 +1,3851 @@ +// // TYPES + +import type { + ActivityTypes, + ApplicationCommandOptionTypes, + ApplicationCommandPermissionTypes, + ApplicationCommandTypes, + BigString, + ButtonStyles, + ChannelTypes, + DefaultMessageNotificationLevels, + ExplicitContentFilterLevels, + GuildFeatures, + InteractionResponseTypes, + MessageComponentTypes, + OverwriteTypes, + ScheduledEventPrivacyLevel, + StickerFormatTypes, + StickerTypes, + VerificationLevels, + VideoQualityModes, + WebhookTypes, +} from '@discordeno/types' +import type Collection from './Collection.js' +import type CategoryChannel from './Structures/channels/Category.js' +import type NewsChannel from './Structures/channels/News.js' +import type PrivateChannel from './Structures/channels/Private.js' +import type StageChannel from './Structures/channels/Stage.js' +import type TextChannel from './Structures/channels/Text.js' +import type TextVoiceChannel from './Structures/channels/TextVoice.js' +import type ThreadMember from './Structures/channels/threads/Member.js' +import type NewsThreadChannel from './Structures/channels/threads/NewsThread.js' +import type PrivateThreadChannel from './Structures/channels/threads/PrivateThread.js' +import type PublicThreadChannel from './Structures/channels/threads/PublicThread.js' +import type ThreadChannel from './Structures/channels/threads/Thread.js' +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 Message from './Structures/Message.js' +import type Permission from './Structures/Permission.js' +import type User from './Structures/users/User.js' + +// // Application Commands +// export type AnyApplicationCommand = ChatInputApplicationCommand | MessageApplicationCommand | UserApplicationCommand; +export type ApplicationCommandStructure = ChatInputApplicationCommandStructure | MessageApplicationCommandStructure | UserApplicationCommandStructure +export type ChatInputApplicationCommand = ApplicationCommand +export type ChatInputApplicationCommandStructure = Omit +export type MessageApplicationCommand = Omit, 'description' | 'options'> +export type MessageApplicationCommandStructure = Omit +export type UserApplicationCommand = Omit, 'description' | 'options'> +export type UserApplicationCommandStructure = Omit +export type ApplicationCommandOptions = + | ApplicationCommandOptionsSubCommand + | ApplicationCommandOptionsSubCommandGroup + | ApplicationCommandOptionsWithValue +export type ApplicationCommandOptionsBoolean = ApplicationCommandOption +export type ApplicationCommandOptionsChannel = ApplicationCommandOption +export type ApplicationCommandOptionsInteger = + | ApplicationCommandOptionsIntegerWithAutocomplete + | ApplicationCommandOptionsIntegerWithoutAutocomplete + | ApplicationCommandOptionsIntegerWithMinMax +export type ApplicationCommandOptionsIntegerWithAutocomplete = Omit< + ApplicationCommandOptionWithChoices, + 'choices' | 'min_value' | 'max_value' +> & + AutocompleteEnabled +export type ApplicationCommandOptionsIntegerWithoutAutocomplete = Omit< + ApplicationCommandOptionWithChoices, + 'autocomplete' | 'min_value' | 'max_value' +> & + AutocompleteDisabledInteger +export type ApplicationCommandOptionsIntegerWithMinMax = Omit< + ApplicationCommandOptionWithChoices, + 'choices' | 'autocomplete' +> & + AutocompleteDisabledIntegerMinMax +export type ApplicationCommandOptionsMentionable = ApplicationCommandOption +export type ApplicationCommandOptionsNumber = + | ApplicationCommandOptionsNumberWithAutocomplete + | ApplicationCommandOptionsNumberWithoutAutocomplete + | ApplicationCommandOptionsNumberWithMinMax +export type ApplicationCommandOptionsNumberWithAutocomplete = Omit< + ApplicationCommandOptionWithChoices, + 'choices' | 'min_value' | 'max_value' +> & + AutocompleteEnabled +export type ApplicationCommandOptionsNumberWithoutAutocomplete = Omit< + ApplicationCommandOptionWithChoices, + 'autocomplete' | 'min_value' | 'max_value' +> & + AutocompleteDisabledInteger +export type ApplicationCommandOptionsNumberWithMinMax = Omit< + ApplicationCommandOptionWithChoices, + 'choices' | 'autocomplete' +> & + AutocompleteDisabledIntegerMinMax +export type ApplicationCommandOptionsRole = ApplicationCommandOption +export type ApplicationCommandOptionsString = ApplicationCommandOptionsStringWithAutocomplete | ApplicationCommandOptionsStringWithoutAutocomplete +export type ApplicationCommandOptionsStringWithAutocomplete = Omit< + ApplicationCommandOptionWithChoices, + 'choices' +> & + AutocompleteEnabled +export type ApplicationCommandOptionsStringWithoutAutocomplete = Omit< + ApplicationCommandOptionWithChoices, + 'autocomplete' +> & + AutocompleteDisabled +export type ApplicationCommandOptionsUser = ApplicationCommandOption +export type ApplicationCommandOptionsWithValue = + | ApplicationCommandOptionsString + | ApplicationCommandOptionsInteger + | ApplicationCommandOptionsBoolean + | ApplicationCommandOptionsUser + | ApplicationCommandOptionsChannel + | ApplicationCommandOptionsRole + | ApplicationCommandOptionsMentionable + | ApplicationCommandOptionsNumber +// export type ApplicationCommandPermissionTypes = +// Constants["ApplicationCommandPermissionTypes"][keyof Constants["ApplicationCommandPermissionTypes"]]; +// export type ApplicationCommandTypes = Constants["ApplicationCommandTypes"][keyof Constants["ApplicationCommandTypes"]]; + +// // Cache +export interface Uncached { + id: string +} + +// // Channel +export type AnyChannel = AnyGuildChannel | PrivateChannel +export type AnyGuildChannel = GuildTextableChannel | AnyVoiceChannel | CategoryChannel +// TODO: Add THREADCHANNEL export type AnyThreadChannel = NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel | ThreadChannel; +export type AnyThreadChannel = NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel +export type AnyVoiceChannel = TextVoiceChannel | StageChannel +export type GuildTextableChannel = TextChannel | TextVoiceChannel | NewsChannel +// export type GuildTextableWithThread = GuildTextableChannel | AnyThreadChannel; +// export type InviteChannel = InvitePartialChannel | Exclude; +export type PossiblyUncachedTextable = Textable | Uncached +// export type PossiblyUncachedTextableChannel = TextableChannel | Uncached; +export type TextableChannel = (GuildTextable & GuildTextableChannel) | (ThreadTextable & AnyThreadChannel) | (Textable & PrivateChannel) +export type VideoQualityMode = VideoQualityModes.Auto | VideoQualityModes.Full +// export type ChannelTypes = GuildChannelTypes | PrivateChannelTypes; +// export type GuildChannelTypes = Exclude; +// export type TextChannelTypes = GuildTextChannelTypes | PrivateChannelTypes; +// export type GuildTextChannelTypes = +// Constants["ChannelTypes"][keyof Pick]; +// export type GuildThreadChannelTypes = Constants["ChannelTypes"][ +// keyof Pick +// ]; +// export type GuildPublicThreadChannelTypes = Exclude< +// GuildThreadChannelTypes, +// Constants["ChannelTypes"]["GUILD_PRIVATE_THREAD"] +// >; +// export type PrivateChannelTypes = Constants["ChannelTypes"][keyof Pick]; +export type TextVoiceChannelTypes = ChannelTypes.GuildVoice | ChannelTypes.GuildStageVoice + +// // Command +// export type CommandGenerator = CommandGeneratorFunction | MessageContent | MessageContent[] | CommandGeneratorFunction[]; +// export type CommandGeneratorFunction = (msg: Message, args: string[]) => GeneratorFunctionReturn; +// export type GeneratorFunctionReturn = Promise | Promise | MessageContent | void; +// export type GenericCheckFunction = (msg: Message) => T | Promise; +// export type ReactionButtonsFilterFunction = (msg: Message, emoji: Emoji, userID: string) => boolean; +// export type ReactionButtonsGenerator = +// | ReactionButtonsGeneratorFunction +// | MessageContent +// | MessageContent[] +// | ReactionButtonsGeneratorFunction[]; +// export type ReactionButtonsGeneratorFunction = (msg: Message, args: string[], userID: string) => GeneratorFunctionReturn; + +// // Gateway/REST +// export type IntentStrings = keyof Constants["Intents"]; +// export type ReconnectDelayFunction = (lastDelay: number, attempts: number) => number; +// export type RequestMethod = "GET" | "PATCH" | "DELETE" | "POST" | "PUT"; + +// // Guild +// export type DefaultNotifications = +// Constants["DefaultMessageNotificationLevels"][keyof Constants["DefaultMessageNotificationLevels"]]; +// export type ExplicitContentFilter = Constants["ExplicitContentFilterLevels"][keyof Constants["ExplicitContentFilterLevels"]]; +// export type GuildFeatures = Constants["GuildFeatures"][number]; +// export type NSFWLevel = Constants["GuildNSFWLevels"][keyof Constants["GuildNSFWLevels"]]; +// export type PossiblyUncachedGuild = Guild | Uncached; +// export type PremiumTier = Constants["PremiumTiers"][keyof Constants["PremiumTiers"]]; +// export type VerificationLevel = Constants["VerificationLevels"][keyof Constants["VerificationLevels"]]; +// export type SystemChannelFlags = Constants["SystemChannelFlags"][keyof Constants["SystemChannelFlags"]]; +// export type GuildIntegrationTypes = Constants["GuildIntegrationTypes"][number]; +// export type GuildIntegrationExpireBehavior = +// Constants["GuildIntegrationExpireBehavior"][keyof Constants["GuildIntegrationExpireBehavior"]]; + +// // Interaction +// export type AnyInteraction = PingInteraction | CommandInteraction | ComponentInteraction | AutocompleteInteraction; +// export type InteractionCallbackData = InteractionAutocomplete | InteractionContent; +export type InteractionContent = Pick +export type InteractionContentEdit = Pick +// export type InteractionDataOptions = +// | InteractionDataOptionsSubCommand +// | InteractionDataOptionsSubCommandGroup +// | InteractionDataOptionsWithValue; +// export type InteractionDataOptionsBoolean = InteractionDataOptionWithValue< +// Constants["ApplicationCommandOptionTypes"]["BOOLEAN"], +// boolean +// >; +// export type InteractionDataOptionsChannel = InteractionDataOptionWithValue< +// Constants["ApplicationCommandOptionTypes"]["CHANNEL"], +// string +// >; +// export type InteractionDataOptionsInteger = InteractionDataOptionWithValue< +// Constants["ApplicationCommandOptionTypes"]["INTEGER"], +// number +// >; +// export type InteractionDataOptionsMentionable = InteractionDataOptionWithValue< +// Constants["ApplicationCommandOptionTypes"]["MENTIONABLE"], +// string +// >; +// export type InteractionDataOptionsNumber = InteractionDataOptionWithValue< +// ApplicationCommandOptionTypes.Number, +// number +// >; +// export type InteractionDataOptionsRole = InteractionDataOptionWithValue< +// Constants["ApplicationCommandOptionTypes"]["ROLE"], +// string +// >; +// export type InteractionDataOptionsString = InteractionDataOptionWithValue< +// ApplicationCommandOptionTypes.String, +// string +// >; +// export type InteractionDataOptionsUser = InteractionDataOptionWithValue< +// Constants["ApplicationCommandOptionTypes"]["USER"], +// string +// >; +// export type InteractionDataOptionsWithValue = +// | InteractionDataOptionsString +// | InteractionDataOptionsInteger +// | InteractionDataOptionsBoolean +// | InteractionDataOptionsUser +// | InteractionDataOptionsChannel +// | InteractionDataOptionsRole +// | InteractionDataOptionsMentionable +// | InteractionDataOptionsNumber; +// export type InteractionResponseTypes = Constants["InteractionResponseTypes"][keyof Constants["InteractionResponseTypes"]]; +// export type InteractionTypes = Constants["InteractionTypes"][keyof Constants["InteractionTypes"]]; + +// // Invite +// export type InviteTargetTypes = Constants["InviteTargetTypes"][keyof Constants["InviteTargetTypes"]]; + +// // Message +export type ActionRowComponents = Button | SelectMenu +export type Button = InteractionButton | URLButton +// export type ButtonStyles = Constants["ButtonStyles"][keyof Constants["ButtonStyles"]]; +// export type Component = ActionRow | ActionRowComponents; +// export type ImageFormat = Constants["ImageFormats"][number]; +// export type MessageActivityFlags = Constants["MessageActivityFlags"][keyof Constants["MessageActivityFlags"]]; +export type MessageContent = string | AdvancedMessageContent +export type MessageContentEdit = string | AdvancedMessageContentEdit +// export type MFALevel = Constants["MFALevels"][keyof Constants["MFALevels"]]; +// export type PossiblyUncachedMessage = Message | { +// channel: TextableChannel | { id: string; guild?: Uncached }; +// guildID?: string; +// id: string; +// }; + +// // Permission +// export type PermissionType = Constants["PermissionOverwriteTypes"][keyof Constants["PermissionOverwriteTypes"]]; + +// // Presence/Relationship +export type ActivityType = ActivityTypes +export type BotActivityType = Exclude + +// export type FriendSuggestionReasons = { name: string; platform_type: string; type: number }[]; +export type Status = 'online' | 'idle' | 'dnd' +export type SelfStatus = Status | 'invisible' +// export type UserStatus = Status | "offline"; + +// // Selfbot +// export type ConnectionVisibilityTypes = Constants["ConnectionVisibilityTypes"][keyof Constants["ConnectionVisibilityTypes"]]; + +// // Sticker +// export type StickerTypes = Constants["StickerTypes"][keyof Constants["StickerTypes"]]; +// export type StickerFormats = Constants["StickerFormats"][keyof Constants["StickerFormats"]]; + +// // Thread +export type AutoArchiveDuration = 60 | 1440 | 4320 | 10080 + +// // User +// export type PremiumTypes = Constants["PremiumTypes"][keyof Constants["PremiumTypes"]]; + +// // Voice +// export type ConverterCommand = "./ffmpeg" | "./avconv" | "ffmpeg" | "avconv"; +// export type StageInstancePrivacyLevel = Constants["StageInstancePrivacyLevel"][keyof Constants["StageInstancePrivacyLevel"]]; + +// // Webhook +export type MessageWebhookContent = Pick +// export type WebhookTypes = Constants["WebhookTypes"][keyof Constants["WebhookTypes"]]; + +// // INTERFACES +// // Internals +export interface JSONCache { + [s: string]: unknown +} +// export interface NestedJSON { +// toJSON(arg?: unknown, cache?: (string | unknown)[]): JSONCache; +// } +export interface SimpleJSON { + toJSON: (props?: string[]) => JSONCache +} + +// // Application Commands +export interface ApplicationCommand { + application_id: string + defaultPermission?: boolean + description: T extends ApplicationCommandTypes.ChatInput ? string : never + guild_id?: string + id: string + name: string + options?: ApplicationCommandOptions[] + type: T +} +export interface ApplicationCommandOptionsSubCommand { + description: string + name: string + options?: ApplicationCommandOptionsWithValue[] + type: ApplicationCommandOptionTypes.SubCommand +} +export interface ApplicationCommandOptionsSubCommandGroup { + description: string + name: string + options?: Array + type: ApplicationCommandOptionTypes.SubCommandGroup +} +// export interface ApplicationCommandOptionChoice< +// T extends +// | Constants["ApplicationCommandOptionTypes"][ +// keyof Pick +// ] +// | unknown = unknown, +// > { +// name: string; +// value: T extends ApplicationCommandOptionTypes.String ? string +// : T extends ApplicationCommandOptionTypes.Number ? number +// : T extends Constants["ApplicationCommandOptionTypes"]["INTEGER"] ? number +// : number | string; +// } +export interface ApplicationCommandOptionWithChoices< + T extends ApplicationCommandOptionTypes.String | ApplicationCommandOptionTypes.Integer | ApplicationCommandOptionTypes.Number = + | ApplicationCommandOptionTypes.String + | ApplicationCommandOptionTypes.Integer + | ApplicationCommandOptionTypes.Number, +> { + autocomplete?: boolean + choices?: ApplicationCommandOptionChoice[] + description: string + name: string + required?: boolean + type: T +} +// export interface ApplicationCommandOptionWithMinMax< +// T extends Constants["ApplicationCommandOptionTypes"][ +// keyof Pick +// ] = Constants["ApplicationCommandOptionTypes"][ +// keyof Pick +// ], +// > { +// autocomplete?: boolean; +// choices?: ApplicationCommandOptionChoice[]; +// description: string; +// max_value?: number; +// min_value?: number; +// name: string; +// required?: boolean; +// type: T; +// } +export interface ApplicationCommandOption< + T extends Exclude, +> { + channel_types: T extends ApplicationCommandOptionTypes.Channel ? ChannelTypes | undefined : never + description: string + name: string + required?: boolean + type: T +} +export interface ApplicationCommandPermissions { + id: BigString + permission: boolean + type: ApplicationCommandPermissionTypes +} +export interface AutocompleteEnabled { + autocomplete: true +} +export interface AutocompleteDisabled { + autocomplete?: false +} +export interface AutocompleteDisabledInteger extends AutocompleteDisabled { + min_value?: null + max_value?: null +} +export interface AutocompleteDisabledIntegerMinMax extends AutocompleteDisabled { + choices?: null +} +export interface GuildApplicationCommandPermissions { + application_id: string + guild_id: string + id: string + permissions?: ApplicationCommandPermissions[] +} + +// // Channel +export interface ChannelFollow { + channel_id: string + webhook_id: string +} +export interface ChannelPosition { + id: string + position: number + lockPermissions?: boolean + parentID?: string +} +export interface CreateChannelOptions { + bitrate?: number + nsfw?: boolean + parentID?: string + permissionOverwrites?: Overwrite[] + position?: number + rateLimitPerUser?: number + reason?: string + topic?: string + userLimit?: number +} +export interface EditChannelOptions extends Omit { + archived?: boolean + autoArchiveDuration?: AutoArchiveDuration + defaultAutoArchiveDuration?: AutoArchiveDuration + icon?: string + invitable?: boolean + locked?: boolean + name?: string + ownerID?: string + rtcRegion?: string | null + videoQualityMode?: VideoQualityModes +} +export interface EditChannelPositionOptions { + lockPermissions?: string + parentID?: string +} +export interface GetMessagesOptions { + after?: string + around?: string + before?: string + limit?: number +} +// export interface GuildPinnable extends Pinnable { +// lastPinTimestamp: number | null; +// topic?: string | null; +// } +export interface GuildTextable extends Textable { + rateLimitPerUser: number + createWebhook: (options: { name: string; avatar?: string | null }, reason?: string) => Promise + deleteMessages: (messageIDs: string[], reason?: string) => Promise + getWebhooks: () => Promise + purge: (options: PurgeChannelOptions) => Promise + removeMessageReactionEmoji: (messageID: string, reaction: string) => Promise + removeMessageReactions: (messageID: string) => Promise +} +export interface PartialChannel { + bitrate?: number + id: string + name?: string + nsfw?: boolean + parent_id?: number + permission_overwrites?: Overwrite[] + rate_limit_per_user?: number + topic?: string + type: number + user_limit?: number +} +export interface Pinnable { + getPins: () => Promise + pinMessage: (messageID: string) => Promise + unpinMessage: (messageID: string) => Promise +} +export interface PurgeChannelOptions { + after?: string + before?: string + filter?: (m: Message) => boolean + limit: number + reason?: string +} +export interface Textable { + lastMessageID: string + messages: Collection + addMessageReaction: (messageID: string, reaction: string) => Promise + createMessage: (content: MessageContent, file?: FileContent | FileContent[]) => Promise + deleteMessage: (messageID: string, reason?: string) => Promise + editMessage: (messageID: string, content: MessageContentEdit) => Promise + getMessage: (messageID: string) => Promise + getMessageReaction: (messageID: string, reaction: string, options?: GetMessageReactionOptions) => Promise + getMessages: (options?: GetMessagesOptions) => Promise + removeMessageReaction: (messageID: string, reaction: string, userID?: string) => Promise + sendTyping: () => Promise + unsendMessage: (messageID: string) => Promise +} +// // @ts-ignore ts(2430) - ThreadTextable can't properly extend Textable because of getMessageReaction deprecated overload +export interface ThreadTextable extends Textable, Pinnable { + lastPinTimestamp?: number + deleteMessages: (messageIDs: string[], reason?: string) => Promise + getMembers: () => Promise + join: (userID: string) => Promise + leave: (userID: string) => Promise + purge: (options: PurgeChannelOptions) => Promise + removeMessageReactionEmoji: (messageID: string, reaction: string) => Promise + removeMessageReactions: (messageID: string) => Promise +} +// export interface WebhookData { +// channelID: string; +// guildID: string; +// } + +// // Client +// export interface ClientOptions { +// allowedMentions?: AllowedMentions; +// autoreconnect?: boolean; +// compress?: boolean; +// connectionTimeout?: number; +// defaultImageFormat?: string; +// defaultImageSize?: number; +// disableEvents?: { [s: string]: boolean }; +// firstShardID?: number; +// getAllUsers?: boolean; +// guildCreateTimeout?: number; +// intents: number | IntentStrings[]; +// largeThreshold?: number; +// lastShardID?: number; +// maxReconnectAttempts?: number; +// maxResumeAttempts?: number; +// maxShards?: number | "auto"; +// messageLimit?: number; +// opusOnly?: boolean; +// reconnectDelay?: ReconnectDelayFunction; +// requestTimeout?: number; +// rest?: RequestHandlerOptions; +// restMode?: boolean; +// seedVoiceConnections?: boolean; +// shardConcurrency?: number | "auto"; +// ws?: unknown; +// } +// export interface CommandClientOptions { +// argsSplitter?: (str: string) => string[]; +// defaultCommandOptions?: CommandOptions; +// defaultHelpCommand?: boolean; +// description?: string; +// ignoreBots?: boolean; +// ignoreSelf?: boolean; +// name?: string; +// owner?: string; +// prefix?: string | string[]; +// } +export interface RequestHandlerOptions { + // agent?: HTTPSAgent; + baseURL?: string + decodeReasons?: boolean + disableLatencyCompensation?: boolean + domain?: string + latencyThreshold?: number + ratelimiterOffset?: number + requestTimeout?: number +} + +// // Command +// export interface CommandCooldownExclusions { +// channelIDs?: string[]; +// guildIDs?: string[]; +// userIDs?: string[]; +// } +// export interface CommandOptions { +// aliases?: string[]; +// argsRequired?: boolean; +// caseInsensitive?: boolean; +// cooldown?: number; +// cooldownExclusions?: CommandCooldownExclusions; +// cooldownMessage?: MessageContent | GenericCheckFunction | false; +// cooldownReturns?: number; +// defaultSubcommandOptions?: CommandOptions; +// deleteCommand?: boolean; +// description?: string; +// dmOnly?: boolean; +// errorMessage?: MessageContent | GenericCheckFunction; +// fullDescription?: string; +// guildOnly?: boolean; +// hidden?: boolean; +// hooks?: Hooks; +// invalidUsageMessage?: MessageContent | GenericCheckFunction | false; +// permissionMessage?: MessageContent | GenericCheckFunction | false; +// reactionButtons?: CommandReactionButtonsOptions[] | null; +// reactionButtonTimeout?: number; +// requirements?: CommandRequirements; +// restartCooldown?: boolean; +// usage?: string; +// } +// export interface CommandReactionButtons extends CommandReactionButtonsOptions { +// execute: (msg: Message, args: string[], userID: string) => string | GeneratorFunctionReturn; +// responses: ((() => string) | ReactionButtonsGeneratorFunction)[]; +// } +// export interface CommandReactionButtonsOptions { +// emoji: string; +// filter: ReactionButtonsFilterFunction; +// response: string | ReactionButtonsGeneratorFunction; +// type: "edit" | "cancel"; +// } +// export interface CommandRequirements { +// custom?: GenericCheckFunction; +// permissions?: { [s: string]: boolean } | GenericCheckFunction<{ [s: string]: boolean }>; +// roleIDs?: string[] | GenericCheckFunction; +// roleNames?: string[] | GenericCheckFunction; +// userIDs?: string[] | GenericCheckFunction; +// } +// export interface Hooks { +// postCheck?: (msg: Message, args: string[], checksPassed: boolean) => void; +// postCommand?: (msg: Message, args: string[], sent?: Message) => void; +// postExecution?: (msg: Message, args: string[], executionSuccess: boolean) => void; +// preCommand?: (msg: Message, args: string[]) => void; +// } + +// // Embed +// // Omit used to override +// export interface Embed extends Omit { +// author?: EmbedAuthor; +// footer?: EmbedFooter; +// image?: EmbedImage; +// provider?: EmbedProvider; +// thumbnail?: EmbedImage; +// type: string; +// video?: EmbedVideo; +// } +// export interface EmbedAuthor extends EmbedAuthorOptions { +// proxy_icon_url?: string; +// } +export interface EmbedAuthorOptions { + icon_url?: string + name: string + url?: string +} +export interface EmbedField { + inline?: boolean + name: string + value: string +} +// export interface EmbedFooter extends EmbedFooterOptions { +// proxy_icon_url?: string; +// } +export interface EmbedFooterOptions { + icon_url?: string + text: string +} +// export interface EmbedImage extends EmbedImageOptions { +// height?: number; +// proxy_url?: string; +// width?: number; +// } +export interface EmbedImageOptions { + url?: string +} +export interface EmbedOptions { + author?: EmbedAuthorOptions + color?: number + description?: string + fields?: EmbedField[] + footer?: EmbedFooterOptions + image?: EmbedImageOptions + thumbnail?: EmbedImageOptions + timestamp?: Date | string + title?: string + url?: string +} +// export interface EmbedProvider { +// name?: string; +// url?: string; +// } +// export interface EmbedVideo { +// height?: number; +// proxy_url?: string; +// url?: string; +// width?: number; +// } + +// // Emoji +export interface Emoji extends EmojiBase { + animated: boolean + available: boolean + id: string + managed: boolean + require_colons: boolean + roles: string[] + user?: PartialUser +} +export interface EmojiBase { + icon?: string + name: string +} +export interface EmojiOptions extends Exclude { + image: string + roles?: string[] +} +export interface PartialEmoji { + id: string | null + name: string + animated?: boolean +} + +// // Events +// export interface OldCall { +// endedTimestamp?: number; +// participants: string[]; +// region: string; +// ringing: string[]; +// unavailable: boolean; +// } +// export interface OldGroupChannel { +// name: string; +// ownerID: string; +// icon: string; +// type: Constants["ChannelTypes"]["GROUP_DM"]; +// } +// export interface OldGuild { +// afkChannelID: string | null; +// afkTimeout: number; +// banner: string | null; +// defaultNotifications: DefaultNotifications; +// description: string | null; +// discoverySplash: string | null; +// emojis: Omit[]; +// explicitContentFilter: ExplicitContentFilter; +// features: GuildFeatures[]; +// icon: string | null; +// large: boolean; +// maxMembers?: number; +// maxVideoChannelUsers?: number; +// mfaLevel: MFALevel; +// name: string; +// nsfwLevel: NSFWLevel; +// ownerID: string; +// preferredLocale?: string; +// premiumSubscriptionCount?: number; +// premiumTier: PremiumTier; +// publicUpdatesChannelID: string | null; +// rulesChannelID: string | null; +// splash: string | null; +// stickers?: Sticker[]; +// systemChannelFlags: SystemChannelFlags; +// systemChannelID: string | null; +// vanityURL: string | null; +// verificationLevel: VerificationLevel; +// } +// export interface OldGuildChannel { +// bitrate?: number; +// name: string; +// nsfw?: boolean; +// parentID: string | null; +// permissionOverwrites: Collection; +// position: number; +// rateLimitPerUser?: number; +// rtcRegion?: string | null; +// topic?: string | null; +// type: GuildChannelTypes; +// } +// export interface OldGuildTextChannel extends OldGuildChannel { +// nsfw: boolean; +// rateLimitPerUser: number; +// topic: string | null; +// type: GuildTextChannelTypes; +// } +// export interface OldMember { +// avatar: string | null; +// communicationDisabledUntil: number | null; +// nick: string | null; +// pending?: boolean; +// premiumSince?: number | null; +// roles: string[]; +// } +// export interface OldMessage { +// attachments: Attachment[]; +// channelMentions: string[]; +// content: string; +// editedTimestamp?: number; +// embeds: Embed[]; +// flags: number; +// mentionedBy?: unknown; +// mentions: User[]; +// pinned: boolean; +// roleMentions: string[]; +// tts: boolean; +// } +// export interface OldRole { +// color: number; +// hoist: boolean; +// icon: string | null; +// managed: boolean; +// mentionable: boolean; +// name: string; +// permissions: Permission; +// position: number; +// unicodeEmoji: string | null; +// } +// export interface OldStageInstance { +// discoverableDisabled: boolean; +// privacyLevel: StageInstancePrivacyLevel; +// topic: string; +// } +// export interface OldTextVoiceChannel extends OldGuildChannel { +// bitrate: number; +// rtcRegion: string | null; +// type: TextVoiceChannelTypes; +// userLimit: number; +// videoQualityMode: VideoQualityMode; +// } +// export interface OldThread { +// name: string; +// rateLimitPerUser: number; +// threadMetadata: ThreadMetadata; +// } +// export interface OldThreadMember { +// flags: number; +// } +// export interface OldVoiceState { +// deaf: boolean; +// mute: boolean; +// selfDeaf: boolean; +// selfMute: boolean; +// selfStream: boolean; +// selfVideo: boolean; +// } +// export interface EventListeners { +// callCreate: [call: Call]; +// callDelete: [call: Call]; +// callRing: [call: Call]; +// callUpdate: [call: Call, oldCall: OldCall]; +// channelCreate: [channel: AnyGuildChannel]; +// channelDelete: [channel: AnyChannel]; +// channelPinUpdate: [channel: TextableChannel, timestamp: number, oldTimestamp: number]; +// channelRecipientAdd: [channel: GroupChannel, user: User]; +// channelRecipientRemove: [channel: GroupChannel, user: User]; +// channelUpdate: +// | [channel: AnyGuildChannel, oldChannel: OldGuildChannel | OldGuildTextChannel | OldTextVoiceChannel] +// | [channel: GroupChannel, oldChannel: OldGroupChannel]; +// connect: [id: number]; +// debug: [message: string, id?: number]; +// disconnect: []; +// error: [err: Error, id?: number]; +// friendSuggestionCreate: [user: User, reasons: FriendSuggestionReasons]; +// friendSuggestionDelete: [user: User]; +// guildAvailable: [guild: Guild]; +// guildBanAdd: [guild: Guild, user: User]; +// guildBanRemove: [guild: Guild, user: User]; +// guildCreate: [guild: Guild]; +// guildDelete: [guild: PossiblyUncachedGuild]; +// guildEmojisUpdate: [guild: PossiblyUncachedGuild, emojis: Emoji[], oldEmojis: Emoji[] | null]; +// guildMemberAdd: [guild: Guild, member: Member]; +// guildMemberChunk: [guild: Guild, member: Member[]]; +// guildMemberRemove: [guild: Guild, member: Member | MemberPartial]; +// guildMemberUpdate: [guild: Guild, member: Member, oldMember: OldMember | null]; +// guildRoleCreate: [guild: Guild, role: Role]; +// guildRoleDelete: [guild: Guild, role: Role]; +// guildRoleUpdate: [guild: Guild, role: Role, oldRole: OldRole]; +// guildStickersUpdate: [guild: PossiblyUncachedGuild, stickers: Sticker[], oldStickers: Sticker[] | null]; +// guildUnavailable: [guild: UnavailableGuild]; +// guildUpdate: [guild: Guild, oldGuild: OldGuild]; +// 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: PossiblyUncachedMessage]; +// messageDeleteBulk: [messages: PossiblyUncachedMessage[]]; +// messageReactionAdd: [message: PossiblyUncachedMessage, emoji: PartialEmoji, reactor: Member | Uncached]; +// messageReactionRemove: [message: PossiblyUncachedMessage, emoji: PartialEmoji, userID: string]; +// messageReactionRemoveAll: [message: PossiblyUncachedMessage]; +// messageReactionRemoveEmoji: [message: PossiblyUncachedMessage, emoji: PartialEmoji]; +// messageUpdate: [message: Message, oldMessage: OldMessage | null]; +// presenceUpdate: [other: Member | Relationship, oldPresence: Presence | null]; +// rawREST: [request: RawRESTRequest]; +// rawWS: [packet: RawPacket, id: number]; +// ready: []; +// relationshipAdd: [relationship: Relationship]; +// relationshipRemove: [relationship: Relationship]; +// relationshipUpdate: [relationship: Relationship, oldRelationship: { type: number }]; +// shardPreReady: [id: number]; +// stageInstanceCreate: [stageInstance: StageInstance]; +// stageInstanceDelete: [stageInstance: StageInstance]; +// stageInstanceUpdate: [stageInstance: StageInstance, oldStageInstance: OldStageInstance | null]; +// threadCreate: [channel: AnyThreadChannel]; +// threadDelete: [channel: AnyThreadChannel]; +// threadListSync: [ +// guild: Guild, +// deletedThreads: (AnyThreadChannel | Uncached)[], +// activeThreads: AnyThreadChannel[], +// joinedThreadsMember: ThreadMember[], +// ]; +// threadMembersUpdate: [ +// channel: AnyThreadChannel, +// addedMembers: ThreadMember[], +// removedMembers: (ThreadMember | Uncached)[], +// ]; +// threadMemberUpdate: [channel: AnyThreadChannel, member: ThreadMember, oldMember: OldThreadMember]; +// threadUpdate: [channel: AnyThreadChannel, oldChannel: OldThread | null]; +// typingStart: +// | [channel: GuildTextableChannel | Uncached, user: User | Uncached, member: Member] +// | [channel: PrivateChannel | Uncached, user: User | Uncached, member: null]; +// unavailableGuildCreate: [guild: UnavailableGuild]; +// unknown: [packet: RawPacket, 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: OldVoiceState]; +// warn: [message: string, id?: number]; +// webhooksUpdate: [data: WebhookData]; +// } +// export interface ClientEvents extends EventListeners { +// shardDisconnect: [err: Error | undefined, id: number]; +// shardReady: [id: number]; +// shardResume: [id: number]; +// } +// export interface ShardEvents extends EventListeners { +// resume: []; +// } +// export interface StreamEvents { +// end: []; +// error: [err: Error]; +// start: []; +// } +// export interface VoiceEvents { +// connect: []; +// debug: [message: string]; +// disconnect: [err?: Error]; +// end: []; +// error: [err: Error]; +// pong: [latency: number]; +// ready: []; +// speakingStart: [userID: string]; +// speakingStop: [userID: string]; +// start: []; +// unknown: [packet: RawPacket]; +// userDisconnect: [userID: string]; +// warn: [message: string]; +// } + +// // Gateway/REST +// export interface HTTPResponse { +// code: number; +// message: string; +// } +// export interface LatencyRef { +// lastTimeOffsetCheck: number; +// latency: number; +// raw: number[]; +// timeOffset: number; +// timeOffsets: number[]; +// } +// export interface RawPacket { +// d?: unknown; +// op: number; +// s?: number; +// t?: string; +// } +// export interface RawRESTRequest { +// auth: boolean; +// body?: unknown; +// file?: FileContent; +// method: string; +// resp: IncomingMessage; +// route: string; +// short: boolean; +// url: string; +// } +export interface RequestMembersPromise { + members: Member + received: number + res: (value: Member[]) => void + timeout: number +} +export interface ShardManagerOptions { + concurrency?: number | 'auto' +} + +// // Guild +export interface CreateGuildOptions { + afkChannelID?: string + afkTimeout?: number + channels?: PartialChannel[] + defaultNotifications?: DefaultMessageNotificationLevels + explicitContentFilter?: ExplicitContentFilterLevels + icon?: string + roles?: PartialRole[] + systemChannelID: string + verificationLevel?: VerificationLevels +} +export interface DiscoveryCategory { + id: number + is_primary: boolean + name: { + default: string + localizations?: { [lang: string]: string } + } +} +export interface DiscoveryMetadata { + category_ids: number[] + emoji_discoverability_enabled: boolean + guild_id: string + keywords: string[] | null + primary_category_id: number +} +export interface DiscoveryOptions { + emojiDiscoverabilityEnabled?: boolean + keywords?: string[] + primaryCategoryID?: string + reason?: string +} +export interface DiscoverySubcategoryResponse { + category_id: number + guild_id: string +} +export interface GetGuildAuditLogOptions { + actionType?: number + before?: string + limit?: number + userID?: string +} +export interface GetGuildBansOptions { + after?: string + before?: string + limit?: number +} +export interface GetPruneOptions { + days?: number + includeRoles?: string[] +} +export interface GetRESTGuildMembersOptions { + after?: string + limit?: number +} +export interface GetRESTGuildsOptions { + after?: string + before?: string + limit?: number +} +export interface GuildAuditLog { + entries: GuildAuditLogEntry[] + integrations: GuildIntegration[] + threads: AnyThreadChannel[] + users: User[] + webhooks: Webhook[] +} +export interface GuildBan { + reason?: string + user: User +} +export interface GuildOptions { + afkChannelID?: string + afkTimeout?: number + banner?: string + defaultNotifications?: DefaultMessageNotificationLevels + description?: string + discoverySplash?: string + explicitContentFilter?: ExplicitContentFilterLevels + features?: GuildFeatures[] // Though only some are editable? + icon?: string + name?: string + ownerID?: string + preferredLocale?: string + publicUpdatesChannelID?: string + rulesChannelID?: string + splash?: string + systemChannelFlags?: number + systemChannelID?: string + verificationLevel?: VerificationLevels +} +export interface GuildTemplateOptions { + name?: string + description?: string | null +} +export interface GuildVanity { + code: string | null + uses: number +} +// export interface IntegrationApplication { +// bot?: User; +// description: string; +// icon: string | null; +// id: string; +// name: string; +// summary: string; +// } +export interface IntegrationOptions { + enableEmoticons?: string + expireBehavior?: string + expireGracePeriod?: string +} +export interface PruneMemberOptions extends GetPruneOptions { + computePruneCount?: boolean + reason?: string +} +export interface VoiceRegion { + custom: boolean + deprecated: boolean + id: string + name: string + optimal: boolean + vip: boolean +} +export interface WelcomeChannel { + channelID: string + description: string + emojiID: string | null + emojiName: string | null +} +export interface WelcomeScreen { + description: string + welcomeChannels: WelcomeChannel[] +} +export interface WelcomeScreenOptions extends WelcomeScreen { + enabled: boolean +} +export interface Widget { + channel_id?: string + enabled: boolean +} +export interface WidgetChannel { + id: string + name: string + position: number +} +export interface WidgetData { + channels: WidgetChannel[] + id: string + instant_invite: string + members: WidgetMember[] + name: string + presence_count: number +} +export interface WidgetMember { + avatar: string | null + avatar_url: string + discriminator: string + id: string + status: string + username: string +} + +// // Interaction +// export interface InteractionAutocomplete { +// choices: ApplicationCommandOptionChoice[]; +// } +// export interface InteractionDataOptionsSubCommand { +// name: string; +// options?: InteractionDataOptions[]; +// type: Constants["ApplicationCommandOptionTypes"]["SUB_COMMAND"]; +// } +// export interface InteractionDataOptionsSubCommandGroup { +// name: string; +// options: InteractionDataOptions[]; +// type: Constants["ApplicationCommandOptionTypes"]["SUB_COMMAND_GROUP"]; +// } +// export interface InteractionDataOptionWithValue< +// T extends Constants["ApplicationCommandOptionTypes"][ +// Exclude +// ] = Constants["ApplicationCommandOptionTypes"][ +// Exclude +// ], +// V = unknown, +// > { +// focused?: boolean; +// name: string; +// type: T; +// value: V; +// } +/** https://discord.com/developers/docs/interactions/slash-commands#interaction-response */ +export interface InteractionResponse { + type: InteractionResponseTypes + data?: InteractionApplicationCommandCallbackData +} + +export interface InteractionApplicationCommandCallbackData { + content?: string + tts?: boolean + embeds?: EmbedOptions[] + allowedMentions?: AllowedMentions + file?: FileContent | FileContent[] + customId?: string + title?: string + components?: ActionRow[] + flags?: number + choices?: ApplicationCommandOptionChoice[] +} + +export interface ApplicationCommandOptionChoice { + name: string + value: string | number +} + +// // Invite +export interface CreateChannelInviteOptions extends CreateInviteOptions { + targetApplicationID?: string + targetType?: InviteTargetTypes + targetUserID?: string +} +export interface CreateInviteOptions { + maxAge?: number + maxUses?: number + temporary?: boolean + unique?: boolean +} +// export interface Invitable { +// createInvite(options?: CreateInviteOptions, reason?: string): Promise; +// getInvites(): Promise; +// } +// export interface InvitePartialChannel { +// icon?: string | null; +// id: string; +// name: string | null; +// recipients?: { username: string }[]; +// type: Exclude; +// } +// export interface InviteStageInstance { +// members: Member[]; +// participantCount: number; +// speakerCount: number; +// topic: string; +// } + +// // Member/User +export interface FetchMembersOptions { + limit?: number + presences?: boolean + query?: string + timeout?: number + userIDs?: string[] +} +export interface MemberOptions { + channelID?: string | null + communicationDisabledUntil?: Date | null + deaf?: boolean + mute?: boolean + nick?: string | null + roles?: string[] +} +// export interface MemberPartial { +// id: string; +// user: User; +// } +// export interface MemberRoles extends BaseData { +// roles: string[]; +// } +export interface PartialUser { + accentColor?: number | null + avatar: string | null + banner?: string | null + discriminator: string + id: string + username: string +} +export interface RequestGuildMembersOptions extends Omit { + nonce: string + user_ids?: string[] +} +// export interface RequestGuildMembersReturn { +// members: Member[]; +// received: number; +// res: (value?: unknown) => void; +// timeout: number; +// } + +// // Message +export interface ActionRow { + components: ActionRowComponents[] + type: MessageComponentTypes.ActionRow +} +// export interface ActiveMessages { +// args: string[]; +// command: Command; +// timeout: number; +// } +export interface AdvancedMessageContent { + allowedMentions?: AllowedMentions + components?: ActionRow[] + content?: string + embed?: EmbedOptions + embeds?: EmbedOptions[] + flags?: number + messageReference?: MessageReferenceReply + stickerIDs?: string[] + tts?: boolean +} +export interface AdvancedMessageContentEdit extends AdvancedMessageContent { + file?: FileContent | FileContent[] +} +export interface AllowedMentions { + /** Whether or not to allow mentioning @everyone */ + everyone?: boolean + /** Whether or not to allow mentioning the replied user. */ + repliedUser?: boolean + /** The roles to allow mentioning by default or enable all roles to be able to be mentioned by default. */ + roles?: boolean | string[] + /** The users to allow mentioning by default or enable all users to be able to be mentioned by default. */ + users?: boolean | string[] +} +// export interface Attachment { +// content_export type?: string; +// ephemeral?: boolean; +// filename: string; +// height?: number; +// id: string; +// proxy_url: string; +// size: number; +// url: string; +// width?: number; +// } +export interface ButtonBase { + disabled?: boolean + emoji?: Partial + label?: string + type: MessageComponentTypes.Button +} +export interface CreateStickerOptions extends Required> { + file: FileContent +} +export interface EditStickerOptions { + description?: string + name?: string + tags?: string +} +export interface SelectMenu { + custom_id: string + disabled?: boolean + max_values?: number + min_values?: number + options: SelectMenuOptions[] + placeholder?: string + type: MessageComponentTypes.SelectMenu +} +export interface SelectMenuOptions { + default?: boolean + description?: string + emoji?: Partial + label: string + value: string +} +export interface GetMessageReactionOptions { + after?: string + limit?: number +} + +export interface InteractionButton extends ButtonBase { + custom_id: string + style: Exclude +} +// export interface MessageActivity { +// party_id?: string; +// type: MessageActivityFlags; +// } +// export interface MessageApplication { +// cover_image?: string; +// description: string; +// icon: string | null; +// id: string; +// name: string; +// } +export interface FileContent { + /** The file data. */ + file: Buffer | string + /** The name of the file, which must include the file suffix. */ + name: string +} +// export interface MessageInteraction { +// id: string; +// member: Member | null; +// name: string; +// type: InteractionTypes; +// user: User; +// } +// export interface MessageReference extends MessageReferenceBase { +// channelID: string; +// } +export interface MessageReferenceBase { + channelID?: string + guildID?: string + messageID?: string +} +export interface MessageReferenceReply extends MessageReferenceBase { + messageID: string + failIfNotExists?: boolean +} +export interface Sticker extends StickerItems { + available?: boolean + description: string + guild_id?: string + pack_id?: string + sort_value?: number + tags: string + type: StickerTypes + user?: User +} +export interface StickerItems { + id: string + name: string + format_type: StickerFormatTypes +} +export interface StickerPack { + id: string + stickers: Sticker[] + name: string + sku_id: string + cover_sticker_id?: string + description: string + banner_asset_id: string +} +export interface URLButton extends ButtonBase { + style: ButtonStyles.Link + url: string +} + +// // Presence +export interface Activity extends ActivityPartial { + application_id?: string + assets?: { + large_image?: string + large_text?: string + small_image?: string + small_text?: string + [key: string]: unknown + } + created_at: number + details?: string + emoji?: { animated?: boolean; id?: string; name: string } + flags?: number + instance?: boolean + party?: { id?: string; size?: [number, number] } + secrets?: { join?: string; spectate?: string; match?: string } + state?: string + timestamps?: { end?: number; start: number } + type: T + // the stuff attached to this object apparently varies even more than documented, so... + [key: string]: unknown +} +export interface ActivityPartial { + name: string + type?: T + url?: string +} +export interface ClientPresence { + activities: Activity[] | null + afk: boolean + since: number | null + status: SelfStatus +} +// export interface ClientStatus { +// desktop: UserStatus; +// mobile: UserStatus; +// web: UserStatus; +// } +// export interface Presence { +// activities?: Activity[]; +// clientStatus?: ClientStatus; +// status?: UserStatus; +// } + +// // Role +export interface Overwrite { + allow: bigint | number + deny: bigint | number + id: string + type: OverwriteTypes +} +export interface PartialRole { + color?: number + hoist?: boolean + id: string + mentionable?: boolean + name?: string + permissions?: number + position?: number +} +export interface RoleOptions { + color?: number + hoist?: boolean + icon?: string + mentionable?: boolean + name?: string + permissions?: bigint | number | string | Permission + unicodeEmoji?: string +} +// export interface RoleTags { +// bot_id?: string; +// integration_id?: string; +// premium_subscriber?: true; +// } + +// // Thread +export interface CreateThreadOptions { + autoArchiveDuration: AutoArchiveDuration + name: string +} +export interface CreateThreadWithoutMessageOptions { + name: string + autoArchiveDuration: 60 | 1440 | 4320 | 10080 + rateLimitPerUser?: number | null + reason?: string + type: ChannelTypes.AnnouncementThread | ChannelTypes.PublicThread | ChannelTypes.PrivateThread + invitable?: boolean +} +export interface GetArchivedThreadsOptions { + before?: Date + limit?: number +} +export interface ListedChannelThreads extends ListedGuildThreads { + hasMore: boolean +} +export interface ListedGuildThreads { + members: ThreadMember[] + threads: T[] +} +// export interface PrivateThreadMetadata extends ThreadMetadata { +// invitable: boolean; +// } +// export interface ThreadMetadata { +// archiveTimestamp: number; +// archived: boolean; +// autoArchiveDuration: AutoArchiveDuration; +// locked: boolean; +// } + +// // Voice +export interface JoinVoiceChannelOptions { + opusOnly?: boolean + selfDeaf?: boolean + selfMute?: boolean + shared?: boolean +} +export interface StageInstanceOptions { + privacyLevel?: ScheduledEventPrivacyLevel + topic?: string +} +// export interface UncachedMemberVoiceState { +// id: string; +// voiceState: OldVoiceState; +// } +// export interface VoiceConnectData { +// channel_id: string; +// endpoint: string; +// session_id: string; +// token: string; +// user_id: string; +// } +// export interface VoiceResourceOptions { +// encoderArgs?: string[]; +// format?: string; +// frameDuration?: number; +// frameSize?: number; +// inlineVolume?: boolean; +// inputArgs?: string[]; +// pcmSize?: number; +// samplingRate?: number; +// voiceDataTimeout?: number; +// } +// export interface VoiceServerUpdateData extends Omit { +// guild_id: string; +// shard: Shard; +// } +export interface VoiceStateOptions { + channelID: string + requestToSpeakTimestamp?: Date | null + suppress?: boolean +} +// export interface VoiceStreamCurrent { +// buffer: Buffer | null; +// bufferingTicks: number; +// options: VoiceResourceOptions; +// pausedTime?: number; +// pausedTimestamp?: number; +// playTime: number; +// startTime: number; +// timeout: number | null; +// } + +// // Webhook +export interface Webhook { + application_id: string | null + avatar: string | null + channel_id: string | null + guild_id: string | null + id: string + name: string + source_channel?: { id: string; name: string } + source_guild: { icon: string | null; id: string; name: string } + token?: string + type: WebhookTypes + url?: string + user?: PartialUser +} +export interface WebhookOptions { + avatar?: string + channelID?: string + name?: string +} +export interface WebhookPayload { + allowedMentions?: AllowedMentions + auth?: boolean + avatarURL?: string + components?: ActionRow[] + content?: string + embed?: EmbedOptions + embeds?: EmbedOptions[] + file?: FileContent | FileContent[] + flags?: number + threadID?: string + tts?: boolean + username?: string + wait?: boolean +} + +// // TODO: Does this have more stuff? +// export interface BaseData { +// id: string; +// [key: string]: unknown; +// } +export interface OAuthApplicationInfo { + bot_public: boolean + bot_require_code_grant: boolean + description: string + icon?: string + id: string + name: string + owner: { + avatar?: string + discriminator: string + id: string + username: string + } + team: OAuthTeamInfo | null +} +export interface OAuthTeamInfo { + icon: string | null + id: string + members: OAuthTeamMember[] + owner_user_id: string +} +export interface OAuthTeamMember { + membership_state: number + permissions: string[] + team_id: string + user: PartialUser +} +// export interface Constants { +// GATEWAY_VERSION: 9; +// REST_VERSION: 9; +// ActivityTypes: { +// GAME: 0; +// STREAMING: 1; +// LISTENING: 2; +// WATCHING: 3; +// CUSTOM: 4; +// COMPETING: 5; +// }; +// ApplicationCommandOptionTypes: { +// SUB_COMMAND: 1; +// SUB_COMMAND_GROUP: 2; +// STRING: 3; +// INTEGER: 4; +// BOOLEAN: 5; +// USER: 6; +// CHANNEL: 7; +// ROLE: 8; +// MENTIONABLE: 9; +// NUMBER: 10; +// }; +// ApplicationCommandTypes: { +// CHAT_INPUT: 1; +// USER: 2; +// MESSAGE: 3; +// }; +// AuditLogActions: { +// GUILD_UPDATE: 1; + +// CHANNEL_CREATE: 10; +// CHANNEL_UPDATE: 11; +// CHANNEL_DELETE: 12; +// CHANNEL_OVERWRITE_CREATE: 13; +// CHANNEL_OVERWRITE_UPDATE: 14; +// CHANNEL_OVERWRITE_DELETE: 15; + +// MEMBER_KICK: 20; +// MEMBER_PRUNE: 21; +// MEMBER_BAN_ADD: 22; +// MEMBER_BAN_REMOVE: 23; +// MEMBER_UPDATE: 24; +// MEMBER_ROLE_UPDATE: 25; +// MEMBER_MOVE: 26; +// MEMBER_DISCONNECT: 27; +// BOT_ADD: 28; + +// ROLE_CREATE: 30; +// ROLE_UPDATE: 31; +// ROLE_DELETE: 32; + +// INVITE_CREATE: 40; +// INVITE_UPDATE: 41; +// INVITE_DELETE: 42; + +// WEBHOOK_CREATE: 50; +// WEBHOOK_UPDATE: 51; +// WEBHOOK_DELETE: 52; + +// EMOJI_CREATE: 60; +// EMOJI_UPDATE: 61; +// EMOJI_DELETE: 62; + +// MESSAGE_DELETE: 72; +// MESSAGE_BULK_DELETE: 73; +// MESSAGE_PIN: 74; +// MESSAGE_UNPIN: 75; + +// INTEGRATION_CREATE: 80; +// INTEGRATION_UPDATE: 81; +// INTEGRATION_DELETE: 82; +// STAGE_INSTANCE_CREATE: 83; +// STAGE_INSTANCE_UPDATE: 84; +// STAGE_INSTANCE_DELETE: 85; + +// STICKER_CREATE: 90; +// STICKER_UPDATE: 91; +// STICKER_DELETE: 92; + +// GUILD_SCHEDULED_EVENT_CREATE: 100; +// GUILD_SCHEDULED_EVENT_UPDATE: 101; +// GUILD_SCHEDULED_EVENT_DELETE: 102; + +// THREAD_CREATE: 110; +// THREAD_UPDATE: 111; +// THREAD_DELETE: 112; + +// APPLICATION_COMMAND_PERMISSION_UPDATE: 121; +// }; +// ButtonStyles: { +// PRIMARY: 1; +// SECONDARY: 2; +// SUCCESS: 3; +// DANGER: 4; +// LINK: 5; +// }; +// ChannelTypes: { +// GUILD_TEXT: 0; +// DM: 1; +// GUILD_VOICE: 2; +// GROUP_DM: 3; +// GUILD_CATEGORY: 4; +// GUILD_NEWS: 5; +// GUILD_STORE: 6; + +// GUILD_NEWS_THREAD: 10; +// GUILD_PUBLIC_THREAD: 11; +// GUILD_PRIVATE_THREAD: 12; +// GUILD_STAGE_VOICE: 13; +// }; +// ComponentTypes: { +// ACTION_ROW: 1; +// BUTTON: 2; +// SELECT_MENU: 3; +// }; +// ConnectionVisibilityTypes: { +// NONE: 0; +// EVERYONE: 1; +// }; +// DefaultMessageNotificationLevels: { +// ALL_MESSAGES: 0; +// ONLY_MENTIONS: 1; +// }; +// ExplicitContentFilterLevels: { +// DISABLED: 0; +// MEMBERS_WITHOUT_ROLES: 1; +// ALL_MEMBERS: 2; +// }; +// GatewayOPCodes: { +// DISPATCH: 0; +// HEARTBEAT: 1; +// IDENTIFY: 2; +// PRESENCE_UPDATE: 3; +// VOICE_STATE_UPDATE: 4; +// VOICE_SERVER_PING: 5; +// RESUME: 6; +// RECONNECT: 7; +// REQUEST_GUILD_MEMBERS: 8; +// INVALID_SESSION: 9; +// HELLO: 10; +// HEARTBEAT_ACK: 11; +// SYNC_GUILD: 12; +// SYNC_CALL: 13; +// }; +// GuildFeatures: [ +// "ANIMATED_ICON", +// "BANNER", +// "COMMERCE", +// "COMMUNITY", +// "DISCOVERABLE", +// "FEATURABLE", +// "INVITE_SPLASH", +// "MEMBER_VERIFICATION_GATE_ENABLED", +// "MONETIZATION_ENABLED", +// "MORE_STICKERS", +// "NEWS", +// "PARTNERED", +// "PREVIEW_ENABLED", +// "PRIVATE_THREADS", +// "ROLE_ICONS", +// "ROLE_SUBSCRIPTIONS_ENABLED", +// "SEVEN_DAY_THREAD_ARCHIVE", +// "THREE_DAY_THREAD_ARCHIVE", +// "TICKETED_EVENTS_ENABLED", +// "VANITY_URL", +// "VERIFIED", +// "VIP_REGIONS", +// "WELCOME_SCREEN_ENABLED", +// ]; +// GuildIntegrationExpireBehavior: { +// REMOVE_ROLE: 0; +// KICK: 1; +// }; +// GuildIntegrationTypes: [ +// "twitch", +// "youtube", +// "discord", +// ]; +// GuildNSFWLevels: { +// DEFAULT: 0; +// EXPLICIT: 1; +// SAFE: 2; +// AGE_RESTRICTED: 3; +// }; +// ImageFormats: [ +// "jpg", +// "jpeg", +// "png", +// "webp", +// "gif", +// ]; +// ImageSizeBoundaries: { +// MAXIMUM: 4096; +// MINIMUM: 16; +// }; +// Intents: { +// guilds: 1; +// guildMembers: 2; +// guildBans: 4; +// guildEmojisAndStickers: 8; +// guildIntegrations: 16; +// guildWebhooks: 32; +// guildInvites: 64; +// guildVoiceStates: 128; +// guildPresences: 256; +// guildMessages: 512; +// guildMessageReactions: 1024; +// guildMessageTyping: 2048; +// directMessages: 4096; +// directMessageReactions: 8192; +// directMessageTyping: 16384; +// allNonPrivileged: 32509; +// allPrivileged: 258; +// all: 32767; +// }; +// InteractionResponseTypes: { +// PONG: 1; +// CHANNEL_MESSAGE_WITH_SOURCE: 4; +// DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5; +// DEFERRED_UPDATE_MESSAGE: 6; +// UPDATE_MESSAGE: 7; +// APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: 8; +// }; +// InteractionTypes: { +// PING: 1; +// APPLICATION_COMMAND: 2; +// MESSAGE_COMPONENT: 3; +// APPLICATION_COMMAND_AUTOCOMPLETE: 4; +// }; +export enum InviteTargetTypes { + STREAM = 1, + EMBEDDED_APPLICATION, +} +// MFALevels: { +// NONE: 0; +// ELEVATED: 1; +// }; +// MessageActivityFlags: { +// INSTANCE: 1; +// JOIN: 2; +// SPECTATE: 4; +// JOIN_REQUEST: 8; +// SYNC: 16; +// PLAY: 32; +// PARTY_PRIVACY_FRIENDS: 64; +// PARTY_PRIVACY_VOICE_CHANNEL: 128; +// EMBEDDED: 256; +// }; +// MessageActivityTypes: { +// JOIN: 1; +// SPECTATE: 2; +// LISTEN: 3; +// JOIN_REQUEST: 5; +// }; +export const MessageFlags = { + CROSSPOSTED: 1, + IS_CROSSPOST: 2, + SUPPRESS_EMBEDS: 4, + SOURCE_MESSAGE_DELETED: 8, + URGENT: 16, + HAS_THREAD: 32, + EPHEMERAL: 64, + LOADING: 128, +} +// MessageTypes: { +// DEFAULT: 0; +// RECIPIENT_ADD: 1; +// RECIPIENT_REMOVE: 2; +// CALL: 3; +// CHANNEL_NAME_CHANGE: 4; +// CHANNEL_ICON_CHANGE: 5; +// CHANNEL_PINNED_MESSAGE: 6; +// GUILD_MEMBER_JOIN: 7; +// USER_PREMIUM_GUILD_SUBSCRIPTION: 8; +// USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1: 9; +// USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2: 10; +// USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3: 11; +// CHANNEL_FOLLOW_ADD: 12; + +// GUILD_DISCOVERY_DISQUALIFIED: 14; +// GUILD_DISCOVERY_REQUALIFIED: 15; +// GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING: 16; +// GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING: 17; +// THREAD_CREATED: 18; +// REPLY: 19; +// CHAT_INPUT_COMMAND: 20; +// THREAD_STARTER_MESSAGE: 21; +// GUILD_INVITE_REMINDER: 22; +// CONTEXT_MENU_COMMAND: 23; +// }; +// PermissionOverwriteTypes: { +// ROLE: 0; +// USER: 1; +// }; +// Permissions: { +// createInstantInvite: 1n; +// kickMembers: 2n; +// banMembers: 4n; +// administrator: 8n; +// manageChannels: 16n; +// manageGuild: 32n; +// addReactions: 64n; +// viewAuditLog: 128n; +// voicePrioritySpeaker: 256n; +// voiceStream: 512n; +// viewChannel: 1024n; +// sendMessages: 2048n; +// sendTTSMessages: 4096n; +// manageMessages: 8192n; +// embedLinks: 16384n; +// attachFiles: 32768n; +// readMessageHistory: 65536n; +// mentionEveryone: 131072n; +// useExternalEmojis: 262144n; +// viewGuildInsights: 524288n; +// voiceConnect: 1048576n; +// voiceSpeak: 2097152n; +// voiceMuteMembers: 4194304n; +// voiceDeafenMembers: 8388608n; +// voiceMoveMembers: 16777216n; +// voiceUseVAD: 33554432n; +// changeNickname: 67108864n; +// manageNicknames: 134217728n; +// manageRoles: 268435456n; +// manageWebhooks: 536870912n; +// manageEmojisAndStickers: 1073741824n; +// useApplicationCommands: 2147483648n; +// voiceRequestToSpeak: 4294967296n; +// manageEvents: 8589934592n; +// manageThreads: 17179869184n; +// createPublicThreads: 34359738368n; +// createPrivateThreads: 68719476736n; +// useExternalStickers: 137438953472n; +// sendMessagesInThreads: 274877906944n; +// startEmbeddedActivities: 549755813888n; +// moderateMembers: 1099511627776n; +// allGuild: 1110182461630n; +// allText: 535529258065n; +// allVoice: 554385278737n; +// all: 2199023255551n; +// }; +// PremiumTiers: { +// NONE: 0; +// TIER_1: 1; +// TIER_2: 2; +// TIER_3: 3; +// }; +// PremiumTypes: { +// NONE: 0; +// NITRO_CLASSIC: 1; +// NITRO: 2; +// }; +// StageInstancePrivacyLevel: { +// PUBLIC: 1; +// GUILD_ONLY: 2; +// }; +// StickerFormats: { +// PNG: 1; +// APNG: 2; +// LOTTIE: 3; +// }; +// StickerTypes: { +// STANDARD: 1; +// GUILD: 2; +// }; +// SystemChannelFlags: { +// SUPPRESS_JOIN_NOTIFICATIONS: 1; +// SUPPRESS_PREMIUM_SUBSCRIPTIONS: 2; +// SUPPRESS_GUILD_REMINDER_NOTIFICATIONS: 4; +// SUPPRESS_JOIN_NOTIFICATION_REPLIES: 8; +// }; +// SystemJoinMessages: [ +// "%user% joined the party.", +// "%user% is here.", +// "Welcome, %user%. We hope you brought pizza.", +// "A wild %user% appeared.", +// "%user% just landed.", +// "%user% just slid into the server.", +// "%user% just showed up!", +// "Welcome %user%. Say hi!", +// "%user% hopped into the server.", +// "Everyone welcome %user%!", +// "Glad you're here, %user%.", +// "Good to see you, %user%.", +// "Yay you made it, %user%!", +// ]; +// ThreadMemberFlags: { +// HAS_INTERACTED: 1; +// ALL_MESSAGES: 2; +// ONLY_MENTIONS: 4; +// NO_MESSAGES: 8; +// }; +// UserFlags: { +// NONE: 0; +// DISCORD_STAFF: 1; +// DISCORD_EMPLOYEE: 1; +// PARTNER: 2; +// PARTNERED_SERVER_OWNER: 2; +// HYPESQUAD: 4; +// HYPESQUAD_EVENTS: 4; +// BUG_HUNTER_LEVEL_1: 8; +// HYPESQUAD_ONLINE_HOUSE_1: 64; +// HOUSE_BRAVERY: 64; +// HYPESQUAD_ONLINE_HOUSE_2: 128; +// HOUSE_BRILLIANCE: 128; +// HYPESQUAD_ONLINE_HOUSE_3: 256; +// HOUSE_BALANCE: 256; +// PREMIUM_EARLY_SUPPORTER: 512; +// EARLY_SUPPORTER: 512; +// TEAM_PSEUDO_USER: 1024; +// TEAM_USER: 1024; +// SYSTEM: 4096; +// BUG_HUNTER_LEVEL_2: 16384; +// VERIFIED_BOT: 65536; +// VERIFIED_DEVELOPER: 131072; +// VERIFIED_BOT_DEVELOPER: 131072; +// EARLY_VERIFIED_BOT_DEVELOPER: 131072; +// CERTIFIED_MODERATOR: 262144; +// DISCORD_CERTIFIED_MODERATOR: 262144; +// BOT_HTTP_INTERACTIONS: 524288; +// }; +// VerificationLevels: { +// NONE: 0; +// LOW: 1; +// MEDIUM: 2; +// HIGH: 3; +// VERY_HIGH: 4; +// }; +// VideoQualityModes: { +// AUTO: 1; +// FULL: 2; +// }; +// VoiceOPCodes: { +// IDENTIFY: 0; +// SELECT_PROTOCOL: 1; +// READY: 2; +// HEARTBEAT: 3; +// SESSION_DESCRIPTION: 4; +// SPEAKING: 5; +// HEARTBEAT_ACK: 6; +// RESUME: 7; +// HELLO: 8; +// RESUMED: 9; +// CLIENT_DISCONNECT: 13; +// }; +// WebhookTypes: { +// INCOMING: 1; +// CHANNEL_FOLLOWER: 2; +// APPLICATION: 3; +// }; +// } + +// // Selfbot +// export interface Connection { +// friend_sync: boolean; +// id: string; +// integrations: unknown[]; // TODO ???? +// name: string; +// revoked: boolean; +// type: string; +// verified: boolean; +// visibility: ConnectionVisibilityTypes; +// } +// export interface GuildSettings { +// channel_override: { +// channel_id: string; +// message_notifications: number; +// muted: boolean; +// }[]; +// guild_id: string; +// message_notifications: number; +// mobile_push: boolean; +// muted: boolean; +// suppress_everyone: boolean; +// } +// export interface SearchOptions { +// attachmentExtensions?: string; +// attachmentFilename?: string; +// authorID?: string; +// channelIDs?: string[]; +// content?: string; +// contextSize?: number; +// embedProviders?: string; +// embedTypes?: string; +// has?: string; +// limit?: number; +// maxID?: string; +// minID?: string; +// offset?: number; +// sortBy?: string; +// sortOrder?: string; +// } +// export interface SearchResults { +// results: (Message & { hit?: boolean })[][]; +// totalResults: number; +// } +// export interface UserProfile { +// connected_accounts: { id: string; name: string; type: string; verified: boolean }[]; +// mutual_guilds: { id: string; nick?: string }[]; +// premium_since?: number; +// user: PartialUser & { flags: number }; +// } +// export interface UserSettings { +// afk_timeout: number; +// convert_emojis: boolean; +// default_guilds_restricted: boolean; +// detect_platform_accounts: boolean; +// developer_mode: boolean; +// enable_tts_command: boolean; +// explicit_content_filter: number; +// friend_source_flags: { +// all: boolean; // not sure about other keys, abal heeeelp +// }; +// inline_attachment_media: boolean; +// inline_embed_media: boolean; +// guild_positions: string[]; +// locale: string; +// message_display_compact: boolean; +// render_embeds: boolean; +// render_reactions: boolean; +// restricted_guilds: string[]; +// show_current_game: boolean; +// status: string; +// theme: string; +// } + +// class Base implements SimpleJSON { +// createdAt: number; +// id: string; +// constructor(id: string); +// static getCreatedAt(id: string): number; +// static getDiscordEpoch(id: string): number; +// inspect(): this; +// toString(): string; +// toJSON(props?: string[]): JSONCache; +// } + +// export class Bucket { +// interval: number; +// lastReset: number; +// lastSend: number; +// tokenLimit: number; +// tokens: number; +// constructor( +// tokenLimit: number, +// interval: number, +// options: { latencyRef: { latency: number }; reservedTokens: number }, +// ); +// check(): void; +// queue(func: () => void, priority?: boolean): void; +// } + +// export class BrowserWebSocket extends EventEmitter { +// static CONNECTING: 0; +// static OPEN: 1; +// static CLOSING: 2; +// static CLOSED: 3; +// readyState: number; +// constructor(url: string); +// close(code?: number, reason?: string): void; +// removeEventListener(event: string | symbol, listener: (...args: any[]) => void): this; +// // @ts-ignore: DOM +// send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; +// terminate(): void; +// } + +// export class BrowserWebSocketError extends Error { +// // @ts-ignore: DOM +// event: Event; +// // @ts-ignore: DOM +// constructor(message: string, event: Event); +// } + +// export class Call extends Base { +// channel: GroupChannel; +// createdAt: number; +// endedTimestamp: number | null; +// id: string; +// participants: string[]; +// region: string | null; +// ringing: string[]; +// unavailable: boolean; +// voiceStates: Collection; +// constructor(data: BaseData, channel: GroupChannel); +// } + +// export class CategoryChannel extends GuildChannel { +// channels: Collection>; +// type: Constants["ChannelTypes"]["GUILD_CATEGORY"]; +// edit(options: Omit, reason?: string): Promise; +// } + +// export class Channel extends Base { +// client: Client; +// createdAt: number; +// id: string; +// mention: string; +// type: ChannelTypes; +// constructor(data: BaseData, client: Client); +// static from(data: BaseData, client: Client): AnyChannel; +// } + +// export class Client extends EventEmitter { +// application?: { id: string; flags: number }; +// bot: boolean; +// channelGuildMap: { [s: string]: string }; +// gatewayURL?: string; +// // groupChannels: Collection; +// guilds: Collection; +// guildShardMap: { [s: string]: number }; +// lastConnect: number; +// lastReconnectDelay: number; +// // notes: { [s: string]: string }; +// options: ClientOptions; +// // presence: ClientPresence; +// privateChannelMap: { [s: string]: string }; +// privateChannels: Collection; +// ready: boolean; +// reconnectAttempts: number; +// // relationships: Collection; +// requestHandler: RequestHandler; +// shards: ShardManager; +// startTime: number; +// threadGuildMap: { [s: string]: string }; +// unavailableGuilds: Collection; +// uptime: number; +// user: ExtendedUser; +// // userGuildSettings: { [s: string]: GuildSettings }; +// users: Collection; +// // userSettings: UserSettings; +// voiceConnections: VoiceConnectionManager; +// constructor(token: string, options?: ClientOptions); +// acceptInvite(inviteID: string): Promise>; +// addGroupRecipient(groupID: string, userID: string): Promise; +// addGuildDiscoverySubcategory( +// guildID: string, +// categoryID: string, +// reason?: string, +// ): Promise; +// addGuildMemberRole(guildID: string, memberID: string, roleID: string, reason?: string): Promise; +// addMessageReaction(channelID: string, messageID: string, reaction: string): Promise; +// addRelationship(userID: string, block?: boolean): Promise; +// addSelfPremiumSubscription(token: string, plan: string): Promise; +// banGuildMember(guildID: string, userID: string, deleteMessageDays?: number, reason?: string): Promise; +// bulkEditCommandPermissions( +// guildID: string, +// permissions: { id: string; permissions: ApplicationCommandPermissions[] }[], +// ): Promise; +// bulkEditCommands(commands: ApplicationCommandStructure[]): Promise; +// bulkEditGuildCommands(guildID: string, commands: ApplicationCommandStructure[]): Promise; +// closeVoiceConnection(guildID: string): void; +// connect(): Promise; +// createChannel(guildID: string, name: string): Promise; +// createChannel( +// guildID: string, +// name: string, +// type: Constants["ChannelTypes"]["GUILD_TEXT"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// guildID: string, +// name: string, +// type: Constants["ChannelTypes"]["GUILD_VOICE"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// guildID: string, +// name: string, +// type: Constants["ChannelTypes"]["GUILD_CATEGORY"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// guildID: string, +// name: string, +// type: Constants["ChannelTypes"]["GUILD_NEWS"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// guildID: string, +// name: string, +// type: Constants["ChannelTypes"]["GUILD_STORE"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// guildID: string, +// name: string, +// type: Constants["ChannelTypes"]["GUILD_STAGE"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// guildID: string, +// name: string, +// export type?: number, +// options?: CreateChannelOptions, +// ): Promise; +// createChannelInvite( +// channelID: string, +// options?: CreateChannelInviteOptions, +// reason?: string, +// ): Promise>; +// createChannelWebhook( +// channelID: string, +// options: { name: string; avatar?: string | null }, +// reason?: string, +// ): Promise; +// createCommand(command: ApplicationCommandStructure): Promise; +// createGroupChannel(userIDs: string[]): Promise; +// createGuild(name: string, options?: CreateGuildOptions): Promise; +// createGuildCommand(guildID: string, command: ApplicationCommandStructure): Promise; +// createGuildEmoji(guildID: string, options: EmojiOptions, reason?: string): Promise; +// createGuildFromTemplate(code: string, name: string, icon?: string): Promise; +// createGuildSticker(guildID: string, options: CreateStickerOptions, reason?: string): Promise; +// createGuildTemplate(guildID: string, name: string, description?: string | null): Promise; +// createInteractionResponse( +// interactionID: string, +// interactionToken: string, +// options: InteractionResponse, +// file?: FileContent | FileContent[], +// ): Promise; +// createMessage(channelID: string, content: MessageContent, file?: FileContent | FileContent[]): Promise; +// createRole(guildID: string, options?: RoleOptions, reason?: string): Promise; +// createRole(guildID: string, options?: Role, reason?: string): Promise; +// createStageInstance(channelID: string, options: StageInstanceOptions): Promise; +// createThreadWithMessage( +// channelID: string, +// messageID: string, +// options: CreateThreadOptions, +// ): Promise; +// createThreadWithoutMessage( +// channelID: string, +// options: CreateThreadWithoutMessageOptions, +// ): Promise; +// crosspostMessage(channelID: string, messageID: string): Promise; +// deleteChannel(channelID: string, reason?: string): Promise; +// deleteChannelPermission(channelID: string, overwriteID: string, reason?: string): Promise; +// deleteCommand(commandID: string): Promise; +// deleteGuild(guildID: string): Promise; +// deleteGuildCommand(guildID: string, commandID: string): Promise; +// deleteGuildDiscoverySubcategory(guildID: string, categoryID: string, reason?: string): Promise; +// deleteGuildEmoji(guildID: string, emojiID: string, reason?: string): Promise; +// deleteGuildIntegration(guildID: string, integrationID: string): Promise; +// deleteGuildSticker(guildID: string, stickerID: string, reason?: string): Promise; +// deleteGuildTemplate(guildID: string, code: string): Promise; +// deleteInvite(inviteID: string, reason?: string): Promise; +// deleteMessage(channelID: string, messageID: string, reason?: string): Promise; +// deleteMessages(channelID: string, messageIDs: string[], reason?: string): Promise; +// deleteRole(guildID: string, roleID: string, reason?: string): Promise; +// deleteSelfConnection(platform: string, id: string): Promise; +// deleteSelfPremiumSubscription(): Promise; +// deleteStageInstance(channelID: string): Promise; +// deleteUserNote(userID: string): Promise; +// deleteWebhook(webhookID: string, token?: string, reason?: string): Promise; +// deleteWebhookMessage(webhookID: string, token: string, messageID: string): Promise; +// disableSelfMFATOTP(code: string): Promise<{ token: string }>; +// disconnect(options: { reconnect?: boolean | "auto" }): void; +// editAFK(afk: boolean): void; +// editChannel( +// channelID: string, +// options: EditChannelOptions, +// reason?: string, +// ): Promise; +// editChannelPermission( +// channelID: string, +// overwriteID: string, +// allow: bigint | number, +// deny: bigint | number, +// type: PermissionType, +// reason?: string, +// ): Promise; +// editChannelPosition(channelID: string, position: number, options?: EditChannelPositionOptions): Promise; +// editChannelPositions(guildID: string, channelPositions: ChannelPosition[]): Promise; +// editCommand(commandID: string, command: ApplicationCommandStructure): Promise; +// editCommandPermissions( +// guildID: string, +// commandID: string, +// permissions: ApplicationCommandPermissions[], +// ): Promise; +// editGuild(guildID: string, options: GuildOptions, reason?: string): Promise; +// editGuildCommand( +// guildID: string, +// commandID: string, +// command: ApplicationCommandStructure, +// ): Promise; +// editGuildDiscovery(guildID: string, options?: DiscoveryOptions): Promise; +// editGuildEmoji( +// guildID: string, +// emojiID: string, +// options: { name?: string; roles?: string[] }, +// reason?: string, +// ): Promise; +// editGuildIntegration(guildID: string, integrationID: string, options: IntegrationOptions): Promise; +// editGuildMember(guildID: string, memberID: string, options: MemberOptions, reason?: string): Promise; +// editGuildSticker(guildID: string, stickerID: string, options?: EditStickerOptions, reason?: string): Promise; +// editGuildTemplate(guildID: string, code: string, options: GuildTemplateOptions): Promise; +// editGuildVanity(guildID: string, code: string | null): Promise; +// editGuildVoiceState(guildID: string, options: VoiceStateOptions, userID?: string): Promise; +// editGuildWelcomeScreen(guildID: string, options: WelcomeScreenOptions): Promise; +// editGuildWidget(guildID: string, options: Widget): Promise; +// editMessage(channelID: string, messageID: string, content: MessageContentEdit): Promise; +// editRole(guildID: string, roleID: string, options: RoleOptions, reason?: string): Promise; // TODO not all options are available? +// editRolePosition(guildID: string, roleID: string, position: number): Promise; +// editSelf(options: { avatar?: string; username?: string }): Promise; +// editSelfConnection( +// platform: string, +// id: string, +// data: { friendSync: boolean; visibility: number }, +// ): Promise; +// editSelfSettings(data: UserSettings): Promise; +// editStageInstance(channelID: string, options: StageInstanceOptions): Promise; +// editStatus( +// status: SelfStatus, +// activities?: ActivityPartial[] | ActivityPartial, +// ): void; +// editStatus(activities?: ActivityPartial[] | ActivityPartial): void; +// editUserNote(userID: string, note: string): Promise; +// editWebhook( +// webhookID: string, +// options: WebhookOptions, +// token?: string, +// reason?: string, +// ): Promise; +// editWebhookMessage( +// webhookID: string, +// token: string, +// messageID: string, +// options: MessageWebhookContent, +// ): Promise>; +// emit(event: K, ...args: ClientEvents[K]): boolean; +// emit(event: string, ...args: any[]): boolean; +// enableSelfMFATOTP( +// secret: string, +// code: string, +// ): Promise<{ backup_codes: { code: string; consumed: boolean }[]; token: string }>; +// executeSlackWebhook( +// webhookID: string, +// token: string, +// options: Record & { auth?: boolean; threadID?: string }, +// ): Promise; +// executeSlackWebhook( +// webhookID: string, +// token: string, +// options: Record & { auth?: boolean; threadID?: string; wait: true }, +// ): Promise>; +// executeWebhook( +// webhookID: string, +// token: string, +// options: WebhookPayload & { wait: true }, +// ): Promise>; +// executeWebhook(webhookID: string, token: string, options: WebhookPayload): Promise; +// followChannel(channelID: string, webhookChannelID: string): Promise; +// getActiveGuildThreads(guildID: string): Promise; +// getArchivedThreads( +// channelID: string, +// type: "private", +// options?: GetArchivedThreadsOptions, +// ): Promise>; +// getArchivedThreads( +// channelID: string, +// type: "public", +// options?: GetArchivedThreadsOptions, +// ): Promise>; +// getBotGateway(): Promise< +// { +// session_start_limit: { max_concurrency: number; remaining: number; reset_after: number; total: number }; +// shards: number; +// url: string; +// } +// >; +// getChannel(channelID: string): AnyChannel; +// getChannelInvites(channelID: string): Promise; +// getChannelWebhooks(channelID: string): Promise; +// getCommand(commandID: string): Promise; +// getCommandPermissions(guildID: string, commandID: string): Promise; +// getCommands(): Promise; +// getDiscoveryCategories(): Promise; +// getDMChannel(userID: string): Promise; +// getEmojiGuild(emojiID: string): Promise; +// getGateway(): Promise<{ url: string }>; +// getGuildAuditLog(guildID: string, options?: GetGuildAuditLogOptions): Promise; +// getGuildBan(guildID: string, userID: string): Promise; +// getGuildBans(guildID: string, options?: GetGuildBansOptions): Promise; +// getGuildCommand(guildID: string, commandID: string): Promise; +// getGuildCommandPermissions(guildID: string): Promise; +// getGuildCommands(guildID: string): Promise; +// getGuildDiscovery(guildID: string): Promise; +// getGuildIntegrations(guildID: string): Promise; +// getGuildInvites(guildID: string): Promise; +// getGuildPreview(guildID: string): Promise; +// getGuildTemplate(code: string): Promise; +// getGuildTemplates(guildID: string): Promise; +// getGuildVanity(guildID: string): Promise; +// getGuildWebhooks(guildID: string): Promise; +// getGuildWelcomeScreen(guildID: string): Promise; +// getGuildWidget(guildID: string): Promise; +// getGuildWidgetSettings(guildID: string): Promise; +// getInvite(inviteID: string, withCounts?: false): Promise>; +// getInvite(inviteID: string, withCounts: true): Promise>; +// getJoinedPrivateArchivedThreads( +// channelID: string, +// options?: GetArchivedThreadsOptions, +// ): Promise>; +// getMessage(channelID: string, messageID: string): Promise; +// getMessageReaction( +// channelID: string, +// messageID: string, +// reaction: string, +// options?: GetMessageReactionOptions, +// ): Promise; +// getMessages(channelID: string, options?: GetMessagesOptions): Promise; +// getNitroStickerPacks(): Promise<{ sticker_packs: StickerPack[] }>; +// getOAuthApplication(appID?: string): Promise; +// getPins(channelID: string): Promise; +// getPruneCount(guildID: string, options?: GetPruneOptions): Promise; +// getRESTChannel(channelID: string): Promise; +// getRESTGuild(guildID: string, withCounts?: boolean): Promise; +// getRESTGuildChannels(guildID: string): Promise; +// getRESTGuildEmoji(guildID: string, emojiID: string): Promise; +// getRESTGuildEmojis(guildID: string): Promise; +// getRESTGuildMember(guildID: string, memberID: string): Promise; +// getRESTGuildMembers(guildID: string, options?: GetRESTGuildMembersOptions): Promise; +// getRESTGuildRoles(guildID: string): Promise; +// getRESTGuilds(options?: GetRESTGuildsOptions): Promise; +// getRESTGuildSticker(guildID: string, stickerID: string): Promise; +// getRESTGuildStickers(guildID: string): Promise; +// getRESTSticker(stickerID: string): Promise; +// getRESTUser(userID: string): Promise; +// getSelf(): Promise; +// getSelfBilling(): Promise<{ +// payment_gateway?: string; +// payment_source?: { +// brand: string; +// expires_month: number; +// expires_year: number; +// invalid: boolean; +// last_4: number; +// type: string; +// }; +// premium_subscription?: { +// canceled_at?: string; +// created_at: string; +// current_period_end?: string; +// current_period_start?: string; +// ended_at?: string; +// plan: string; +// status: number; +// }; +// }>; +// getSelfConnections(): Promise; +// getSelfMFACodes( +// password: string, +// regenerate?: boolean, +// ): Promise<{ backup_codes: { code: string; consumed: boolean }[] }>; +// getSelfPayments(): Promise<{ +// amount: number; +// amount_refunded: number; +// created_at: string; // date +// currency: string; +// description: string; +// status: number; +// }[]>; +// getSelfSettings(): Promise; +// getStageInstance(channelID: string): Promise; +// getThreadMembers(channelID: string): Promise; +// getUserProfile(userID: string): Promise; +// getVoiceRegions(guildID?: string): Promise; +// getWebhook(webhookID: string, token?: string): Promise; +// getWebhookMessage(webhookID: string, token: string, messageID: string): Promise>; +// joinThread(channelID: string, userID?: string): Promise; +// joinVoiceChannel(channelID: string, options?: JoinVoiceChannelOptions): Promise; +// kickGuildMember(guildID: string, userID: string, reason?: string): Promise; +// leaveGuild(guildID: string): Promise; +// leaveThread(channelID: string, userID?: string): Promise; +// leaveVoiceChannel(channelID: string): void; +// off(event: K, listener: (...args: ClientEvents[K]) => void): this; +// off(event: string, listener: (...args: any[]) => void): this; +// once(event: K, listener: (...args: ClientEvents[K]) => void): this; +// once(event: string, listener: (...args: any[]) => void): this; +// pinMessage(channelID: string, messageID: string): Promise; +// pruneMembers(guildID: string, options?: PruneMemberOptions): Promise; +// purgeChannel(channelID: string, options: PurgeChannelOptions): Promise; +// removeGroupRecipient(groupID: string, userID: string): Promise; +// removeGuildMemberRole(guildID: string, memberID: string, roleID: string, reason?: string): Promise; +// removeMessageReaction(channelID: string, messageID: string, reaction: string, userID?: string): Promise; +// removeMessageReactionEmoji(channelID: string, messageID: string, reaction: string): Promise; +// removeMessageReactions(channelID: string, messageID: string): Promise; +// removeRelationship(userID: string): Promise; +// searchChannelMessages(channelID: string, query: SearchOptions): Promise; +// searchGuildMembers(guildID: string, query: string, limit?: number): Promise; +// searchGuildMessages(guildID: string, query: SearchOptions): Promise; +// sendChannelTyping(channelID: string): Promise; +// syncGuildIntegration(guildID: string, integrationID: string): Promise; +// syncGuildTemplate(guildID: string, code: string): Promise; +// unbanGuildMember(guildID: string, userID: string, reason?: string): Promise; +// unpinMessage(channelID: string, messageID: string): Promise; +// validateDiscoverySearchTerm(term: string): Promise<{ valid: boolean }>; +// on(event: K, listener: (...args: ClientEvents[K]) => void): this; +// on(event: string, listener: (...args: any[]) => void): this; +// toString(): string; +// } + +// export class ErisCollection extends Map { +// baseObject: new (...args: any[]) => T; +// limit?: number; +// constructor(baseObject: new (...args: any[]) => T, limit?: number); +// add(obj: T, extra?: unknown, replace?: boolean): T; +// every(func: (i: T) => boolean): boolean; +// filter(func: (i: T) => boolean): T[]; +// find(func: (i: T) => boolean): T | undefined; +// map(func: (i: T) => R): R[]; +// random(): T | undefined; +// reduce(func: (accumulator: U, val: T) => U, initialValue?: U): U; +// remove(obj: T | Uncached): T | null; +// some(func: (i: T) => boolean): boolean; +// update(obj: T, extra?: unknown, replace?: boolean): T; +// } + +// export class Command implements CommandOptions, SimpleJSON { +// aliases: string[]; +// argsRequired: boolean; +// caseInsensitive: boolean; +// cooldown: number; +// cooldownExclusions: CommandCooldownExclusions; +// cooldownMessage: MessageContent | false | GenericCheckFunction; +// cooldownReturns: number; +// defaultSubcommandOptions: CommandOptions; +// deleteCommand: boolean; +// description: string; +// dmOnly: boolean; +// errorMessage: MessageContent | GenericCheckFunction; +// fullDescription: string; +// fullLabel: string; +// guildOnly: boolean; +// hidden: boolean; +// hooks: Hooks; +// invalidUsageMessage: MessageContent | false | GenericCheckFunction; +// label: string; +// parentCommand?: Command; +// permissionMessage: MessageContent | false | GenericCheckFunction; +// reactionButtons: null | CommandReactionButtons[]; +// reactionButtonTimeout: number; +// requirements: CommandRequirements; +// restartCooldown: boolean; +// subcommandAliases: { [alias: string]: string }; +// subcommands: { [s: string]: Command }; +// usage: string; +// constructor(label: string, generate: CommandGenerator, options?: CommandOptions); +// cooldownCheck(msg: Message): boolean; +// cooldownExclusionCheck(msg: Message): boolean; +// executeCommand(msg: Message, args: string[]): Promise; +// permissionCheck(msg: Message): Promise; +// process(args: string[], msg: Message): Promise; +// registerSubcommand(label: string, generator: CommandGenerator, options?: CommandOptions): Command; +// registerSubcommandAlias(alias: string, label: string): void; +// unregisterSubcommand(label: string): void; +// toString(): string; +// toJSON(props?: string[]): JSONCache; +// } + +// export class CommandClient extends Client { +// activeMessages: { [s: string]: ActiveMessages }; +// commandAliases: { [s: string]: string }; +// commandOptions: CommandClientOptions; +// commands: { [s: string]: Command }; +// guildPrefixes: { [s: string]: string | string[] }; +// preReady?: true; +// constructor(token: string, options: ClientOptions, commandOptions?: CommandClientOptions); +// checkPrefix(msg: Message): string; +// onMessageCreate(msg: Message): Promise; +// onMessageReactionEvent(msg: Message, emoji: Emoji, reactor: Member | Uncached | string): Promise; +// registerCommand(label: string, generator: CommandGenerator, options?: CommandOptions): Command; +// registerCommandAlias(alias: string, label: string): void; +// registerGuildPrefix(guildID: string, prefix: string[] | string): void; +// resolveCommand(label: string): Command; +// unregisterCommand(label: string): void; +// unwatchMessage(id: string, channelID: string): void; +// toString(): string; +// } + +// export class DiscordHTTPError extends Error { +// code: number; +// headers: IncomingHttpHeaders; +// name: "DiscordHTTPError"; +// req: ClientRequest; +// res: IncomingMessage; +// response: HTTPResponse; +// constructor(req: ClientRequest, res: IncomingMessage, response: HTTPResponse, stack: string); +// flattenErrors(errors: HTTPResponse, keyPrefix?: string): string[]; +// } + +// export class DiscordRESTError extends Error { +// code: number; +// headers: IncomingHttpHeaders; +// name: string; +// req: ClientRequest; +// res: IncomingMessage; +// response: HTTPResponse; +// constructor(req: ClientRequest, res: IncomingMessage, response: HTTPResponse, stack: string); +// flattenErrors(errors: HTTPResponse, keyPrefix?: string): string[]; +// } + +// export class ExtendedUser extends User { +// email: string; +// mfaEnabled: boolean; +// premiumType: PremiumTypes; +// verified: boolean; +// } + +// export class GroupChannel extends PrivateChannel { +// icon: string | null; +// iconURL: string | null; +// name: string; +// ownerID: string; +// recipients: Collection; +// type: Constants["ChannelTypes"]["GROUP_DM"]; +// addRecipient(userID: string): Promise; +// dynamicIconURL(format?: ImageFormat, size?: number): string | null; +// edit(options: { icon?: string; name?: string; ownerID?: string }): Promise; +// removeRecipient(userID: string): Promise; +// } + +// export class Guild extends Base { +// afkChannelID: string | null; +// afkTimeout: number; +// applicationID: string | null; +// approximateMemberCount?: number; +// approximatePresenceCount?: number; +// autoRemoved?: boolean; +// banner: string | null; +// bannerURL: string | null; +// channels: Collection; +// createdAt: number; +// defaultNotifications: DefaultNotifications; +// description: string | null; +// discoverySplash: string | null; +// discoverySplashURL: string | null; +// emojiCount?: number; +// emojis: Emoji[]; +// explicitContentFilter: ExplicitContentFilter; +// features: GuildFeatures[]; +// icon: string | null; +// iconURL: string | null; +// id: string; +// joinedAt: number; +// large: boolean; +// maxMembers: number; +// maxPresences?: number | null; +// maxVideoChannelUsers?: number; +// memberCount: number; +// members: Collection; +// mfaLevel: MFALevel; +// name: string; +// nsfwLevel: NSFWLevel; +// ownerID: string; +// preferredLocale: string; +// premiumProgressBarEnabled: boolean; +// premiumSubscriptionCount?: number; +// premiumTier: PremiumTier; +// primaryCategory?: DiscoveryCategory; +// primaryCategoryID?: number; +// publicUpdatesChannelID: string; +// roles: Collection; +// rulesChannelID: string | null; +// shard: Shard; +// splash: string | null; +// splashURL: string | null; +// stageInstances: Collection; +// stickers?: Sticker[]; +// systemChannelFlags: number; +// systemChannelID: string | null; +// threads: Collection; +// unavailable: boolean; +// vanityURL: string | null; +// verificationLevel: VerificationLevel; +// voiceStates: Collection; +// welcomeScreen?: WelcomeScreen; +// widgetChannelID?: string | null; +// widgetEnabled?: boolean | null; +// constructor(data: BaseData, client: Client); +// addDiscoverySubcategory(categoryID: string, reason?: string): Promise; +// addMemberRole(memberID: string, roleID: string, reason?: string): Promise; +// banMember(userID: string, deleteMessageDays?: number, reason?: string): Promise; +// bulkEditCommands(commands: ApplicationCommandStructure[]): Promise; +// createChannel(name: string): Promise; +// createChannel( +// name: string, +// type: Constants["ChannelTypes"]["GUILD_TEXT"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// name: string, +// type: Constants["ChannelTypes"]["GUILD_VOICE"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// name: string, +// type: Constants["ChannelTypes"]["GUILD_CATEGORY"], +// options?: CreateChannelOptions, +// ): Promise; +// createChannel( +// name: string, +// type: Constants["ChannelTypes"]["GUILD_NEWS"], +// options?: CreateChannelOptions | string, +// ): Promise; +// createChannel( +// name: string, +// type: Constants["ChannelTypes"]["GUILD_STORE"], +// options?: CreateChannelOptions | string, +// ): Promise; +// createChannel( +// name: string, +// type: Constants["ChannelTypes"]["GUILD_STAGE"], +// options?: CreateChannelOptions | string, +// ): Promise; +// createChannel(name: string, export type?: number, options?: CreateChannelOptions): Promise; +// createCommand(command: ApplicationCommandStructure): Promise; +// createEmoji(options: { image: string; name: string; roles?: string[] }, reason?: string): Promise; +// createRole(options: RoleOptions, reason?: string): Promise; +// createRole(options: Role, reason?: string): Promise; +// createSticker(options: CreateStickerOptions, reason?: string): Promise; +// createTemplate(name: string, description?: string | null): Promise; +// delete(): Promise; +// deleteCommand(commandID: string): Promise; +// deleteDiscoverySubcategory(categoryID: string, reason?: string): Promise; +// deleteEmoji(emojiID: string, reason?: string): Promise; +// deleteIntegration(integrationID: string): Promise; +// deleteRole(roleID: string): Promise; +// deleteSticker(stickerID: string, reason?: string): Promise; +// deleteTemplate(code: string): Promise; +// dynamicBannerURL(format?: ImageFormat, size?: number): string | null; +// dynamicDiscoverySplashURL(format?: ImageFormat, size?: number): string | null; +// dynamicIconURL(format?: ImageFormat, size?: number): string | null; +// dynamicSplashURL(format?: ImageFormat, size?: number): string | null; +// edit(options: GuildOptions, reason?: string): Promise; +// editChannelPositions(channelPositions: ChannelPosition[]): Promise; +// editCommand(commandID: string, command: ApplicationCommandStructure): Promise; +// editCommandPermissions(permissions: ApplicationCommandPermissions[]): Promise; +// editDiscovery(options?: DiscoveryOptions): Promise; +// editEmoji(emojiID: string, options: { name: string; roles?: string[] }, reason?: string): Promise; +// editIntegration(integrationID: string, options: IntegrationOptions): Promise; +// editMember(memberID: string, options: MemberOptions, reason?: string): Promise; +// editRole(roleID: string, options: RoleOptions): Promise; +// editSticker(stickerID: string, options?: EditStickerOptions, reason?: string): Promise; +// editTemplate(code: string, options: GuildTemplateOptions): Promise; +// editVanity(code: string | null): Promise; +// editVoiceState(options: VoiceStateOptions, userID?: string): Promise; +// editWelcomeScreen(options: WelcomeScreenOptions): Promise; +// editWidget(options: Widget): Promise; +// fetchAllMembers(timeout?: number): Promise; +// fetchMembers(options?: FetchMembersOptions): Promise; +// getActiveThreads(): Promise; +// getAuditLog(options?: GetGuildAuditLogOptions): Promise; +// getBan(userID: string): Promise; +// getBans(options?: GetGuildBansOptions): Promise; +// getCommand(commandID: string): Promise; +// getCommandPermissions(): Promise; +// getCommands(): Promise; +// getDiscovery(): Promise; +// getIntegrations(): Promise; +// getInvites(): Promise; +// getPruneCount(options?: GetPruneOptions): Promise; +// getRESTChannels(): Promise; +// getRESTEmoji(emojiID: string): Promise; +// getRESTEmojis(): Promise; +// getRESTMember(memberID: string): Promise; +// getRESTMembers(options?: GetRESTGuildMembersOptions): Promise; +// getRESTRoles(): Promise; +// getRESTSticker(stickerID: string): Promise; +// getRESTStickers(): Promise; +// getTemplates(): Promise; +// getVanity(): Promise; +// getVoiceRegions(): Promise; +// getWebhooks(): Promise; +// getWelcomeScreen(): Promise; +// getWidget(): Promise; +// getWidgetSettings(): Promise; +// kickMember(userID: string, reason?: string): Promise; +// leave(): Promise; +// leaveVoiceChannel(): void; +// permissionsOf(memberID: string | Member | MemberRoles): Permission; +// pruneMembers(options?: PruneMemberOptions): Promise; +// removeMemberRole(memberID: string, roleID: string, reason?: string): Promise; +// searchMembers(query: string, limit?: number): Promise; +// syncIntegration(integrationID: string): Promise; +// syncTemplate(code: string): Promise; +// unbanMember(userID: string, reason?: string): Promise; +// } + +// export class GuildAuditLogEntry extends Base { +// actionType: number; +// after: { [key: string]: unknown } | null; +// before: { [key: string]: unknown } | null; +// channel?: AnyGuildChannel; +// count?: number; +// deleteMemberDays?: number; +// guild: Guild; +// id: string; +// member?: Member | Uncached; +// membersRemoved?: number; +// message?: Message; +// reason: string | null; +// role?: Role | { id: string; name: string }; +// target?: Guild | AnyGuildChannel | Member | Role | Invite | Emoji | Sticker | Message | null; +// targetID: string; +// user: User; +// constructor(data: BaseData, guild: Guild); +// } + +// export class GuildChannel extends Channel { +// guild: Guild; +// name: string; +// nsfw: boolean; +// parentID: string | null; +// permissionOverwrites: Collection; +// position: number; +// type: GuildChannelTypes; +// constructor(data: BaseData, client: Client); +// delete(reason?: string): Promise; +// deletePermission(overwriteID: string, reason?: string): Promise; +// edit(options: Omit, reason?: string): Promise; +// editPermission( +// overwriteID: string, +// allow: bigint | number, +// deny: bigint | number, +// type: PermissionType, +// reason?: string, +// ): Promise; +// editPosition(position: number, options?: EditChannelPositionOptions): Promise; +// getInvites(): Promise; +// permissionsOf(memberID: string | Member | MemberRoles): Permission; +// } + +// export class GuildIntegration extends Base { +// account: { id: string; name: string }; +// application?: IntegrationApplication; +// createdAt: number; +// enabled: boolean; +// enableEmoticons?: boolean; +// expireBehavior?: GuildIntegrationExpireBehavior; +// expireGracePeriod?: number; +// id: string; +// name: string; +// revoked?: boolean; +// roleID?: string; +// subscriberCount?: number; +// syncedAt?: number; +// syncing?: boolean; +// type: GuildIntegrationTypes; +// user?: User; +// constructor(data: BaseData, guild: Guild); +// delete(): Promise; +// edit(options: IntegrationOptions): Promise; +// sync(): Promise; +// } + +// export class GuildPreview extends Base { +// approximateMemberCount: number; +// approximatePresenceCount: number; +// description: string | null; +// discoverySplash: string | null; +// discoverySplashURL: string | null; +// emojis: Emoji[]; +// features: GuildFeatures[]; +// icon: string | null; +// iconURL: string | null; +// id: string; +// name: string; +// splash: string | null; +// splashURL: string | null; +// constructor(data: BaseData, client: Client); +// dynamicDiscoverySplashURL(format?: ImageFormat, size?: number): string | null; +// dynamicIconURL(format?: ImageFormat, size?: number): string | null; +// dynamicSplashURL(format?: ImageFormat, size?: number): string | null; +// } + +// export class GuildTemplate { +// code: string; +// createdAt: number; +// creator: User; +// description: string | null; +// isDirty: string | null; +// name: string; +// serializedSourceGuild: Guild; +// sourceGuild: Guild | Uncached; +// updatedAt: number; +// usageCount: number; +// constructor(data: BaseData, client: Client); +// createGuild(name: string, icon?: string): Promise; +// delete(): Promise; +// edit(options: GuildTemplateOptions): Promise; +// sync(): Promise; +// toJSON(props?: string[]): JSONCache; +// } + +// export class TextVoiceChannel extends VoiceChannel implements GuildTextable { +// lastMessageID: string; +// messages: Collection>; +// rateLimitPerUser: number; +// addMessageReaction(messageID: string, reaction: string): Promise; +// createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; +// createWebhook(options: { name: string; avatar?: string | null }, reason?: string): Promise; +// deleteMessage(messageID: string, reason?: string): Promise; +// deleteMessages(messageIDs: string[], reason?: string): Promise; +// editMessage(messageID: string, content: MessageContentEdit): Promise>; +// getMessage(messageID: string): Promise>; +// getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; +// getMessages(options?: GetMessagesOptions): Promise[]>; +// getWebhooks(): Promise; +// purge(options: PurgeChannelOptions): Promise; +// removeMessageReaction(messageID: string, reaction: string, userID?: string): Promise; +// removeMessageReactionEmoji(messageID: string, reaction: string): Promise; +// removeMessageReactions(messageID: string): Promise; +// sendTyping(): Promise; +// unsendMessage(messageID: string): Promise; +// } +// export class Interaction extends Base { +// acknowledged: boolean; +// applicationID: string; +// id: string; +// token: string; +// type: number; +// version: number; +// static from(data: BaseData): AnyInteraction; +// } + +// export class PingInteraction extends Interaction { +// type: Constants["InteractionTypes"]["PING"]; +// acknowledge(): Promise; +// pong(): Promise; +// } + +// export class CommandInteraction extends Interaction { +// channel: T; +// data: { +// id: string; +// name: string; +// type: ApplicationCommandTypes; +// target_id?: string; +// resolved?: { +// users?: Collection; +// members?: Collection>; +// roles?: Collection; +// channels?: Collection; +// messages?: Collection; +// }; +// options?: InteractionDataOptions[]; +// }; +// guildID?: string; +// member?: Member; +// type: Constants["InteractionTypes"]["APPLICATION_COMMAND"]; +// user?: User; +// acknowledge(flags?: number): Promise; +// createFollowup(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise; +// createMessage(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise; +// defer(flags?: number): Promise; +// deleteMessage(messageID: string): Promise; +// deleteOriginalMessage(): Promise; +// editMessage( +// messageID: string, +// content: string | InteractionContentEdit, +// file?: FileContent | FileContent[], +// ): Promise; +// editOriginalMessage(content: string | InteractionContentEdit, file?: FileContent | FileContent[]): Promise; +// getOriginalMessage(): Promise; +// } + +// export interface ComponentInteractionButtonData { +// component_type: Constants["ComponentTypes"]["BUTTON"]; +// custom_id: string; +// } + +// export interface ComponentInteractionSelectMenuData { +// component_type: Constants["ComponentTypes"]["SELECT_MENU"]; +// custom_id: string; +// values: string[]; +// } + +// export class ComponentInteraction extends Interaction { +// channel: T; +// data: ComponentInteractionButtonData | ComponentInteractionSelectMenuData; +// guildID?: string; +// member?: Member; +// message: Message; +// type: Constants["InteractionTypes"]["MESSAGE_COMPONENT"]; +// user?: User; +// acknowledge(): Promise; +// createFollowup(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise; +// createMessage(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise; +// defer(flags?: number): Promise; +// deferUpdate(): Promise; +// deleteMessage(messageID: string): Promise; +// deleteOriginalMessage(): Promise; +// editMessage( +// messageID: string, +// content: string | InteractionContentEdit, +// file?: FileContent | FileContent[], +// ): Promise; +// editOriginalMessage(content: string | InteractionContentEdit, file?: FileContent | FileContent[]): Promise; +// editParent(content: InteractionContentEdit, file?: FileContent | FileContent[]): Promise; +// getOriginalMessage(): Promise; +// } +// export class AutocompleteInteraction extends Interaction { +// channel: T; +// data: { +// id: string; +// name: string; +// type: Constants["ApplicationCommandTypes"]["CHAT_INPUT"]; +// target_id?: string; +// options: InteractionDataOptions[]; +// }; +// guildID?: string; +// member?: Member; +// type: Constants["InteractionTypes"]["APPLICATION_COMMAND_AUTOCOMPLETE"]; +// user?: User; +// acknowledge(choices: ApplicationCommandOptionChoice[]): Promise; +// result(choices: ApplicationCommandOptionChoice[]): Promise; +// } +// export class UnknownInteraction extends Interaction { +// channel?: T; +// data?: unknown; +// guildID?: string; +// member?: Member; +// message?: Message; +// type: number; +// user?: User; +// acknowledge(data: InteractionResponse): Promise; +// createFollowup(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise; +// createMessage(content: string | InteractionContent, file?: FileContent | FileContent[]): Promise; +// defer(flags?: number): Promise; +// deferUpdate(): Promise; +// deleteMessage(messageID: string): Promise; +// deleteOriginalMessage(): Promise; +// editMessage( +// messageID: string, +// content: string | InteractionContentEdit, +// file?: FileContent | FileContent[], +// ): Promise; +// editOriginalMessage(content: string | InteractionContentEdit, file?: FileContent | FileContent[]): Promise; +// editParent(content: InteractionContentEdit, file?: FileContent | FileContent[]): Promise; +// getOriginalMessage(): Promise; +// pong(): Promise; +// result(choices: ApplicationCommandOptionChoice[]): Promise; +// } + +// // If CT (count) is "withMetadata", it will not have count properties +// export class Invite< +// CT extends "withMetadata" | "withCount" | "withoutCount" = "withMetadata", +// CH extends InviteChannel = InviteChannel, +// > extends Base { +// channel: CH; +// code: string; +// // @ts-ignore: Property is only not null when invite metadata is supplied +// createdAt: CT extends "withMetadata" ? number : null; +// guild: CT extends "withMetadata" ? Guild // Invite with Metadata always has guild prop +// : CH extends Extract // Invite without Metadata +// ? never // If the channel is GroupChannel, there is no guild +// : CH extends Exclude // Invite without Metadata and not GroupChanel +// ? Guild // If the invite channel is not partial +// : Guild | undefined; // If the invite channel is partial +// inviter?: User; +// maxAge: CT extends "withMetadata" ? number : null; +// maxUses: CT extends "withMetadata" ? number : null; +// memberCount: CT extends "withMetadata" | "withoutCount" ? null : number; +// presenceCount: CT extends "withMetadata" | "withoutCount" ? null : number; +// stageInstance: CH extends StageChannel ? InviteStageInstance : null; +// temporary: CT extends "withMetadata" ? boolean : null; +// uses: CT extends "withMetadata" ? number : null; +// constructor(data: BaseData, client: Client); +// delete(reason?: string): Promise; +// } + +// export class Member extends Base implements Presence { +// accentColor?: number | null; +// activities?: Activity[]; +// avatar: string | null; +// avatarURL: string; +// banner?: string | null; +// bannerURL: string | null; +// bot: boolean; +// clientStatus?: ClientStatus; +// communicationDisabledUntil: number | null; +// createdAt: number; +// defaultAvatar: string; +// defaultAvatarURL: string; +// discriminator: string; +// game: Activity | null; +// guild: Guild; +// id: string; +// joinedAt: number | null; +// mention: string; +// nick: string | null; +// pending?: boolean; +// permissions: Permission; +// premiumSince?: number | null; +// roles: string[]; +// staticAvatarURL: string; +// status?: Status; +// user: User; +// username: string; +// voiceState: VoiceState; +// constructor(data: BaseData, guild?: Guild, client?: Client); +// addRole(roleID: string, reason?: string): Promise; +// ban(deleteMessageDays?: number, reason?: string): Promise; +// edit(options: MemberOptions, reason?: string): Promise; +// kick(reason?: string): Promise; +// removeRole(roleID: string, reason?: string): Promise; +// unban(reason?: string): Promise; +// } + +// export class Message extends Base { +// activity?: MessageActivity; +// application?: MessageApplication; +// applicationID?: string; +// attachments: Attachment[]; +// author: User; +// channel: T; +// channelMentions: string[]; +// command?: Command; +// components?: ActionRow[]; +// content: string; +// createdAt: number; +// editedTimestamp?: number; +// embeds: Embed[]; +// flags: number; +// guildID: T extends GuildTextableWithThread ? string : undefined; +// id: string; +// interaction: MessageInteraction | null; +// jumpLink: string; +// member: T extends GuildTextableWithThread ? Member : null; +// mentionEveryone: boolean; +// mentions: User[]; +// messageReference: MessageReference | null; +// pinned: boolean; +// prefix?: string; +// reactions: { [s: string]: { count: number; me: boolean } }; +// referencedMessage?: Message | null; +// roleMentions: string[]; +// stickerItems?: StickerItems[]; +// timestamp: number; +// tts: boolean; +// type: number; +// webhookID: T extends GuildTextableWithThread ? string | undefined : undefined; +// constructor(data: BaseData, client: Client); +// addReaction(reaction: string): Promise; +// createThreadWithMessage(options: CreateThreadOptions): Promise; +// crosspost(): Promise : never>; +// delete(reason?: string): Promise; +// deleteWebhook(token: string): Promise; +// edit(content: MessageContent): Promise>; +// editWebhook(token: string, options: MessageWebhookContent): Promise>; +// getReaction(reaction: string, options?: GetMessageReactionOptions): Promise; +// pin(): Promise; +// removeReaction(reaction: string, userID?: string): Promise; +// removeReactionEmoji(reaction: string): Promise; +// removeReactions(): Promise; +// unpin(): Promise; +// } + +// // News channel rate limit is always 0 +// export class NewsChannel extends TextChannel implements GuildPinnable { +// rateLimitPerUser: 0; +// type: Constants["ChannelTypes"]["GUILD_NEWS"]; +// createInvite(options?: CreateInviteOptions, reason?: string): Promise>; +// createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; +// createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; +// crosspostMessage(messageID: string): Promise>; +// editMessage(messageID: string, content: MessageContentEdit): Promise>; +// follow(webhookChannelID: string): Promise; +// getInvites(): Promise<(Invite<"withMetadata", this>)[]>; +// getMessage(messageID: string): Promise>; +// getMessages(options?: GetMessagesOptions): Promise[]>; +// getPins(): Promise[]>; +// } + +// export class NewsThreadChannel extends ThreadChannel { +// type: Constants["ChannelTypes"]["GUILD_NEWS_THREAD"]; +// } + +// export class Permission extends Base { +// allow: bigint; +// deny: bigint; +// json: Record; +// constructor(allow: number | string | bigint, deny?: number | string | bigint); +// has(permission: keyof Constants["Permissions"] | bigint): boolean; +// } + +// export class PermissionOverwrite extends Permission { +// id: string; +// type: PermissionType; +// constructor(data: Overwrite); +// } + +// export class Piper extends EventEmitter { +// converterCommand: ConverterCommand; +// dataPacketCount: number; +// encoding: boolean; +// libopus: boolean; +// opus: OpusScript | null; +// opusFactory: () => OpusScript; +// volumeLevel: number; +// constructor(converterCommand: string, opusFactory: OpusScript); +// addDataPacket(packet: unknown): void; +// encode(source: string | Stream, options: VoiceResourceOptions): boolean; +// getDataPacket(): Buffer; +// reset(): void; +// resetPackets(): void; +// setVolume(volume: number): void; +// stop(e: Error, source: Duplex): void; +// } + +// export class PrivateChannel extends Channel implements Textable, Pinnable { +// lastMessageID: string; +// messages: Collection>; +// recipient: User; +// type: PrivateChannelTypes; +// addMessageReaction(messageID: string, reaction: string): Promise; +// createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; +// deleteMessage(messageID: string, reason?: string): Promise; +// editMessage(messageID: string, content: MessageContentEdit): Promise>; +// getMessage(messageID: string): Promise>; +// getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; +// getMessages(options?: GetMessagesOptions): Promise[]>; +// getPins(): Promise[]>; +// leave(): Promise; +// pinMessage(messageID: string): Promise; +// removeMessageReaction(messageID: string, reaction: string): Promise; +// ring(recipient: string[]): void; +// sendTyping(): Promise; +// syncCall(): void; +// unpinMessage(messageID: string): Promise; +// unsendMessage(messageID: string): Promise; +// } + +// export class PrivateThreadChannel extends ThreadChannel { +// threadMetadata: PrivateThreadMetadata; +// type: Constants["ChannelTypes"]["GUILD_PRIVATE_THREAD"]; +// } + +// export class PublicThreadChannel extends ThreadChannel { +// type: GuildPublicThreadChannelTypes; +// edit( +// options: Pick, +// reason?: string, +// ): Promise; +// } + +// export class Relationship extends Base implements Omit { +// activities: Activity[] | null; +// clientStatus?: ClientStatus; +// id: string; +// status: Status; +// type: number; +// user: User; +// constructor(data: BaseData, client: Client); +// } + +// export class ErisRequestHandler implements SimpleJSON { +// globalBlock: boolean; +// latencyRef: LatencyRef; +// options: RequestHandlerOptions; +// ratelimits: { [route: string]: SequentialBucket }; +// readyQueue: (() => void)[]; +// userAgent: string; +// constructor(client: Client, options?: RequestHandlerOptions); +// globalUnblock(): void; +// request( +// method: RequestMethod, +// url: string, +// auth?: boolean, +// body?: { [s: string]: unknown }, +// file?: FileContent, +// _route?: string, +// short?: boolean, +// ): Promise; +// routefy(url: string, method: RequestMethod): string; +// toString(): string; +// toJSON(props?: string[]): JSONCache; +// } + +// export class Role extends Base { +// color: number; +// createdAt: number; +// guild: Guild; +// hoist: boolean; +// icon: string | null; +// iconURL: string | null; +// id: string; +// json: Partial, boolean>>; +// managed: boolean; +// mention: string; +// mentionable: boolean; +// name: string; +// permissions: Permission; +// position: number; +// tags?: RoleTags; +// unicodeEmoji: string | null; +// constructor(data: BaseData, guild: Guild); +// delete(reason?: string): Promise; +// edit(options: RoleOptions, reason?: string): Promise; +// editPosition(position: number): Promise; +// } + +// class SequentialBucket { +// latencyRef: LatencyRef; +// limit: number; +// processing: boolean; +// remaining: number; +// reset: number; +// constructor(limit: number, latencyRef?: LatencyRef); +// check(override?: boolean): void; +// queue(func: (cb: () => void) => void, short?: boolean): void; +// } + +// export class Shard extends EventEmitter implements SimpleJSON { +// client: Client; +// connectAttempts: number; +// connecting: boolean; +// connectTimeout: number | null; +// discordServerTrace?: string[]; +// getAllUsersCount: { [guildID: string]: boolean }; +// getAllUsersLength: number; +// getAllUsersQueue: string; +// globalBucket: Bucket; +// guildCreateTimeout: number | null; +// guildSyncQueue: string[]; +// guildSyncQueueLength: number; +// heartbeatInterval: number | null; +// id: number; +// lastHeartbeatAck: boolean; +// lastHeartbeatReceived: number | null; +// lastHeartbeatSent: number | null; +// latency: number; +// preReady: boolean; +// presence: ClientPresence; +// presenceUpdateBucket: Bucket; +// ready: boolean; +// reconnectInterval: number; +// requestMembersPromise: { [s: string]: RequestMembersPromise }; +// seq: number; +// sessionID: string | null; +// status: "connecting" | "disconnected" | "handshaking" | "identifying" | "ready" | "resuming"; +// unsyncedGuilds: number; +// ws: WebSocket | BrowserWebSocket | null; +// constructor(id: number, client: Client); +// checkReady(): void; +// connect(): void; +// createGuild(_guild: Guild): Guild; +// disconnect(options?: { reconnect?: boolean | "auto" }, error?: Error): void; +// editAFK(afk: boolean): void; +// editStatus( +// status: SelfStatus, +// activities?: ActivityPartial[] | ActivityPartial, +// ): void; +// editStatus(activities?: ActivityPartial[] | ActivityPartial): void; +// // @ts-ignore: Method override +// emit(event: string, ...args: any[]): void; +// emit(event: K, ...args: ShardEvents[K]): boolean; +// emit(event: string, ...args: any[]): boolean; +// getGuildMembers(guildID: string, timeout: number): void; +// hardReset(): void; +// heartbeat(normal?: boolean): void; +// identify(): void; +// initializeWS(): void; +// off(event: K, listener: (...args: ShardEvents[K]) => void): this; +// off(event: string, listener: (...args: any[]) => void): this; +// once(event: K, listener: (...args: ShardEvents[K]) => void): this; +// once(event: string, listener: (...args: any[]) => void): this; +// onPacket(packet: RawPacket): void; +// requestGuildMembers(guildID: string, options?: RequestGuildMembersOptions): Promise; +// requestGuildSync(guildID: string): void; +// reset(): void; +// restartGuildCreateTimeout(): void; +// resume(): void; +// sendStatusUpdate(): void; +// sendWS(op: number, _data: Record, priority?: boolean): void; +// syncGuild(guildID: string): void; +// wsEvent(packet: Required): void; +// on(event: K, listener: (...args: ShardEvents[K]) => void): this; +// on(event: string, listener: (...args: any[]) => void): this; +// toJSON(props?: string[]): JSONCache; +// } + +// export class ShardManager extends Collection implements SimpleJSON { +// buckets: Map; +// connectQueue: Shard[]; +// connectTimeout: NodeJS.Timer | null; +// constructor(client: Client, options: ShardManagerOptions); +// connect(shard: Shard): void; +// spawn(id: number): void; +// tryConnect(): void; +// toString(): string; +// toJSON(props?: string[]): JSONCache; +// } + +// export class SharedStream extends EventEmitter { +// bitrate: number; +// channels: number; +// current?: VoiceStreamCurrent; +// ended: boolean; +// frameDuration: number; +// piper: Piper; +// playing: boolean; +// samplingRate: number; +// speaking: boolean; +// voiceConnections: Collection; +// volume: number; +// add(connection: VoiceConnection): void; +// emit(event: K, ...args: StreamEvents[K]): boolean; +// emit(event: string, ...args: any[]): boolean; +// off(event: K, listener: (...args: StreamEvents[K]) => void): this; +// off(event: string, listener: (...args: any[]) => void): this; +// once(event: K, listener: (...args: StreamEvents[K]) => void): this; +// once(event: string, listener: (...args: any[]) => void): this; +// play(resource: ReadableStream | string, options?: VoiceResourceOptions): void; +// remove(connection: VoiceConnection): void; +// setSpeaking(value: boolean): void; +// setVolume(volume: number): void; +// stopPlaying(): void; +// on(event: K, listener: (...args: StreamEvents[K]) => void): this; +// on(event: string, listener: (...args: any[]) => void): this; +// } + +// export class StageChannel extends VoiceChannel { +// topic?: string; +// type: Constants["ChannelTypes"]["GUILD_STAGE_VOICE"]; +// createInstance(options: StageInstanceOptions): Promise; +// deleteInstance(): Promise; +// editInstance(options: StageInstanceOptions): Promise; +// getInstance(): Promise; +// } + +// export class StageInstance extends Base { +// channel: StageChannel | Uncached; +// client: Client; +// discoverableDisabled: boolean; +// guild: Guild | Uncached; +// privacyLevel: StageInstancePrivacyLevel; +// topic: string; +// constructor(data: BaseData, client: Client); +// delete(): Promise; +// edit(options: StageInstanceOptions): Promise; +// update(data: BaseData): void; +// } + +// export class StoreChannel extends GuildChannel { +// type: Constants["ChannelTypes"]["GUILD_STORE"]; +// edit(options: Omit, reason?: string): Promise; +// } + +// export class TextChannel extends GuildChannel implements GuildTextable, Invitable, GuildPinnable { +// defaultAutoArchiveDuration: AutoArchiveDuration; +// lastMessageID: string; +// lastPinTimestamp: number | null; +// messages: Collection>; +// rateLimitPerUser: number; +// topic?: string | null; +// type: GuildTextChannelTypes; +// constructor(data: BaseData, client: Client, messageLimit: number); +// addMessageReaction(messageID: string, reaction: string): Promise; +// createInvite(options?: CreateInviteOptions, reason?: string): Promise>; +// createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; +// createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; +// createThreadWithoutMessage(options: CreateThreadWithoutMessageOptions): Promise; +// createWebhook(options: { name: string; avatar?: string | null }, reason?: string): Promise; +// deleteMessage(messageID: string, reason?: string): Promise; +// deleteMessages(messageIDs: string[], reason?: string): Promise; +// edit(options: Omit, reason?: string): Promise; +// editMessage(messageID: string, content: MessageContentEdit): Promise>; +// getArchivedThreads( +// type: "private", +// options?: GetArchivedThreadsOptions, +// ): Promise>; +// getArchivedThreads( +// type: "public", +// options?: GetArchivedThreadsOptions, +// ): Promise>; +// getInvites(): Promise<(Invite<"withMetadata", this>)[]>; +// getJoinedPrivateArchivedThreads( +// options: GetArchivedThreadsOptions, +// ): Promise>; +// getMessage(messageID: string): Promise>; +// getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; +// getMessages(options?: GetMessagesOptions): Promise[]>; +// getPins(): Promise[]>; +// getWebhooks(): Promise; +// pinMessage(messageID: string): Promise; +// purge(options: PurgeChannelOptions): Promise; +// removeMessageReaction(messageID: string, reaction: string, userID?: string): Promise; +// removeMessageReactionEmoji(messageID: string, reaction: string): Promise; +// removeMessageReactions(messageID: string): Promise; +// sendTyping(): Promise; +// unpinMessage(messageID: string): Promise; +// unsendMessage(messageID: string): Promise; +// } + +// export class ThreadChannel extends GuildChannel implements ThreadTextable { +// lastMessageID: string; +// lastPinTimestamp?: number; +// member?: ThreadMember; +// memberCount: number; +// members: Collection; +// messageCount: number; +// messages: Collection>; +// ownerID: string; +// rateLimitPerUser: number; +// threadMetadata: ThreadMetadata; +// type: GuildThreadChannelTypes; +// constructor(data: BaseData, client: Client, messageLimit?: number); +// addMessageReaction(messageID: string, reaction: string): Promise; +// createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; +// deleteMessage(messageID: string, reason?: string): Promise; +// deleteMessages(messageIDs: string[], reason?: string): Promise; +// edit( +// options: Pick< +// EditChannelOptions, +// "archived" | "autoArchiveDuration" | "invitable" | "locked" | "name" | "rateLimitPerUser" +// >, +// reason?: string, +// ): Promise; +// editMessage(messageID: string, content: MessageContentEdit): Promise>; +// getMembers(): Promise; +// getMessage(messageID: string): Promise>; +// getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; +// getMessages(options?: GetMessagesOptions): Promise[]>; +// getPins(): Promise[]>; +// join(userID?: string): Promise; +// leave(userID?: string): Promise; +// pinMessage(messageID: string): Promise; +// purge(options: PurgeChannelOptions): Promise; +// removeMessageReaction(messageID: string, reaction: string, userID?: string): Promise; +// removeMessageReactionEmoji(messageID: string, reaction: string): Promise; +// removeMessageReactions(messageID: string): Promise; +// sendTyping(): Promise; +// unpinMessage(messageID: string): Promise; +// unsendMessage(messageID: string): Promise; +// } + +// export class ThreadMember extends Base { +// flags: number; +// guildMember?: Member; +// joinTimestamp: number; +// threadID: string; +// constructor(data: BaseData, client: Client); +// leave(): Promise; +// update(data: BaseData): void; +// } + +// export class UnavailableGuild extends Base { +// createdAt: number; +// id: string; +// shard: Shard; +// unavailable: boolean; +// constructor(data: BaseData, client: Client); +// } + +// export class User extends Base { +// accentColor?: number | null; +// avatar: string | null; +// avatarURL: string; +// banner?: string | null; +// bannerURL: string | null; +// bot: boolean; +// createdAt: number; +// defaultAvatar: string; +// defaultAvatarURL: string; +// discriminator: string; +// id: string; +// mention: string; +// publicFlags?: number; +// staticAvatarURL: string; +// system: boolean; +// username: string; +// constructor(data: BaseData, client: Client); +// addRelationship(block?: boolean): Promise; +// deleteNote(): Promise; +// dynamicAvatarURL(format?: ImageFormat, size?: number): string; +// dynamicBannerURL(format?: ImageFormat, size?: number): string | null; +// editNote(note: string): Promise; +// getDMChannel(): Promise; +// getProfile(): Promise; +// removeRelationship(): Promise; +// } + +// export class VoiceChannel extends GuildChannel implements Invitable { +// bitrate: number; +// rtcRegion: string | null; +// type: TextVoiceChannelTypes; +// userLimit: number; +// videoQualityMode: VideoQualityMode; +// voiceMembers: Collection; +// createInvite(options?: CreateInviteOptions, reason?: string): Promise>; +// getInvites(): Promise<(Invite<"withMetadata", VoiceChannel>)[]>; +// join(options?: JoinVoiceChannelOptions): Promise; +// leave(): void; +// } + +// export class VoiceConnection extends EventEmitter implements SimpleJSON { +// bitrate: number; +// channelID: string | null; +// channels: number; +// connecting: boolean; +// connectionTimeout: number | null; +// current?: VoiceStreamCurrent | null; +// ended?: boolean; +// endpoint: URL; +// frameDuration: number; +// frameSize: number; +// heartbeatInterval: number | null; +// id: string; +// mode?: string; +// modes?: string; +// /** Optional dependencies OpusScript (opusscript) or OpusEncoder (@discordjs/opus) */ +// opus: { [userID: string]: unknown }; +// opusOnly: boolean; +// paused: boolean; +// pcmSize: number; +// piper: Piper; +// playing: boolean; +// ready: boolean; +// receiveStreamOpus?: VoiceDataStream | null; +// receiveStreamPCM?: VoiceDataStream | null; +// reconnecting: boolean; +// samplingRate: number; +// secret: Buffer; +// sendBuffer: Buffer; +// sendNonce: Buffer; +// sequence: number; +// shard: Shard | Record; +// shared: boolean; +// speaking: boolean; +// ssrc?: number; +// ssrcUserMap: { [s: number]: string }; +// timestamp: number; +// udpIP?: string; +// udpPort?: number; +// udpSocket: DgramSocket | null; +// volume: number; +// ws: BrowserWebSocket | WebSocket | null; +// constructor(id: string, options?: { shard?: Shard; shared?: boolean; opusOnly?: boolean }); +// connect(data: VoiceConnectData): NodeJS.Timer | void; +// disconnect(error?: Error, reconnecting?: boolean): void; +// emit(event: K, ...args: VoiceEvents[K]): boolean; +// emit(event: string, ...args: any[]): boolean; +// heartbeat(): void; +// off(event: K, listener: (...args: VoiceEvents[K]) => void): this; +// off(event: string, listener: (...args: any[]) => void): this; +// once(event: K, listener: (...args: VoiceEvents[K]) => void): this; +// once(event: string, listener: (...args: any[]) => void): this; +// pause(): void; +// play(resource: ReadableStream | string, options?: VoiceResourceOptions): void; +// receive(type: "opus" | "pcm"): VoiceDataStream; +// registerReceiveEventHandler(): void; +// resume(): void; +// sendAudioFrame(frame: Buffer): void; +// sendUDPPacket(packet: Buffer): void; +// sendWS(op: number, data: Record): void; +// setSpeaking(value: boolean): void; +// setVolume(volume: number): void; +// stopPlaying(): void; +// switchChannel(channelID: string): void; +// updateVoiceState(selfMute: boolean, selfDeaf: boolean): void; +// on(event: K, listener: (...args: VoiceEvents[K]) => void): this; +// on(event: string, listener: (...args: any[]) => void): this; +// toJSON(props?: string[]): JSONCache; +// } + +// export class VoiceConnectionManager extends Collection +// implements SimpleJSON { +// constructor(vcObject: new () => T); +// join(guildID: string, channelID: string, options: VoiceResourceOptions): Promise; +// leave(guildID: string): void; +// switch(guildID: string, channelID: string): void; +// voiceServerUpdate(data: VoiceServerUpdateData): void; +// toJSON(props?: string[]): JSONCache; +// } + +// export class VoiceDataStream extends EventEmitter { +// type: "opus" | "pcm"; +// constructor(type: string); +// on(event: "data", listener: (data: Buffer, userID: string, timestamp: number, sequence: number) => void): this; +// } + +// export class VoiceState extends Base { +// channelID: string | null; +// createdAt: number; +// deaf: boolean; +// id: string; +// mute: boolean; +// requestToSpeakTimestamp: number | null; +// selfDeaf: boolean; +// selfMute: boolean; +// selfStream: boolean; +// selfVideo: boolean; +// sessionID: string | null; +// suppress: boolean; +// constructor(data: BaseData); +// } diff --git a/packages/client/src/utils/BrowserWebSocket.ts b/packages/client/src/utils/BrowserWebSocket.ts new file mode 100644 index 000000000..0c4cae659 --- /dev/null +++ b/packages/client/src/utils/BrowserWebSocket.ts @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-var-requires */ +let EventEmitter +try { + EventEmitter = require('eventemitter3') +} catch (err) { + EventEmitter = require('events').EventEmitter +} + +class BrowserWebSocketError extends Error { + static CONNECTING: 0 + static OPEN: 1 + static CLOSING: 2 + static CLOSED: 3 + + readyState: number = 0 + event: Event + + constructor(message: string | undefined, event: Event) { + super(message) + + this.event = event + } +} + +/** + * Represents a browser's websocket usable by Eris + * @extends EventEmitter + * @prop {String} url The URL to connect to + */ +class BrowserWebSocket extends EventEmitter { + _ws: WebSocket + + constructor(url: string) { + super() + + if (typeof window === 'undefined') { + throw new Error('BrowserWebSocket cannot be used outside of a browser environment') + } + + this._ws = new window.WebSocket(url) + this._ws.onopen = () => this.emit('open') + this._ws.onmessage = this._onMessage.bind(this) + this._ws.onerror = (event) => this.emit('error', new BrowserWebSocketError('Unknown error', event)) + this._ws.onclose = (event) => this.emit('close', event.code, event.reason) + } + + get readyState() { + return this._ws.readyState + } + + close(code?: number, reason?: string) { + return this._ws.close(code, reason) + } + + removeEventListener(type: string | symbol, listener: (...args: any[]) => void): this { + return this.removeListener(type, listener) + } + + send(data: string | ArrayBufferLike | Blob | ArrayBufferView) { + return this._ws.send(data) + } + + terminate() { + return this._ws.close() + } + + async _onMessage(event: MessageEvent) { + if (event.data instanceof window.Blob) { + this.emit('message', await event.data.arrayBuffer()) + } else { + this.emit('message', event.data) + } + } +} + +BrowserWebSocket.CONNECTING = 0 +BrowserWebSocket.OPEN = 1 +BrowserWebSocket.CLOSING = 2 +BrowserWebSocket.CLOSED = 3 + +export default BrowserWebSocket diff --git a/packages/client/src/utils/Bucket.ts b/packages/client/src/utils/Bucket.ts new file mode 100644 index 000000000..6758c709f --- /dev/null +++ b/packages/client/src/utils/Bucket.ts @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +interface BucketOptions { + latencyRef?: { latency: number } + reservedTokens?: number +} + +/** + * Handle ratelimiting something + * @prop {Number} interval How long (in ms) to wait between clearing used tokens + * @prop {Number} lastReset Timestamp of last token clearing + * @prop {Number} lastSend Timestamp of last token consumption + * @prop {Number} tokenLimit The max number tokens the bucket can consume per interval + * @prop {Number} tokens How many tokens the bucket has consumed in this interval + */ +class Bucket { + interval: number + latencyRef: { latency: number } + lastReset: number + lastSend: number + tokenLimit: number + tokens: number + reservedTokens: number + _queue: Array<{ func: Function; priority: boolean }> + timeout: NodeJS.Timeout | null = null + + /** + * Construct a Bucket + * @arg {Number} tokenLimit The max number of tokens the bucket can consume per interval + * @arg {Number} interval How long (in ms) to wait between clearing used tokens + * @arg {Object} [options] Optional parameters + * @arg {Object} options.latencyRef A latency reference object + * @arg {Number} options.latencyRef.latency Interval between consuming tokens + * @arg {Number} options.reservedTokens How many tokens to reserve for priority operations + */ + constructor(tokenLimit: number, interval: number, options: BucketOptions = {} as BucketOptions) { + this.tokenLimit = tokenLimit + this.interval = interval + this.latencyRef = options.latencyRef ?? { latency: 0 } + this.lastReset = this.tokens = this.lastSend = 0 + this.reservedTokens = options.reservedTokens ?? 0 + this._queue = [] + } + + check() { + if (this.timeout ?? this._queue.length === 0) { + return + } + if (this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency < Date.now()) { + this.lastReset = Date.now() + this.tokens = Math.max(0, this.tokens - this.tokenLimit) + } + + let val + let tokensAvailable = this.tokens < this.tokenLimit + let unreservedTokensAvailable = this.tokens < this.tokenLimit - this.reservedTokens + while (this._queue.length > 0 && (unreservedTokensAvailable || (tokensAvailable && this._queue[0].priority))) { + this.tokens++ + tokensAvailable = this.tokens < this.tokenLimit + unreservedTokensAvailable = this.tokens < this.tokenLimit - this.reservedTokens + const item = this._queue.shift() + val = this.latencyRef.latency - Date.now() + this.lastSend + if (this.latencyRef.latency === 0 || val <= 0) { + item!.func() + this.lastSend = Date.now() + } else { + setTimeout(() => { + item!.func() + }, val) + this.lastSend = Date.now() + val + } + } + + if (this._queue.length > 0 && !this.timeout) { + this.timeout = setTimeout( + () => { + this.timeout = null + this.check() + }, + this.tokens < this.tokenLimit + ? this.latencyRef.latency + : Math.max(0, this.lastReset + this.interval + this.tokenLimit * this.latencyRef.latency - Date.now()), + ) + } + } + + /** + * Queue something in the Bucket + * @arg {Function} func A callback to call when a token can be consumed + * @arg {Boolean} [priority=false] Whether or not the callback should use reserved tokens + */ + queue(func: () => void, priority = false) { + if (priority) { + this._queue.unshift({ func, priority }) + } else { + this._queue.push({ func, priority }) + } + this.check() + } +} + +export default Bucket diff --git a/packages/client/tests/empty.spec.ts b/packages/client/tests/empty.spec.ts new file mode 100644 index 000000000..f0a766d36 --- /dev/null +++ b/packages/client/tests/empty.spec.ts @@ -0,0 +1 @@ +export { } diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json new file mode 100644 index 000000000..50e6ef6eb --- /dev/null +++ b/packages/client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "outDir": "./dist", + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.tsx" + ], + "exclude": [ + "node_modules", + "dist", + "test", + "tests" + ] +} \ No newline at end of file diff --git a/packages/client/tsconfig.test.json b/packages/client/tsconfig.test.json new file mode 100644 index 000000000..eeb80c521 --- /dev/null +++ b/packages/client/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "tsconfig/test.json", + "include": [ + "tests", + ], + "exclude": [ + "node_modules", + "dist", + "src" + ] +} \ No newline at end of file diff --git a/packages/discordeno/src/bot.ts b/packages/discordeno/src/bot.ts index 7d98dea3d..c6b06da25 100644 --- a/packages/discordeno/src/bot.ts +++ b/packages/discordeno/src/bot.ts @@ -58,7 +58,7 @@ import type { */ export function createBot(options: CreateBotOptions): Bot { if (!options.rest) options.rest = { token: options.token } - if (!options.gateway) + if (!options.gateway) { options.gateway = { token: options.token, events: { @@ -79,6 +79,7 @@ export function createBot(options: CreateBotOptions): Bot { }, }, } + } options.rest.token = options.token options.gateway.token = options.token diff --git a/packages/types/src/discord.ts b/packages/types/src/discord.ts index 1cdb77634..68d56fef6 100644 --- a/packages/types/src/discord.ts +++ b/packages/types/src/discord.ts @@ -47,23 +47,29 @@ import type { ActivityTypes, + AllowedMentionsTypes, ApplicationCommandOptionTypes, ApplicationCommandPermissionTypes, ApplicationCommandTypes, ApplicationFlags, + AuditLogEvents, ButtonStyles, ChannelFlags, ChannelTypes, DefaultMessageNotificationLevels, + EmbedTypes, ExplicitContentFilterLevels, GatewayEventNames, GuildFeatures, GuildNsfwLevel, IntegrationExpireBehaviors, InteractionTypes, + MessageActivityTypes, MessageComponentTypes, + MessageTypes, MfaLevels, OverwriteTypes, + PickPartial, PremiumTiers, PremiumTypes, ScheduledEventEntityType, @@ -359,124 +365,122 @@ export interface DiscordWebhookUpdate { channel_id: string } -// /** https://discord.com/developers/docs/resources/channel#allowed-mentions-object */ -// export interface DiscordAllowedMentions { -// /** An array of allowed mention types to parse from the content. */ -// parse?: AllowedMentionsTypes[] -// /** For replies, whether to mention the author of the message being replied to (default false) */ -// replied_user?: boolean +/** https://discord.com/developers/docs/resources/channel#allowed-mentions-object */ +export interface DiscordAllowedMentions { + /** An array of allowed mention types to parse from the content. */ + parse?: AllowedMentionsTypes[] + /** For replies, whether to mention the author of the message being replied to (default false) */ + replied_user?: boolean + /** Array of role_ids to mention (Max size of 100) */ + roles?: string[] + /** Array of user_ids to mention (Max size of 100) */ + users?: string[] +} -// /** Array of role_ids to mention (Max size of 100) */ -// roles?: string[] -// /** Array of user_ids to mention (Max size of 100) */ -// users?: string[] -// } +/** https://discord.com/developers/docs/resources/channel#embed-object */ +export interface DiscordEmbed { + /** Title of embed */ + title?: string + /** Type of embed (always "rich" for webhook embeds) */ + type?: EmbedTypes + /** Description of embed */ + description?: string + /** Url of embed */ + url?: string + /** Color code of the embed */ + color?: number + /** Timestamp of embed content */ + timestamp?: string + /** Footer information */ + footer?: DiscordEmbedFooter + /** Image information */ + image?: DiscordEmbedImage + /** Thumbnail information */ + thumbnail?: DiscordEmbedThumbnail + /** Video information */ + video?: DiscordEmbedVideo + /** Provider information */ + provider?: DiscordEmbedProvider + /** Author information */ + author?: DiscordEmbedAuthor + /** Fields information */ + fields?: DiscordEmbedField[] +} -// /** https://discord.com/developers/docs/resources/channel#embed-object */ -// export interface DiscordEmbed { -// /** Title of embed */ -// title?: string -// /** Type of embed (always "rich" for webhook embeds) */ -// type?: EmbedTypes -// /** Description of embed */ -// description?: string -// /** Url of embed */ -// url?: string -// /** Color code of the embed */ -// color?: number +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure */ +export interface DiscordEmbedAuthor { + /** Name of author */ + name: string + /** Url of author */ + url?: string + /** Url of author icon (only supports http(s) and attachments) */ + icon_url?: string + /** A proxied url of author icon */ + proxy_icon_url?: string +} -// /** Timestamp of embed content */ -// timestamp?: string -// /** Footer information */ -// footer?: DiscordEmbedFooter -// /** Image information */ -// image?: DiscordEmbedImage -// /** Thumbnail information */ -// thumbnail?: DiscordEmbedThumbnail -// /** Video information */ -// video?: DiscordEmbedVideo -// /** Provider information */ -// provider?: DiscordEmbedProvider -// /** Author information */ -// author?: DiscordEmbedAuthor -// /** Fields information */ -// fields?: DiscordEmbedField[] -// } +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure */ +export interface DiscordEmbedField { + /** Name of the field */ + name: string + /** Value of the field */ + value: string + /** Whether or not this field should display inline */ + inline?: boolean +} -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure */ -// export interface DiscordEmbedAuthor { -// /** Name of author */ -// name: string -// /** Url of author */ -// url?: string -// /** Url of author icon (only supports http(s) and attachments) */ -// icon_url?: string -// /** A proxied url of author icon */ -// proxy_icon_url?: string -// } +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure */ +export interface DiscordEmbedFooter { + /** Footer text */ + text: string + /** Url of footer icon (only supports http(s) and attachments) */ + icon_url?: string + /** A proxied url of footer icon */ + proxy_icon_url?: string +} -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure */ -// export interface DiscordEmbedField { -// /** Name of the field */ -// name: string -// /** Value of the field */ -// value: string -// /** Whether or not this field should display inline */ -// inline?: boolean -// } +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure */ +export interface DiscordEmbedImage { + /** Source url of image (only supports http(s) and attachments) */ + url: string + /** A proxied url of the image */ + proxy_url?: string + /** Height of image */ + height?: number + /** Width of image */ + width?: number +} -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure */ -// export interface DiscordEmbedFooter { -// /** Footer text */ -// text: string -// /** Url of footer icon (only supports http(s) and attachments) */ -// icon_url?: string -// /** A proxied url of footer icon */ -// proxy_icon_url?: string -// } +export interface DiscordEmbedProvider { + /** Name of provider */ + name?: string + /** Url of provider */ + url?: string +} -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure */ -// export interface DiscordEmbedImage { -// /** Source url of image (only supports http(s) and attachments) */ -// url: string -// /** A proxied url of the image */ -// proxy_url?: string -// /** Height of image */ -// height?: number -// /** Width of image */ -// width?: number -// } +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure */ +export interface DiscordEmbedThumbnail { + /** Source url of thumbnail (only supports http(s) and attachments) */ + url: string + /** A proxied url of the thumbnail */ + proxy_url?: string + /** Height of thumbnail */ + height?: number + /** Width of thumbnail */ + width?: number +} -// export interface DiscordEmbedProvider { -// /** Name of provider */ -// name?: string -// /** Url of provider */ -// url?: string -// } - -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure */ -// export interface DiscordEmbedThumbnail { -// /** Source url of thumbnail (only supports http(s) and attachments) */ -// url: string -// /** A proxied url of the thumbnail */ -// proxy_url?: string -// /** Height of thumbnail */ -// height?: number -// /** Width of thumbnail */ -// width?: number -// } - -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure */ -// export interface DiscordEmbedVideo { -// /** Source url of video */ -// url?: string -// /** A proxied url of the video */ -// proxy_url?: string -// /** Height of video */ -// height?: number -// /** Width of video */ -// width?: number -// } +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure */ +export interface DiscordEmbedVideo { + /** Source url of video */ + url?: string + /** A proxied url of the video */ + proxy_url?: string + /** Height of video */ + height?: number + /** Width of video */ + width?: number +} /** https://discord.com/developers/docs/resources/channel#attachment-object */ export interface DiscordAttachment { @@ -1035,135 +1039,135 @@ export interface DiscordMemberWithUser extends DiscordMember { export interface DiscordMessage { /** id of the message */ id: string - // /** id of the channel the message was sent in */ - // channel_id: string - // /** - // * id of the guild the message was sent in - // * Note: For MESSAGE_CREATE and MESSAGE_UPDATE events, the message object may not contain a guild_id or member field since the events are sent directly to the receiving user and the bot who sent the message, rather than being sent through the guild like non-ephemeral messages. - // */ - // guild_id?: string - // /** - // * The author of this message (not guaranteed to be a valid user) - // * Note: The author object follows the structure of the user object, but is only a valid user in the case where the message is generated by a user or bot user. If the message is generated by a webhook, the author object corresponds to the webhook's id, username, and avatar. You can tell if a message is generated by a webhook by checking for the webhook_id on the message object. - // */ - // author: DiscordUser - // /** - // * Member properties for this message's author - // * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory. - // */ - // member?: DiscordMember + /** id of the channel the message was sent in */ + channel_id: string + /** + * id of the guild the message was sent in + * Note: For MESSAGE_CREATE and MESSAGE_UPDATE events, the message object may not contain a guild_id or member field since the events are sent directly to the receiving user and the bot who sent the message, rather than being sent through the guild like non-ephemeral messages. + */ + guild_id?: string + /** + * The author of this message (not guaranteed to be a valid user) + * Note: The author object follows the structure of the user object, but is only a valid user in the case where the message is generated by a user or bot user. If the message is generated by a webhook, the author object corresponds to the webhook's id, username, and avatar. You can tell if a message is generated by a webhook by checking for the webhook_id on the message object. + */ + author: DiscordUser + /** + * Member properties for this message's author + * Note: The member object exists in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. This allows bots to obtain real-time member data without requiring bots to store member state in memory. + */ + member?: DiscordMember /** Contents of the message */ content?: string - // /** When this message was sent */ - // timestamp: string - // /** When this message was edited (or null if never) */ - // edited_timestamp: string | null - // /** Whether this was a TTS message */ - // tts: boolean - // /** Whether this message mentions everyone */ - // mention_everyone: boolean - // /** - // * Users specifically mentioned in the message - // * Note: The user objects in the mentions array will only have the partial member field present in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. - // */ - // mentions: Array }> - // /** Roles specifically mentioned in this message */ - // mention_roles?: string[] - // /** - // * Channels specifically mentioned in this message - // * Note: Not all channel mentions in a message will appear in `mention_channels`. Only textual channels that are visible to everyone in a lurkable guild will ever be included. Only crossposted messages (via Channel Following) currently include `mention_channels` at all. If no mentions in the message meet these requirements, this field will not be sent. - // */ - // mention_channels?: DiscordChannelMention[] - // /** Any attached files */ - // attachments: DiscordAttachment[] - // /** Any embedded content */ - // embeds: DiscordEmbed[] - // /** Reactions to the message */ - // reactions?: DiscordReaction[] - // /** Used for validating a message was sent */ - // nonce?: number | string - // /** Whether this message is pinned */ - // pinned: boolean - // /** If the message is generated by a webhook, this is the webhook's id */ - // webhook_id?: string - // /** Type of message */ - // type: MessageTypes - // /** Sent with Rich Presence-related chat embeds */ - // activity?: DiscordMessageActivity - // /** Sent with Rich Presence-related chat embeds */ - // application?: Partial - // /** if the message is an Interaction or application-owned webhook, this is the id of the application */ - // application_id?: string - // /** Data showing the source of a crossposted channel follow add, pin or reply message */ - // message_reference?: Omit - // /** Message flags combined as a bitfield */ - // flags?: number - // /** - // * The stickers sent with the message (bots currently can only receive messages with stickers, not send) - // * @deprecated - // */ - // stickers?: DiscordSticker[] - // /** - // * The message associated with the `message_reference` - // * Note: This field is only returned for messages with a `type` of `19` (REPLY). If the message is a reply but the `referenced_message` field is not present, the backend did not attempt to fetch the message that was being replied to, so its state is unknown. If the field exists but is null, the referenced message was deleted. - // */ - // referenced_message?: DiscordMessage - // /** Sent if the message is a response to an Interaction */ - // interaction?: DiscordMessageInteraction - // /** The thread that was started from this message, includes thread member object */ - // thread?: Omit & { member: DiscordThreadMember } - // /** The components related to this message */ - // components?: DiscordMessageComponents - // /** Sent if the message contains stickers */ - // sticker_items?: DiscordStickerItem[] - // /** A generally increasing integer (there may be gaps or duplicates) that represents the approximate position of the message in a thread, it can be used to estimate the relative position of the message in a thread in company with `total_message_sent` on parent thread */ - // position?: number + /** When this message was sent */ + timestamp: string + /** When this message was edited (or null if never) */ + edited_timestamp: string | null + /** Whether this was a TTS message */ + tts: boolean + /** Whether this message mentions everyone */ + mention_everyone: boolean + /** + * Users specifically mentioned in the message + * Note: The user objects in the mentions array will only have the partial member field present in `MESSAGE_CREATE` and `MESSAGE_UPDATE` events from text-based guild channels. + */ + mentions: Array }> + /** Roles specifically mentioned in this message */ + mention_roles?: string[] + /** + * Channels specifically mentioned in this message + * Note: Not all channel mentions in a message will appear in `mention_channels`. Only textual channels that are visible to everyone in a lurkable guild will ever be included. Only crossposted messages (via Channel Following) currently include `mention_channels` at all. If no mentions in the message meet these requirements, this field will not be sent. + */ + mention_channels?: DiscordChannelMention[] + /** Any attached files */ + attachments: DiscordAttachment[] + /** Any embedded content */ + embeds: DiscordEmbed[] + /** Reactions to the message */ + reactions?: DiscordReaction[] + /** Used for validating a message was sent */ + nonce?: number | string + /** Whether this message is pinned */ + pinned: boolean + /** If the message is generated by a webhook, this is the webhook's id */ + webhook_id?: string + /** Type of message */ + type: MessageTypes + /** Sent with Rich Presence-related chat embeds */ + activity?: DiscordMessageActivity + /** Sent with Rich Presence-related chat embeds */ + application?: Partial + /** if the message is an Interaction or application-owned webhook, this is the id of the application */ + application_id?: string + /** Data showing the source of a crossposted channel follow add, pin or reply message */ + message_reference?: Omit + /** Message flags combined as a bitfield */ + flags?: number + /** + * The stickers sent with the message (bots currently can only receive messages with stickers, not send) + * @deprecated + */ + stickers?: DiscordSticker[] + /** + * The message associated with the `message_reference` + * Note: This field is only returned for messages with a `type` of `19` (REPLY). If the message is a reply but the `referenced_message` field is not present, the backend did not attempt to fetch the message that was being replied to, so its state is unknown. If the field exists but is null, the referenced message was deleted. + */ + referenced_message?: DiscordMessage + /** Sent if the message is a response to an Interaction */ + interaction?: DiscordMessageInteraction + /** The thread that was started from this message, includes thread member object */ + thread?: Omit & { member: DiscordThreadMember } + /** The components related to this message */ + components?: DiscordMessageComponents + /** Sent if the message contains stickers */ + sticker_items?: DiscordStickerItem[] + /** A generally increasing integer (there may be gaps or duplicates) that represents the approximate position of the message in a thread, it can be used to estimate the relative position of the message in a thread in company with `total_message_sent` on parent thread */ + position?: number } -// /** https://discord.com/developers/docs/resources/channel#channel-mention-object */ -// export interface DiscordChannelMention { -// /** id of the channel */ -// id: string -// /** id of the guild containing the channel */ -// guild_id: string -// /** The type of channel */ -// type: number -// /** The name of the channel */ -// name: string -// } +/** https://discord.com/developers/docs/resources/channel#channel-mention-object */ +export interface DiscordChannelMention { + /** id of the channel */ + id: string + /** id of the guild containing the channel */ + guild_id: string + /** The type of channel */ + type: number + /** The name of the channel */ + name: string +} -// /** https://discord.com/developers/docs/resources/channel#reaction-object */ -// export interface DiscordReaction { -// /** Times this emoji has been used to react */ -// count: number -// /** Whether the current user reacted using this emoji */ -// me: boolean -// /** Emoji information */ -// emoji: Partial -// } +/** https://discord.com/developers/docs/resources/channel#reaction-object */ +export interface DiscordReaction { + /** Times this emoji has been used to react */ + count: number + /** Whether the current user reacted using this emoji */ + me: boolean + /** Emoji information */ + emoji: Partial +} -// /** https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure */ -// export interface DiscordMessageActivity { -// /** Type of message activity */ -// type: MessageActivityTypes -// /** `party_id` from a Rich Presence event */ -// party_id?: string -// } +/** https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure */ +export interface DiscordMessageActivity { + /** Type of message activity */ + type: MessageActivityTypes + /** `party_id` from a Rich Presence event */ + party_id?: string +} -// /** https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure */ -// export interface DiscordMessageReference { -// /** id of the originating message */ -// message_id?: string -// /** -// * id of the originating message's channel -// * Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. -// */ -// channel_id?: string -// /** id of the originating message's guild */ -// guild_id?: string -// /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ -// fail_if_not_exists: boolean -// } +/** https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure */ +export interface DiscordMessageReference { + /** id of the originating message */ + message_id?: string + /** + * id of the originating message's channel + * Note: `channel_id` is optional when creating a reply, but will always be present when receiving an event/response that includes this data model. + */ + channel_id?: string + /** id of the originating message's guild */ + guild_id?: string + /** When sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ + fail_if_not_exists: boolean +} /** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-structure */ export interface DiscordSticker { @@ -1191,19 +1195,19 @@ export interface DiscordSticker { sort_value?: number } -// /** https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure */ -// export interface DiscordMessageInteraction { -// /** Id of the interaction */ -// id: string -// /** The type of interaction */ -// type: InteractionTypes -// /** The name of the ApplicationCommand including the name of the subcommand/subcommand group */ -// name: string -// /** The user who invoked the interaction */ -// user: DiscordUser -// /** The member who invoked the interaction in the guild */ -// member?: Partial -// } +/** https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure */ +export interface DiscordMessageInteraction { + /** Id of the interaction */ + id: string + /** The type of interaction */ + type: InteractionTypes + /** The name of the ApplicationCommand including the name of the subcommand/subcommand group */ + name: string + /** The user who invoked the interaction */ + user: DiscordUser + /** The member who invoked the interaction in the guild */ + member?: Partial +} export type DiscordMessageComponents = DiscordActionRow[] @@ -1213,26 +1217,11 @@ export interface DiscordActionRow { type: 1 /** The components in this row */ components: - | [ - | DiscordSelectMenuComponent - | DiscordButtonComponent - | DiscordInputTextComponent - ] - | [DiscordButtonComponent, DiscordButtonComponent] - | [DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent] - | [ - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent - ] - | [ - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent, - DiscordButtonComponent - ] + | [DiscordSelectMenuComponent | DiscordButtonComponent | DiscordInputTextComponent] + | [DiscordButtonComponent, DiscordButtonComponent] + | [DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent] + | [DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent] + | [DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent, DiscordButtonComponent] } export interface DiscordSelectMenuComponent { @@ -1316,15 +1305,15 @@ export interface DiscordInputTextComponent { value?: string } -// /** https://discord.com/developers/docs/resources/sticker#sticker-item-object-sticker-item-structure */ -// export interface DiscordStickerItem { -// /** Id of the sticker */ -// id: string -// /** Name of the sticker */ -// name: string -// /** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */ -// format_type: StickerFormatTypes -// } +/** https://discord.com/developers/docs/resources/sticker#sticker-item-object-sticker-item-structure */ +export interface DiscordStickerItem { + /** Id of the sticker */ + id: string + /** Name of the sticker */ + name: string + /** [Type of sticker format](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) */ + format_type: StickerFormatTypes +} // /** https://discord.com/developers/docs/resources/sticker#sticker-pack-object-sticker-pack-structure */ // export interface DiscordStickerPack { @@ -1403,17 +1392,11 @@ export interface DiscordInteractionData { /** The Ids and User objects */ users?: Record /** The Ids and partial Member objects */ - members?: Record< - string, - Omit - > + members?: Record> /** The Ids and Role objects */ roles?: Record /** The Ids and partial Channel objects */ - channels?: Record< - string, - Pick - > + channels?: Record> /** The ids and attachment objects */ attachments: Record } @@ -1431,13 +1414,7 @@ export interface DiscordInteractionDataOption { /** Value of application command option type */ type: ApplicationCommandOptionTypes /** Value of the option resulting from user input */ - value?: - | string - | boolean - | number - | DiscordMember - | DiscordChannel - | DiscordRole + value?: string | boolean | number | DiscordMember | DiscordChannel | DiscordRole /** Present if this option is a group or subcommand */ options?: DiscordInteractionDataOption[] /** `true` if this option is the currently focused option for autocomplete */ @@ -1620,188 +1597,188 @@ export interface DiscordAutoModerationActionExecution { matched_content: string | null } -// /** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-entry-structure */ -// export interface DiscordAuditLogEntry { -// /** ID of the affected entity (webhook, user, role, etc.) */ -// target_id: string | null -// /** Changes made to the `target_id` */ -// changes?: DiscordAuditLogChange[] -// /** User or app that made the changes */ -// user_id: string | null -// /** ID of the entry */ -// id: string -// /** Type of action that occurred */ -// action_type: AuditLogEvents -// /** Additional info for certain event types */ -// options?: DiscordOptionalAuditEntryInfo -// /** Reason for the change (1-512 characters) */ -// reason?: string -// } +/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-entry-structure */ +export interface DiscordAuditLogEntry { + /** ID of the affected entity (webhook, user, role, etc.) */ + target_id: string | null + /** Changes made to the `target_id` */ + changes?: DiscordAuditLogChange[] + /** User or app that made the changes */ + user_id: string | null + /** ID of the entry */ + id: string + /** Type of action that occurred */ + action_type: AuditLogEvents + /** Additional info for certain event types */ + options?: DiscordOptionalAuditEntryInfo + /** Reason for the change (1-512 characters) */ + reason?: string +} -// /** https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-structure */ -// export type DiscordAuditLogChange = -// | { -// new_value: string -// old_value: string -// key: -// | 'name' -// | 'description' -// | 'discovery_splash_hash' -// | 'banner_hash' -// | 'preferred_locale' -// | 'rules_channel_id' -// | 'public_updates_channel_id' -// | 'icon_hash' -// | 'image_hash' -// | 'splash_hash' -// | 'owner_id' -// | 'region' -// | 'afk_channel_id' -// | 'vanity_url_code' -// | 'widget_channel_id' -// | 'system_channel_id' -// | 'topic' -// | 'application_id' -// | 'permissions' -// | 'allow' -// | 'deny' -// | 'code' -// | 'channel_id' -// | 'inviter_id' -// | 'nick' -// | 'avatar_hash' -// | 'id' -// | 'location' -// | 'command_id' -// } -// | { -// new_value: number -// old_value: number -// key: -// | 'afk_timeout' -// | 'mfa_level' -// | 'verification_level' -// | 'explicit_content_filter' -// | 'default_message_notifications' -// | 'prune_delete_days' -// | 'position' -// | 'bitrate' -// | 'rate_limit_per_user' -// | 'color' -// | 'max_uses' -// | 'uses' -// | 'max_age' -// | 'expire_behavior' -// | 'expire_grace_period' -// | 'user_limit' -// | 'privacy_level' -// | 'auto_archive_duration' -// | 'default_auto_archive_duration' -// | 'entity_type' -// | 'status' -// | 'communication_disabled_until' -// } -// | { -// new_value: Array> -// old_value?: Array> -// key: '$add' | '$remove' -// } -// | { -// new_value: boolean -// old_value: boolean -// key: -// | 'widget_enabled' -// | 'nsfw' -// | 'hoist' -// | 'mentionable' -// | 'temporary' -// | 'deaf' -// | 'mute' -// | 'enable_emoticons' -// | 'archived' -// | 'locked' -// | 'invitable' -// } -// | { -// new_value: DiscordOverwrite[] -// old_value: DiscordOverwrite[] -// key: 'permission_overwrites' -// } -// | { -// new_value: string | number -// old_value: string | number -// key: 'type' -// } +/** https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-structure */ +export type DiscordAuditLogChange = + | { + new_value: string + old_value: string + key: + | 'name' + | 'description' + | 'discovery_splash_hash' + | 'banner_hash' + | 'preferred_locale' + | 'rules_channel_id' + | 'public_updates_channel_id' + | 'icon_hash' + | 'image_hash' + | 'splash_hash' + | 'owner_id' + | 'region' + | 'afk_channel_id' + | 'vanity_url_code' + | 'widget_channel_id' + | 'system_channel_id' + | 'topic' + | 'application_id' + | 'permissions' + | 'allow' + | 'deny' + | 'code' + | 'channel_id' + | 'inviter_id' + | 'nick' + | 'avatar_hash' + | 'id' + | 'location' + | 'command_id' + } + | { + new_value: number + old_value: number + key: + | 'afk_timeout' + | 'mfa_level' + | 'verification_level' + | 'explicit_content_filter' + | 'default_message_notifications' + | 'prune_delete_days' + | 'position' + | 'bitrate' + | 'rate_limit_per_user' + | 'color' + | 'max_uses' + | 'uses' + | 'max_age' + | 'expire_behavior' + | 'expire_grace_period' + | 'user_limit' + | 'privacy_level' + | 'auto_archive_duration' + | 'default_auto_archive_duration' + | 'entity_type' + | 'status' + | 'communication_disabled_until' + } + | { + new_value: Array> + old_value?: Array> + key: '$add' | '$remove' + } + | { + new_value: boolean + old_value: boolean + key: + | 'widget_enabled' + | 'nsfw' + | 'hoist' + | 'mentionable' + | 'temporary' + | 'deaf' + | 'mute' + | 'enable_emoticons' + | 'archived' + | 'locked' + | 'invitable' + } + | { + new_value: DiscordOverwrite[] + old_value: DiscordOverwrite[] + key: 'permission_overwrites' + } + | { + new_value: string | number + old_value: string | number + key: 'type' + } -// /** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info */ -// export interface DiscordOptionalAuditEntryInfo { -// /** -// * ID of the app whose permissions were targeted. -// * -// * Event types: `APPLICATION_COMMAND_PERMISSION_UPDATE` -// */ -// application_id: string -// /** -// * Name of the Auto Moderation rule that was triggered. -// * -// * Event types: `AUTO_MODERATION_BLOCK_MESSAGE`, `AUTO_MODERATION_FLAG_TO_CHANNEL`, `AUTO_MODERATION_USER_COMMUNICATION_DISABLED` -// */ -// auto_moderation_rule_name: string -// /** -// * Trigger type of the Auto Moderation rule that was triggered. -// * -// * Event types: `AUTO_MODERATION_BLOCK_MESSAGE`, `AUTO_MODERATION_FLAG_TO_CHANNEL`, `AUTO_MODERATION_USER_COMMUNICATION_DISABLED` -// */ -// auto_moderation_rule_trigger_type: string -// /** -// * Channel in which the entities were targeted. -// * -// * Event types: `MEMBER_MOVE`, `MESSAGE_PIN`, `MESSAGE_UNPIN`, `MESSAGE_DELETE`, `STAGE_INSTANCE_CREATE`, `STAGE_INSTANCE_UPDATE`, `STAGE_INSTANCE_DELETE` -// */ -// channel_id: string -// /** -// * Number of entities that were targeted. -// * -// * Event types: `MESSAGE_DELETE`, `MESSAGE_BULK_DELETE`, `MEMBER_DISCONNECT`, `MEMBER_MOVE` -// */ -// count: string -// /** -// * Number of days after which inactive members were kicked. -// * -// * Event types: `MEMBER_PRUNE` -// */ -// delete_member_days: string -// /** -// * ID of the overwritten entity. -// * -// * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` -// */ -// id: string -// /** -// * Number of members removed by the prune. -// * -// * Event types: `MEMBER_PRUNE` -// */ -// members_removed: string -// /** -// * ID of the message that was targeted. -// * -// * Event types: `MESSAGE_PIN`, `MESSAGE_UNPIN`, `STAGE_INSTANCE_CREATE`, `STAGE_INSTANCE_UPDATE`, `STAGE_INSTANCE_DELETE` -// */ -// message_id: string -// /** -// * Name of the role if type is "0" (not present if type is "1"). -// * -// * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` -// */ -// role_name: string -// /** -// * Type of overwritten entity - "0", for "role", or "1" for "member". -// * -// * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` -// */ -// type: string -// } +/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info */ +export interface DiscordOptionalAuditEntryInfo { + /** + * ID of the app whose permissions were targeted. + * + * Event types: `APPLICATION_COMMAND_PERMISSION_UPDATE` + */ + application_id: string + /** + * Name of the Auto Moderation rule that was triggered. + * + * Event types: `AUTO_MODERATION_BLOCK_MESSAGE`, `AUTO_MODERATION_FLAG_TO_CHANNEL`, `AUTO_MODERATION_USER_COMMUNICATION_DISABLED` + */ + auto_moderation_rule_name: string + /** + * Trigger type of the Auto Moderation rule that was triggered. + * + * Event types: `AUTO_MODERATION_BLOCK_MESSAGE`, `AUTO_MODERATION_FLAG_TO_CHANNEL`, `AUTO_MODERATION_USER_COMMUNICATION_DISABLED` + */ + auto_moderation_rule_trigger_type: string + /** + * Channel in which the entities were targeted. + * + * Event types: `MEMBER_MOVE`, `MESSAGE_PIN`, `MESSAGE_UNPIN`, `MESSAGE_DELETE`, `STAGE_INSTANCE_CREATE`, `STAGE_INSTANCE_UPDATE`, `STAGE_INSTANCE_DELETE` + */ + channel_id: string + /** + * Number of entities that were targeted. + * + * Event types: `MESSAGE_DELETE`, `MESSAGE_BULK_DELETE`, `MEMBER_DISCONNECT`, `MEMBER_MOVE` + */ + count: string + /** + * Number of days after which inactive members were kicked. + * + * Event types: `MEMBER_PRUNE` + */ + delete_member_days: string + /** + * ID of the overwritten entity. + * + * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` + */ + id: string + /** + * Number of members removed by the prune. + * + * Event types: `MEMBER_PRUNE` + */ + members_removed: string + /** + * ID of the message that was targeted. + * + * Event types: `MESSAGE_PIN`, `MESSAGE_UNPIN`, `STAGE_INSTANCE_CREATE`, `STAGE_INSTANCE_UPDATE`, `STAGE_INSTANCE_DELETE` + */ + message_id: string + /** + * Name of the role if type is "0" (not present if type is "1"). + * + * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` + */ + role_name: string + /** + * Type of overwritten entity - "0", for "role", or "1" for "member". + * + * Event types: `CHANNEL_OVERWRITE_CREATE`, `CHANNEL_OVERWRITE_UPDATE`, `CHANNEL_OVERWRITE_DELETE` + */ + type: string +} export interface DiscordScheduledEvent { /** the id of the scheduled event */ @@ -1879,44 +1856,44 @@ export interface DiscordSessionStartLimit { // created_at: string // } -// /** https://discord.com/developers/docs/resources/invite#invite-object */ -// export interface DiscordInvite { -// /** The invite code (unique Id) */ -// code: string -// /** The guild this invite is for */ -// guild?: Partial -// /** The channel this invite is for */ -// channel: Partial | null -// /** The user who created the invite */ -// inviter?: DiscordUser -// /** The type of target for this voice channel invite */ -// target_type?: TargetTypes -// /** The target user for this invite */ -// target_user?: DiscordUser -// /** The embedded application to open for this voice channel embedded application invite */ -// target_application?: Partial -// /** Approximate count of online members (only present when target_user is set) */ -// approximate_presence_count?: number -// /** Approximate count of total members */ -// approximate_member_count?: number -// /** The expiration date of this invite, returned from the `GET /invites/` endpoint when `with_expiration` is `true` */ -// expires_at?: string | null -// /** Stage instance data if there is a public Stage instance in the Stage channel this invite is for */ -// stage_instance?: DiscordInviteStageInstance -// /** guild scheduled event data */ -// guild_scheduled_event?: DiscordScheduledEvent -// } +/** https://discord.com/developers/docs/resources/invite#invite-object */ +export interface DiscordInvite { + /** The invite code (unique Id) */ + code: string + /** The guild this invite is for */ + guild?: Partial + /** The channel this invite is for */ + channel: Partial | null + /** The user who created the invite */ + inviter?: DiscordUser + /** The type of target for this voice channel invite */ + target_type?: TargetTypes + /** The target user for this invite */ + target_user?: DiscordUser + /** The embedded application to open for this voice channel embedded application invite */ + target_application?: Partial + /** Approximate count of online members (only present when target_user is set) */ + approximate_presence_count?: number + /** Approximate count of total members */ + approximate_member_count?: number + /** The expiration date of this invite, returned from the `GET /invites/` endpoint when `with_expiration` is `true` */ + expires_at?: string | null + /** Stage instance data if there is a public Stage instance in the Stage channel this invite is for */ + stage_instance?: DiscordInviteStageInstance + /** guild scheduled event data */ + guild_scheduled_event?: DiscordScheduledEvent +} -// export interface DiscordInviteStageInstance { -// /** The members speaking in the Stage */ -// members: Array> -// /** The number of users in the Stage */ -// participant_count: number -// /** The number of users speaking in the Stage */ -// speaker_count: number -// /** The topic of the Stage instance (1-120 characters) */ -// topic: string -// } +export interface DiscordInviteStageInstance { + /** The members speaking in the Stage */ + members: Array> + /** The number of users in the Stage */ + participant_count: number + /** The number of users speaking in the Stage */ + speaker_count: number + /** The topic of the Stage instance (1-120 characters) */ + topic: string +} // /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure */ // export interface DiscordApplicationCommand @@ -2053,31 +2030,31 @@ export interface DiscordApplicationCommandPermissions { // presence_count: number // } -// /** https://discord.com/developers/docs/resources/guild#guild-preview-object */ -// export interface DiscordGuildPreview { -// /** Guild id */ -// id: string -// /** Guild name (2-100 characters) */ -// name: string -// /** Icon hash */ -// icon: string | null -// /** Splash hash */ -// splash: string | null -// /** Discovery splash hash */ -// discovery_splash: string | null -// /** Custom guild emojis */ -// emojis: DiscordEmoji[] -// /** Enabled guild features */ -// features: GuildFeatures[] -// /** Approximate number of members in this guild */ -// approximate_member_count: number -// /** Approximate number of online members in this guild */ -// approximate_presence_count: number -// /** The description for the guild, if the guild is discoverable */ -// description: string | null -// /** Custom guild stickers */ -// stickers: DiscordSticker[] -// } +/** https://discord.com/developers/docs/resources/guild#guild-preview-object */ +export interface DiscordGuildPreview { + /** Guild id */ + id: string + /** Guild name (2-100 characters) */ + name: string + /** Icon hash */ + icon: string | null + /** Splash hash */ + splash: string | null + /** Discovery splash hash */ + discovery_splash: string | null + /** Custom guild emojis */ + emojis: DiscordEmoji[] + /** Enabled guild features */ + features: GuildFeatures[] + /** Approximate number of members in this guild */ + approximate_member_count: number + /** Approximate number of online members in this guild */ + approximate_presence_count: number + /** The description for the guild, if the guild is discoverable */ + description: string | null + /** Custom guild stickers */ + stickers: DiscordSticker[] +} // export interface DiscordDiscoveryCategory { // /** Numeric id of the category */ @@ -2219,8 +2196,7 @@ export interface DiscordGuildBanAddRemove { } /** https://discord.com/developers/docs/topics/gateway#message-reaction-remove */ -export interface DiscordMessageReactionRemove - extends Omit {} +export interface DiscordMessageReactionRemove extends Omit {} /** https://discord.com/developers/docs/topics/gateway#message-reaction-add */ export interface DiscordMessageReactionAdd { @@ -2313,63 +2289,51 @@ export interface DiscordMessageDeleteBulk { guild_id?: string } -// /** https://discord.com/developers/docs/resources/template#template-object-template-structure */ -// export interface DiscordTemplate { -// /** The template code (unique Id) */ -// code: string -// /** Template name */ -// name: string -// /** The description for the template */ -// description: string | null -// /** Number of times this template has been used */ -// usage_count: number -// /** The Id of the user who created the template */ -// creator_id: string -// /** The user who created the template */ -// creator: DiscordUser -// /** When this template was created */ -// created_at: string -// /** When this template was last synced to the source guild */ -// updated_at: string -// /** The Id of the guild this template is based on */ -// source_guild_id: string -// /** The guild snapshot this template contains */ -// serialized_source_guild: Omit< -// PickPartial< -// DiscordGuild, -// | 'name' -// | 'description' -// | 'verification_level' -// | 'default_message_notifications' -// | 'explicit_content_filter' -// | 'preferred_locale' -// | 'afk_timeout' -// | 'channels' -// | 'afk_channel_id' -// | 'system_channel_id' -// | 'system_channel_flags' -// >, -// 'roles' -// > & { -// roles: Array< -// Omit< -// PickPartial< -// DiscordRole, -// | 'name' -// | 'color' -// | 'hoist' -// | 'mentionable' -// | 'permissions' -// | 'icon' -// | 'unicode_emoji' -// >, -// 'id' -// > & { id: number } -// > -// } -// /** Whether the template has un-synced changes */ -// is_dirty: boolean | null -// } +/** https://discord.com/developers/docs/resources/template#template-object-template-structure */ +export interface DiscordTemplate { + /** The template code (unique Id) */ + code: string + /** Template name */ + name: string + /** The description for the template */ + description: string | null + /** Number of times this template has been used */ + usage_count: number + /** The Id of the user who created the template */ + creator_id: string + /** The user who created the template */ + creator: DiscordUser + /** When this template was created */ + created_at: string + /** When this template was last synced to the source guild */ + updated_at: string + /** The Id of the guild this template is based on */ + source_guild_id: string + /** The guild snapshot this template contains */ + serialized_source_guild: Omit< + PickPartial< + DiscordGuild, + | 'name' + | 'description' + | 'verification_level' + | 'default_message_notifications' + | 'explicit_content_filter' + | 'preferred_locale' + | 'afk_timeout' + | 'channels' + | 'afk_channel_id' + | 'system_channel_id' + | 'system_channel_flags' + >, + 'roles' + > & { + roles: Array< + Omit, 'id'> & { id: number } + > + } + /** Whether the template has un-synced changes */ + is_dirty: boolean | null +} /** https://discord.com/developers/docs/topics/gateway#guild-member-add */ export interface DiscordGuildMemberAdd extends DiscordMemberWithUser { @@ -2471,11 +2435,7 @@ export interface DiscordGuildMemberUpdate { } /** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all */ -export interface DiscordMessageReactionRemoveAll - extends Pick< - DiscordMessageReactionAdd, - 'channel_id' | 'message_id' | 'guild_id' - > {} +export interface DiscordMessageReactionRemoveAll extends Pick {} // // TODO: add docs link // export interface DiscordValidateDiscoverySearchTerm { @@ -2501,10 +2461,7 @@ export interface DiscordScheduledEventUserAdd { } /** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji */ -export type DiscordMessageReactionRemoveEmoji = Pick< -DiscordMessageReactionAdd, -'channel_id' | 'guild_id' | 'message_id' | 'emoji' -> +export type DiscordMessageReactionRemoveEmoji = Pick /** https://discord.com/developers/docs/topics/gateway#guild-member-remove */ export interface DiscordGuildMemberRemove { diff --git a/packages/types/src/discordeno.ts b/packages/types/src/discordeno.ts index 475b77297..a7e3112c8 100644 --- a/packages/types/src/discordeno.ts +++ b/packages/types/src/discordeno.ts @@ -500,3 +500,19 @@ export interface ModifyGuildEmoji extends WithReason { /** Roles allowed to use this emoji */ roles?: BigString[] | null } + +/** https://discord.com/developers/docs/topics/gateway#request-guild-members */ +export interface RequestGuildMembers { + /** id of the guild to get members for */ + guildId: BigString + /** String that username starts with, or an empty string to return all members */ + query?: string + /** Maximum number of members to send matching the query; a limit of 0 can be used with an empty string query to return all members */ + limit: number + /** Used to specify if we want the presences of the matched members */ + presences?: boolean + /** Used to specify which users you wish to fetch */ + userIds?: BigString[] + /** Nonce to identify the Guild Members Chunk response */ + nonce?: string +} diff --git a/packages/types/src/shared.ts b/packages/types/src/shared.ts index c2cc0ee24..3cc788e7b 100644 --- a/packages/types/src/shared.ts +++ b/packages/types/src/shared.ts @@ -133,34 +133,28 @@ export enum ButtonStyles { Link, } -// /** https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types */ -// export enum AllowedMentionsTypes { -// /** Controls role mentions */ -// RoleMentions = 'roles', -// /** Controls user mentions */ -// UserMentions = 'users', -// /** Controls @everyone and @here mentions */ -// EveryoneMentions = 'everyone', -// } +/** https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types */ +export enum AllowedMentionsTypes { + /** Controls role mentions */ + RoleMentions = 'roles', + /** Controls user mentions */ + UserMentions = 'users', + /** Controls @everyone and @here mentions */ + EveryoneMentions = 'everyone', +} -// /** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types */ -// export enum WebhookTypes { -// /** Incoming Webhooks can post messages to channels with a generated token */ -// Incoming = 1, -// /** Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels */ -// ChannelFollower, -// /** Application webhooks are webhooks used with Interactions */ -// Application, -// } +/** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types */ +export enum WebhookTypes { + /** Incoming Webhooks can post messages to channels with a generated token */ + Incoming = 1, + /** Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels */ + ChannelFollower, + /** Application webhooks are webhooks used with Interactions */ + Application, +} -// /** https://discord.com/developers/docs/resources/channel#embed-object-embed-types */ -// export type EmbedTypes = -// | 'rich' -// | 'image' -// | 'video' -// | 'gifv' -// | 'article' -// | 'link' +/** https://discord.com/developers/docs/resources/channel#embed-object-embed-types */ +export type EmbedTypes = 'rich' | 'image' | 'video' | 'gifv' | 'article' | 'link' /** https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level */ export enum DefaultMessageNotificationLevels { @@ -374,13 +368,13 @@ export enum MessageTypes { AutoModerationAction, } -// /** https://discord.com/developers/docs/resources/channel#message-object-message-activity-types */ -// export enum MessageActivityTypes { -// Join = 1, -// Spectate, -// Listen, -// JoinRequest, -// } +/** https://discord.com/developers/docs/resources/channel#message-object-message-activity-types */ +export enum MessageActivityTypes { + Join = 1, + Spectate, + Listen, + JoinRequest, +} /** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types */ export enum StickerTypes { @@ -421,113 +415,113 @@ export enum ApplicationCommandOptionTypes { Attachment, } -// /** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events */ -// export enum AuditLogEvents { -// /** Server settings were updated */ -// GuildUpdate = 1, -// /** Channel was created */ -// ChannelCreate = 10, -// /** Channel settings were updated */ -// ChannelUpdate, -// /** Channel was deleted */ -// ChannelDelete, -// /** Permission overwrite was added to a channel */ -// ChannelOverwriteCreate, -// /** Permission overwrite was updated for a channel */ -// ChannelOverwriteUpdate, -// /** Permission overwrite was deleted from a channel */ -// ChannelOverwriteDelete, -// /** Member was removed from server */ -// MemberKick = 20, -// /** Members were pruned from server */ -// MemberPrune, -// /** Member was banned from server */ -// MemberBanAdd, -// /** Server ban was lifted for a member */ -// MemberBanRemove, -// /** Member was updated in server */ -// MemberUpdate, -// /** Member was added or removed from a role */ -// MemberRoleUpdate, -// /** Member was moved to a different voice channel */ -// MemberMove, -// /** Member was disconnected from a voice channel */ -// MemberDisconnect, -// /** Bot user was added to server */ -// BotAdd, -// /** Role was created */ -// RoleCreate = 30, -// /** Role was edited */ -// RoleUpdate, -// /** Role was deleted */ -// RoleDelete, -// /** Server invite was created */ -// InviteCreate = 40, -// /** Server invite was updated */ -// InviteUpdate, -// /** Server invite was deleted */ -// InviteDelete, -// /** Webhook was created */ -// WebhookCreate = 50, -// /** Webhook properties or channel were updated */ -// WebhookUpdate, -// /** Webhook was deleted */ -// WebhookDelete, -// /** Emoji was created */ -// EmojiCreate = 60, -// /** Emoji name was updated */ -// EmojiUpdate, -// /** Emoji was deleted */ -// EmojiDelete, -// /** Single message was deleted */ -// MessageDelete = 72, -// /** Multiple messages were deleted */ -// MessageBulkDelete, -// /** Messaged was pinned to a channel */ -// MessagePin, -// /** Message was unpinned from a channel */ -// MessageUnpin, -// /** App was added to server */ -// IntegrationCreate = 80, -// /** App was updated (as an example, its scopes were updated) */ -// IntegrationUpdate, -// /** App was removed from server */ -// IntegrationDelete, -// /** Stage instance was created (stage channel becomes live) */ -// StageInstanceCreate, -// /** Stage instace details were updated */ -// StageInstanceUpdate, -// /** Stage instance was deleted (stage channel no longer live) */ -// StageInstanceDelete, -// /** Sticker was created */ -// StickerCreate = 90, -// /** Sticker details were updated */ -// StickerUpdate, -// /** Sticker was deleted */ -// StickerDelete, -// /** Event was created */ -// GuildScheduledEventCreate = 100, -// /** Event was updated */ -// GuildScheduledEventUpdate, -// /** Event was cancelled */ -// GuildScheduledEventDelete, -// /** Thread was created in a channel */ -// ThreadCreate = 110, -// /** Thread was updated */ -// ThreadUpdate, -// /** Thread was deleted */ -// ThreadDelete, -// /** Permissions were updated for a command */ -// ApplicationCommandPermissionUpdate = 121, -// /** Auto moderation rule was created */ -// AutoModerationRuleCreate = 140, -// /** Auto moderation rule was updated */ -// AutoModerationRuleUpdate, -// /** Auto moderation rule was deleted */ -// AutoModerationRuleDelete, -// /** Message was blocked by AutoMod according to a rule. */ -// AutoModerationBlockMessage, -// } +/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events */ +export enum AuditLogEvents { + /** Server settings were updated */ + GuildUpdate = 1, + /** Channel was created */ + ChannelCreate = 10, + /** Channel settings were updated */ + ChannelUpdate, + /** Channel was deleted */ + ChannelDelete, + /** Permission overwrite was added to a channel */ + ChannelOverwriteCreate, + /** Permission overwrite was updated for a channel */ + ChannelOverwriteUpdate, + /** Permission overwrite was deleted from a channel */ + ChannelOverwriteDelete, + /** Member was removed from server */ + MemberKick = 20, + /** Members were pruned from server */ + MemberPrune, + /** Member was banned from server */ + MemberBanAdd, + /** Server ban was lifted for a member */ + MemberBanRemove, + /** Member was updated in server */ + MemberUpdate, + /** Member was added or removed from a role */ + MemberRoleUpdate, + /** Member was moved to a different voice channel */ + MemberMove, + /** Member was disconnected from a voice channel */ + MemberDisconnect, + /** Bot user was added to server */ + BotAdd, + /** Role was created */ + RoleCreate = 30, + /** Role was edited */ + RoleUpdate, + /** Role was deleted */ + RoleDelete, + /** Server invite was created */ + InviteCreate = 40, + /** Server invite was updated */ + InviteUpdate, + /** Server invite was deleted */ + InviteDelete, + /** Webhook was created */ + WebhookCreate = 50, + /** Webhook properties or channel were updated */ + WebhookUpdate, + /** Webhook was deleted */ + WebhookDelete, + /** Emoji was created */ + EmojiCreate = 60, + /** Emoji name was updated */ + EmojiUpdate, + /** Emoji was deleted */ + EmojiDelete, + /** Single message was deleted */ + MessageDelete = 72, + /** Multiple messages were deleted */ + MessageBulkDelete, + /** Messaged was pinned to a channel */ + MessagePin, + /** Message was unpinned from a channel */ + MessageUnpin, + /** App was added to server */ + IntegrationCreate = 80, + /** App was updated (as an example, its scopes were updated) */ + IntegrationUpdate, + /** App was removed from server */ + IntegrationDelete, + /** Stage instance was created (stage channel becomes live) */ + StageInstanceCreate, + /** Stage instace details were updated */ + StageInstanceUpdate, + /** Stage instance was deleted (stage channel no longer live) */ + StageInstanceDelete, + /** Sticker was created */ + StickerCreate = 90, + /** Sticker details were updated */ + StickerUpdate, + /** Sticker was deleted */ + StickerDelete, + /** Event was created */ + GuildScheduledEventCreate = 100, + /** Event was updated */ + GuildScheduledEventUpdate, + /** Event was cancelled */ + GuildScheduledEventDelete, + /** Thread was created in a channel */ + ThreadCreate = 110, + /** Thread was updated */ + ThreadUpdate, + /** Thread was deleted */ + ThreadDelete, + /** Permissions were updated for a command */ + ApplicationCommandPermissionUpdate = 121, + /** Auto moderation rule was created */ + AutoModerationRuleCreate = 140, + /** Auto moderation rule was updated */ + AutoModerationRuleUpdate, + /** Auto moderation rule was deleted */ + AutoModerationRuleDelete, + /** Message was blocked by AutoMod according to a rule. */ + AutoModerationBlockMessage, +} export enum ScheduledEventPrivacyLevel { /** the scheduled event is public and available in discovery. DISCORD DEVS DISABLED THIS! WILL ERROR IF USED! */ @@ -583,91 +577,91 @@ export enum ApplicationCommandPermissionTypes { // Embedded = 1 << 8, // } -// /** https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags */ -// export enum BitwisePermissionFlags { -// /** Allows creation of instant invites */ -// CREATE_INSTANT_INVITE = 0x0000000000000001, -// /** Allows kicking members */ -// KICK_MEMBERS = 0x0000000000000002, -// /** Allows banning members */ -// BAN_MEMBERS = 0x0000000000000004, -// /** Allows all permissions and bypasses channel permission overwrites */ -// ADMINISTRATOR = 0x0000000000000008, -// /** Allows management and editing of channels */ -// MANAGE_CHANNELS = 0x0000000000000010, -// /** Allows management and editing of the guild */ -// MANAGE_GUILD = 0x0000000000000020, -// /** Allows for the addition of reactions to messages */ -// ADD_REACTIONS = 0x0000000000000040, -// /** Allows for viewing of audit logs */ -// VIEW_AUDIT_LOG = 0x0000000000000080, -// /** Allows for using priority speaker in a voice channel */ -// PRIORITY_SPEAKER = 0x0000000000000100, -// /** Allows the user to go live */ -// STREAM = 0x0000000000000200, -// /** Allows guild members to view a channel, which includes reading messages in text channels and joining voice channels */ -// VIEW_CHANNEL = 0x0000000000000400, -// /** Allows for sending messages in a channel. (does not allow sending messages in threads) */ -// SEND_MESSAGES = 0x0000000000000800, -// /** Allows for sending of /tts messages */ -// SEND_TTS_MESSAGES = 0x0000000000001000, -// /** Allows for deletion of other users messages */ -// MANAGE_MESSAGES = 0x0000000000002000, -// /** Links sent by users with this permission will be auto-embedded */ -// EMBED_LINKS = 0x0000000000004000, -// /** Allows for uploading images and files */ -// ATTACH_FILES = 0x0000000000008000, -// /** Allows for reading of message history */ -// READ_MESSAGE_HISTORY = 0x0000000000010000, -// /** Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel */ -// MENTION_EVERYONE = 0x0000000000020000, -// /** Allows the usage of custom emojis from other servers */ -// USE_EXTERNAL_EMOJIS = 0x0000000000040000, -// /** Allows for viewing guild insights */ -// VIEW_GUILD_INSIGHTS = 0x0000000000080000, -// /** Allows for joining of a voice channel */ -// CONNECT = 0x0000000000100000, -// /** Allows for speaking in a voice channel */ -// SPEAK = 0x0000000000200000, -// /** Allows for muting members in a voice channel */ -// MUTE_MEMBERS = 0x0000000000400000, -// /** Allows for deafening of members in a voice channel */ -// DEAFEN_MEMBERS = 0x0000000000800000, -// /** Allows for moving of members between voice channels */ -// MOVE_MEMBERS = 0x0000000001000000, -// /** Allows for using voice-activity-detection in a voice channel */ -// USE_VAD = 0x0000000002000000, -// /** Allows for modification of own nickname */ -// CHANGE_NICKNAME = 0x0000000004000000, -// /** Allows for modification of other users nicknames */ -// MANAGE_NICKNAMES = 0x0000000008000000, -// /** Allows management and editing of roles */ -// MANAGE_ROLES = 0x0000000010000000, -// /** Allows management and editing of webhooks */ -// MANAGE_WEBHOOKS = 0x0000000020000000, -// /** Allows management and editing of emojis and stickers */ -// MANAGE_EMOJIS_AND_STICKERS = 0x0000000040000000, -// /** Allows members to use application commands in text channels */ -// USE_SLASH_COMMANDS = 0x0000000080000000, -// /** Allows for requesting to speak in stage channels. */ -// REQUEST_TO_SPEAK = 0x0000000100000000, -// /** Allows for creating, editing, and deleting scheduled events */ -// MANAGE_EVENTS = 0x0000000200000000, -// /** Allows for deleting and archiving threads, and viewing all private threads */ -// MANAGE_THREADS = 0x0000000400000000, -// /** Allows for creating public and announcement threads */ -// CREATE_PUBLIC_THREADS = 0x0000000800000000, -// /** Allows for creating private threads */ -// CREATE_PRIVATE_THREADS = 0x0000001000000000, -// /** Allows the usage of custom stickers from other servers */ -// USE_EXTERNAL_STICKERS = 0x0000002000000000, -// /** Allows for sending messages in threads */ -// SEND_MESSAGES_IN_THREADS = 0x0000004000000000, -// /** Allows for launching activities (applications with the `EMBEDDED` flag) in a voice channel. */ -// USE_EMBEDDED_ACTIVITIES = 0x0000008000000000, -// /** Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels */ -// MODERATE_MEMBERS = 0x0000010000000000, -// } +/** https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags */ +export enum BitwisePermissionFlags { + /** Allows creation of instant invites */ + CREATE_INSTANT_INVITE = 0x0000000000000001, + /** Allows kicking members */ + KICK_MEMBERS = 0x0000000000000002, + /** Allows banning members */ + BAN_MEMBERS = 0x0000000000000004, + /** Allows all permissions and bypasses channel permission overwrites */ + ADMINISTRATOR = 0x0000000000000008, + /** Allows management and editing of channels */ + MANAGE_CHANNELS = 0x0000000000000010, + /** Allows management and editing of the guild */ + MANAGE_GUILD = 0x0000000000000020, + /** Allows for the addition of reactions to messages */ + ADD_REACTIONS = 0x0000000000000040, + /** Allows for viewing of audit logs */ + VIEW_AUDIT_LOG = 0x0000000000000080, + /** Allows for using priority speaker in a voice channel */ + PRIORITY_SPEAKER = 0x0000000000000100, + /** Allows the user to go live */ + STREAM = 0x0000000000000200, + /** Allows guild members to view a channel, which includes reading messages in text channels and joining voice channels */ + VIEW_CHANNEL = 0x0000000000000400, + /** Allows for sending messages in a channel. (does not allow sending messages in threads) */ + SEND_MESSAGES = 0x0000000000000800, + /** Allows for sending of /tts messages */ + SEND_TTS_MESSAGES = 0x0000000000001000, + /** Allows for deletion of other users messages */ + MANAGE_MESSAGES = 0x0000000000002000, + /** Links sent by users with this permission will be auto-embedded */ + EMBED_LINKS = 0x0000000000004000, + /** Allows for uploading images and files */ + ATTACH_FILES = 0x0000000000008000, + /** Allows for reading of message history */ + READ_MESSAGE_HISTORY = 0x0000000000010000, + /** Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel */ + MENTION_EVERYONE = 0x0000000000020000, + /** Allows the usage of custom emojis from other servers */ + USE_EXTERNAL_EMOJIS = 0x0000000000040000, + /** Allows for viewing guild insights */ + VIEW_GUILD_INSIGHTS = 0x0000000000080000, + /** Allows for joining of a voice channel */ + CONNECT = 0x0000000000100000, + /** Allows for speaking in a voice channel */ + SPEAK = 0x0000000000200000, + /** Allows for muting members in a voice channel */ + MUTE_MEMBERS = 0x0000000000400000, + /** Allows for deafening of members in a voice channel */ + DEAFEN_MEMBERS = 0x0000000000800000, + /** Allows for moving of members between voice channels */ + MOVE_MEMBERS = 0x0000000001000000, + /** Allows for using voice-activity-detection in a voice channel */ + USE_VAD = 0x0000000002000000, + /** Allows for modification of own nickname */ + CHANGE_NICKNAME = 0x0000000004000000, + /** Allows for modification of other users nicknames */ + MANAGE_NICKNAMES = 0x0000000008000000, + /** Allows management and editing of roles */ + MANAGE_ROLES = 0x0000000010000000, + /** Allows management and editing of webhooks */ + MANAGE_WEBHOOKS = 0x0000000020000000, + /** Allows management and editing of emojis and stickers */ + MANAGE_EMOJIS_AND_STICKERS = 0x0000000040000000, + /** Allows members to use application commands in text channels */ + USE_SLASH_COMMANDS = 0x0000000080000000, + /** Allows for requesting to speak in stage channels. */ + REQUEST_TO_SPEAK = 0x0000000100000000, + /** Allows for creating, editing, and deleting scheduled events */ + MANAGE_EVENTS = 0x0000000200000000, + /** Allows for deleting and archiving threads, and viewing all private threads */ + MANAGE_THREADS = 0x0000000400000000, + /** Allows for creating public and announcement threads */ + CREATE_PUBLIC_THREADS = 0x0000000800000000, + /** Allows for creating private threads */ + CREATE_PRIVATE_THREADS = 0x0000001000000000, + /** Allows the usage of custom stickers from other servers */ + USE_EXTERNAL_STICKERS = 0x0000002000000000, + /** Allows for sending messages in threads */ + SEND_MESSAGES_IN_THREADS = 0x0000004000000000, + /** Allows for launching activities (applications with the `EMBEDDED` flag) in a voice channel. */ + USE_EMBEDDED_ACTIVITIES = 0x0000008000000000, + /** Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels */ + MODERATE_MEMBERS = 0x0000010000000000, +} // export type PermissionStrings = keyof typeof BitwisePermissionFlags @@ -1108,152 +1102,152 @@ export type GatewayDispatchEventNames = export type GatewayEventNames = GatewayDispatchEventNames | 'READY' | 'RESUMED' -// /** https://discord.com/developers/docs/topics/gateway#list-of-intents */ -// export enum GatewayIntents { -// /** -// * - GUILD_CREATE -// * - GUILD_DELETE -// * - GUILD_ROLE_CREATE -// * - GUILD_ROLE_UPDATE -// * - GUILD_ROLE_DELETE -// * - CHANNEL_CREATE -// * - CHANNEL_UPDATE -// * - CHANNEL_DELETE -// * - CHANNEL_PINS_UPDATE -// * - THREAD_CREATE -// * - THREAD_UPDATE -// * - THREAD_DELETE -// * - THREAD_LIST_SYNC -// * - THREAD_MEMBER_UPDATE -// * - THREAD_MEMBERS_UPDATE -// * - STAGE_INSTANCE_CREATE -// * - STAGE_INSTANCE_UPDATE -// * - STAGE_INSTANCE_DELETE -// */ -// Guilds = 1 << 0, -// /** -// * - GUILD_MEMBER_ADD -// * - GUILD_MEMBER_UPDATE -// * - GUILD_MEMBER_REMOVE -// */ -// GuildMembers = 1 << 1, -// /** -// * - GUILD_BAN_ADD -// * - GUILD_BAN_REMOVE -// */ -// GuildBans = 1 << 2, -// /** -// * - GUILD_EMOJIS_UPDATE -// */ -// GuildEmojis = 1 << 3, -// /** -// * - GUILD_INTEGRATIONS_UPDATE -// * - INTEGRATION_CREATE -// * - INTEGRATION_UPDATE -// * - INTEGRATION_DELETE -// */ -// GuildIntegrations = 1 << 4, -// /** Enables the following events: -// * - WEBHOOKS_UPDATE -// */ -// GuildWebhooks = 1 << 5, -// /** -// * - INVITE_CREATE -// * - INVITE_DELETE -// */ -// GuildInvites = 1 << 6, -// /** -// * - VOICE_STATE_UPDATE -// */ -// GuildVoiceStates = 1 << 7, -// /** -// * - PRESENCE_UPDATE -// */ -// GuildPresences = 1 << 8, -// /** -// * - MESSAGE_CREATE -// * - MESSAGE_UPDATE -// * - MESSAGE_DELETE -// */ -// GuildMessages = 1 << 9, -// /** -// * - MESSAGE_REACTION_ADD -// * - MESSAGE_REACTION_REMOVE -// * - MESSAGE_REACTION_REMOVE_ALL -// * - MESSAGE_REACTION_REMOVE_EMOJI -// */ -// GuildMessageReactions = 1 << 10, -// /** -// * - TYPING_START -// */ -// GuildMessageTyping = 1 << 11, -// /** -// * - CHANNEL_CREATE -// * - MESSAGE_CREATE -// * - MESSAGE_UPDATE -// * - MESSAGE_DELETE -// * - CHANNEL_PINS_UPDATE -// */ -// DirectMessages = 1 << 12, -// /** -// * - MESSAGE_REACTION_ADD -// * - MESSAGE_REACTION_REMOVE -// * - MESSAGE_REACTION_REMOVE_ALL -// * - MESSAGE_REACTION_REMOVE_EMOJI -// */ -// DirectMessageReactions = 1 << 13, -// /** -// * - TYPING_START -// */ -// DirectMessageTyping = 1 << 14, +/** https://discord.com/developers/docs/topics/gateway#list-of-intents */ +export enum GatewayIntents { + /** + * - GUILD_CREATE + * - GUILD_DELETE + * - GUILD_ROLE_CREATE + * - GUILD_ROLE_UPDATE + * - GUILD_ROLE_DELETE + * - CHANNEL_CREATE + * - CHANNEL_UPDATE + * - CHANNEL_DELETE + * - CHANNEL_PINS_UPDATE + * - THREAD_CREATE + * - THREAD_UPDATE + * - THREAD_DELETE + * - THREAD_LIST_SYNC + * - THREAD_MEMBER_UPDATE + * - THREAD_MEMBERS_UPDATE + * - STAGE_INSTANCE_CREATE + * - STAGE_INSTANCE_UPDATE + * - STAGE_INSTANCE_DELETE + */ + Guilds = 1 << 0, + /** + * - GUILD_MEMBER_ADD + * - GUILD_MEMBER_UPDATE + * - GUILD_MEMBER_REMOVE + */ + GuildMembers = 1 << 1, + /** + * - GUILD_BAN_ADD + * - GUILD_BAN_REMOVE + */ + GuildBans = 1 << 2, + /** + * - GUILD_EMOJIS_UPDATE + */ + GuildEmojis = 1 << 3, + /** + * - GUILD_INTEGRATIONS_UPDATE + * - INTEGRATION_CREATE + * - INTEGRATION_UPDATE + * - INTEGRATION_DELETE + */ + GuildIntegrations = 1 << 4, + /** Enables the following events: + * - WEBHOOKS_UPDATE + */ + GuildWebhooks = 1 << 5, + /** + * - INVITE_CREATE + * - INVITE_DELETE + */ + GuildInvites = 1 << 6, + /** + * - VOICE_STATE_UPDATE + */ + GuildVoiceStates = 1 << 7, + /** + * - PRESENCE_UPDATE + */ + GuildPresences = 1 << 8, + /** + * - MESSAGE_CREATE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE + */ + GuildMessages = 1 << 9, + /** + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - MESSAGE_REACTION_REMOVE_EMOJI + */ + GuildMessageReactions = 1 << 10, + /** + * - TYPING_START + */ + GuildMessageTyping = 1 << 11, + /** + * - CHANNEL_CREATE + * - MESSAGE_CREATE + * - MESSAGE_UPDATE + * - MESSAGE_DELETE + * - CHANNEL_PINS_UPDATE + */ + DirectMessages = 1 << 12, + /** + * - MESSAGE_REACTION_ADD + * - MESSAGE_REACTION_REMOVE + * - MESSAGE_REACTION_REMOVE_ALL + * - MESSAGE_REACTION_REMOVE_EMOJI + */ + DirectMessageReactions = 1 << 13, + /** + * - TYPING_START + */ + DirectMessageTyping = 1 << 14, -// /** -// * This intent will add `content` values to all message objects. -// */ -// MessageContent = 1 << 15, -// /** -// * - GUILD_SCHEDULED_EVENT_CREATE -// * - GUILD_SCHEDULED_EVENT_UPDATE -// * - GUILD_SCHEDULED_EVENT_DELETE -// * - GUILD_SCHEDULED_EVENT_USER_ADD this is experimental and unstable. -// * - GUILD_SCHEDULED_EVENT_USER_REMOVE this is experimental and unstable. -// */ -// GuildScheduledEvents = 1 << 16, + /** + * This intent will add `content` values to all message objects. + */ + MessageContent = 1 << 15, + /** + * - GUILD_SCHEDULED_EVENT_CREATE + * - GUILD_SCHEDULED_EVENT_UPDATE + * - GUILD_SCHEDULED_EVENT_DELETE + * - GUILD_SCHEDULED_EVENT_USER_ADD this is experimental and unstable. + * - GUILD_SCHEDULED_EVENT_USER_REMOVE this is experimental and unstable. + */ + GuildScheduledEvents = 1 << 16, -// /** -// * - AUTO_MODERATION_RULE_CREATE -// * - AUTO_MODERATION_RULE_UPDATE -// * - AUTO_MODERATION_RULE_DELETE -// */ -// AutoModerationConfiguration = 1 << 20, -// /** -// * - AUTO_MODERATION_ACTION_EXECUTION -// */ -// AutoModerationExecution = 1 << 21, -// } + /** + * - AUTO_MODERATION_RULE_CREATE + * - AUTO_MODERATION_RULE_UPDATE + * - AUTO_MODERATION_RULE_DELETE + */ + AutoModerationConfiguration = 1 << 20, + /** + * - AUTO_MODERATION_ACTION_EXECUTION + */ + AutoModerationExecution = 1 << 21, +} -// // ALIASES JUST FOR BETTER UX IN THIS CASE +// ALIASES JUST FOR BETTER UX IN THIS CASE -// /** https://discord.com/developers/docs/topics/gateway#list-of-intents */ -// export const Intents = GatewayIntents +/** https://discord.com/developers/docs/topics/gateway#list-of-intents */ +export const Intents = GatewayIntents -// /** https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionresponsetype */ -// export enum InteractionResponseTypes { -// /** ACK a `Ping` */ -// Pong = 1, -// /** Respond to an interaction with a message */ -// ChannelMessageWithSource = 4, -// /** ACK an interaction and edit a response later, the user sees a loading state */ -// DeferredChannelMessageWithSource = 5, -// /** For components, ACK an interaction and edit the original message later; the user does not see a loading state */ -// DeferredUpdateMessage = 6, -// /** For components, edit the message the component was attached to */ -// UpdateMessage = 7, -// /** For Application Command Options, send an autocomplete result */ -// ApplicationCommandAutocompleteResult = 8, -// /** For Command or Component interactions, send a Modal response */ -// Modal = 9, -// } +/** https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionresponsetype */ +export enum InteractionResponseTypes { + /** ACK a `Ping` */ + Pong = 1, + /** Respond to an interaction with a message */ + ChannelMessageWithSource = 4, + /** ACK an interaction and edit a response later, the user sees a loading state */ + DeferredChannelMessageWithSource = 5, + /** For components, ACK an interaction and edit the original message later; the user does not see a loading state */ + DeferredUpdateMessage = 6, + /** For components, edit the message the component was attached to */ + UpdateMessage = 7, + /** For Application Command Options, send an autocomplete result */ + ApplicationCommandAutocompleteResult = 8, + /** For Command or Component interactions, send a Modal response */ + Modal = 9, +} export enum SortOrderTypes { /** Sort forum posts by activity */ @@ -1443,8 +1437,8 @@ export type Camelize = T extends any[] ? Array> : T : T extends Record - ? { [K in keyof T as CamelCase]: Camelize } - : T + ? { [K in keyof T as CamelCase]: Camelize } + : T // /** Non object primitives */ // export type Primitive = diff --git a/packages/utils/src/hash.ts b/packages/utils/src/hash.ts new file mode 100644 index 000000000..2b8421eaf --- /dev/null +++ b/packages/utils/src/hash.ts @@ -0,0 +1,19 @@ +export function iconHashToBigInt (hash: string): bigint { + // The icon is animated so it needs special handling + if (hash.startsWith('a_')) { + // Change the `a_` to just be `a` + hash = `a${hash.substring(2)}` + } else { + // The icon is not animated but it could be that it starts with a 0 so we just put a `b` in front so nothing breaks + hash = `b${hash}` + } + + return BigInt(`0x${hash}`) +} + +export function iconBigintToHash (icon: bigint): string { + // Convert the bigint back to a hash + const hash = icon.toString(16) + // Hashes starting with a are animated and with b are not so need to handle that + return hash.startsWith('a') ? `a_${hash.substring(1)}` : hash.substring(1) +} diff --git a/packages/utils/src/token.ts b/packages/utils/src/token.ts new file mode 100644 index 000000000..4cc0c51f7 --- /dev/null +++ b/packages/utils/src/token.ts @@ -0,0 +1,23 @@ +import { Buffer } from 'node:buffer' + +/** Removes the Bot before the token. */ +export function removeTokenPrefix ( + token?: string, + type: 'GATEWAY' | 'REST' = 'REST' +): string { + // If no token is provided, throw an error + if (token === undefined) { + throw new Error( + `The ${type} was not given a token. Please provide a token and try again.` + ) + } + // If the token does not have a prefix just return token + if (!token.startsWith('Bot ')) return token + // Remove the prefix and return only the token. + return token.substring(token.indexOf(' ') + 1) +} + +/** Get the bot id from the bot token. WARNING: Discord staff has mentioned this may not be stable forever. Use at your own risk. However, note for over 5 years this has never broken. */ +export function getBotIdFromToken (token: string): bigint { + return BigInt(Buffer.from(token.split('.')[0], 'base64').toString()) +} diff --git a/yarn.lock b/yarn.lock index 02be3b9ca..4502b1155 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,6 +21,31 @@ __metadata: languageName: node linkType: hard +"@discordeno/client@workspace:packages/client": + version: 0.0.0-use.local + resolution: "@discordeno/client@workspace:packages/client" + dependencies: + "@discordeno/gateway": 18.0.0-alpha.1 + "@discordeno/rest": 18.0.0-alpha.1 + "@discordeno/types": 18.0.0-alpha.1 + "@discordeno/utils": 18.0.0-alpha.1 + "@swc/cli": ^0.1.57 + "@swc/core": ^1.3.21 + "@types/chai": ^4 + "@types/mocha": ^10 + "@types/node": ^18.11.9 + "@types/sinon": ^10.0.13 + c8: ^7.12.0 + chai: ^4.3.7 + eslint: ^8.0.1 + eslint-config-discordeno: "*" + mocha: ^10.1.0 + sinon: ^15.0.0 + tsconfig: "*" + typescript: ^4.9.3 + languageName: unknown + linkType: soft + "@discordeno/gateway@18.0.0-alpha.1, @discordeno/gateway@workspace:packages/gateway": version: 0.0.0-use.local resolution: "@discordeno/gateway@workspace:packages/gateway"