diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index 7f76e22..4e1a9f6 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -20,14 +20,14 @@ jobs: strategy: matrix: - deno: ['v1.x', 'nightly'] + deno: ['v1.x', 'canary'] steps: - name: Setup repo uses: actions/checkout@v2 - name: Setup Deno - uses: denolib/setup-deno@v2.3.0 + uses: denoland/setup-deno@main with: deno-version: ${{ matrix.deno }} # tests across multiple Deno versions diff --git a/.gitignore b/.gitignore index acbed07..da1c043 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ yarn.lock # PRIVACY XDDDD src/test/config.ts +test/config.ts .vscode # macOS is shit xD @@ -117,4 +118,4 @@ src/test/config.ts # Webstorm dont forget this duude :) .idea/ -src/test/music.mp3 +src/test/music.mp3 \ No newline at end of file diff --git a/README.md b/README.md index f750ffe..c987f5c 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ client.on('ready', () => { // Listen for event whenever a Message is sent client.on('messageCreate', (msg: Message): void => { if (msg.content === '!ping') { - msg.channel.send(`Pong! WS Ping: ${client.ping}`) + msg.channel.send(`Pong! WS Ping: ${client.gateway.ping}`) } }) @@ -95,7 +95,7 @@ class PingCommand extends Command { name = 'ping' execute(ctx: CommandContext) { - ctx.message.reply(`pong! Ping: ${ctx.client.ping}ms`) + ctx.message.reply(`pong! Ping: ${ctx.client.gateway.ping}ms`) } } @@ -156,7 +156,7 @@ Documentation is available for `main` (branch) and `stable` (release). ## Found a bug or want support? Join our discord server! -[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/WVN2JF2FRv) +[![Widget for the Discord Server](https://discord.com/api/guilds/783319033205751809/widget.png?style=banner1)](https://discord.gg/harmonyland) ## Maintainer diff --git a/deploy.ts b/deploy.ts new file mode 100644 index 0000000..7684b41 --- /dev/null +++ b/deploy.ts @@ -0,0 +1,131 @@ +import { + SlashCommandsManager, + SlashClient, + SlashCommandHandlerCallback, + SlashCommandHandler +} from './src/interactions/mod.ts' +import { InteractionResponseType, InteractionType } from './src/types/slash.ts' + +export interface DeploySlashInitOptions { + env?: boolean + publicKey?: string + token?: string +} + +/** Current Slash Client being used to handle commands */ +let client: SlashClient +/** Manage Slash Commands right in Deploy */ +let commands: SlashCommandsManager + +/** + * Initialize Slash Commands Handler for [Deno Deploy](https://deno.com/deploy). + * Easily create Serverless Slash Commands on the fly. + * + * **Examples** + * + * ```ts + * init({ + * publicKey: "my public key", + * token: "my bot's token", // only required if you want to manage slash commands in code + * }) + * ``` + * + * ```ts + * // takes up `PUBLIC_KEY` and `TOKEN` from ENV + * init({ env: true }) + * ``` + * + * @param options Initialization options + */ +export function init(options: { env: boolean }): void +export function init(options: { publicKey: string; token?: string }): void +export function init(options: DeploySlashInitOptions): void { + if (client !== undefined) throw new Error('Already initialized') + if (options.env === true) { + options.publicKey = Deno.env.get('PUBLIC_KEY') + options.token = Deno.env.get('TOKEN') + } + + if (options.publicKey === undefined) + throw new Error('Public Key not provided') + + client = new SlashClient({ + token: options.token, + publicKey: options.publicKey + }) + + commands = client.commands + + const cb = async (evt: { + respondWith: CallableFunction + request: Request + }): Promise => { + try { + // we have to wrap because there are some weird scope errors + const d = await client.verifyFetchEvent({ + respondWith: (...args: any[]) => evt.respondWith(...args), + request: evt.request + }) + if (d === false) { + await evt.respondWith( + new Response('Not Authorized', { + status: 400 + }) + ) + return + } + + if (d.type === InteractionType.PING) { + await d.respond({ type: InteractionResponseType.PONG }) + client.emit('ping') + return + } + + await (client as any)._process(d) + } catch (e) { + await client.emit('interactionError', e) + } + } + + addEventListener('fetch', cb as any) +} + +/** + * Register Slash Command handler. + * + * Example: + * + * ```ts + * handle("ping", (interaction) => { + * interaction.reply("Pong!") + * }) + * ``` + * + * Also supports Sub Command and Group handling out of the box! + * ```ts + * handle("command-name group-name sub-command", (i) => { + * // ... + * }) + * + * handle("command-name sub-command", (i) => { + * // ... + * }) + * ``` + * + * @param cmd Command to handle. Either Handler object or command name followed by handler function in next parameter. + * @param handler Handler function (required if previous argument was command name) + */ +export function handle( + cmd: string | SlashCommandHandler, + handler?: SlashCommandHandlerCallback +): void { + if (client === undefined) + throw new Error('Slash Client not initialized. Call `init` first') + client.handle(cmd, handler) +} + +export { commands, client } +export * from './src/types/slash.ts' +export * from './src/structures/slash.ts' +export * from './src/interactions/mod.ts' +export * from './src/types/channel.ts' diff --git a/deps.ts b/deps.ts index e20a610..43fc90c 100644 --- a/deps.ts +++ b/deps.ts @@ -1,11 +1,6 @@ export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts' export { unzlib } from 'https://denopkg.com/DjDeveloperr/denoflate@1.2/mod.ts' export { fetchAuto } from 'https://deno.land/x/fetchbase64@1.0.0/mod.ts' -export { connect } from 'https://deno.land/x/redis@v0.14.1/mod.ts' -export type { - Redis, - RedisConnectOptions -} from 'https://deno.land/x/redis@v0.14.1/mod.ts' export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts' export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' export { Mixin } from 'https://esm.sh/ts-mixer@5.4.0' diff --git a/mod.ts b/mod.ts index ae8d04c..ffa82ad 100644 --- a/mod.ts +++ b/mod.ts @@ -1,14 +1,18 @@ export { GatewayIntents } from './src/types/gateway.ts' export { Base } from './src/structures/base.ts' -export { Gateway } from './src/gateway/index.ts' -export type { GatewayTypedEvents } from './src/gateway/index.ts' -export type { ClientEvents } from './src/gateway/handlers/index.ts' -export * from './src/models/client.ts' -export * from './src/models/slashClient.ts' -export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts' -export type { RequestHeaders } from './src/models/rest.ts' -export type { RESTOptions } from './src/models/rest.ts' -export * from './src/models/cacheAdapter.ts' +export { Gateway } from './src/gateway/mod.ts' +export type { GatewayTypedEvents } from './src/gateway/mod.ts' +export type { ClientEvents } from './src/gateway/handlers/mod.ts' +export * from './src/client/mod.ts' +export * from './src/interactions/mod.ts' +export { + RESTManager, + TokenType, + HttpResponseCode, + DiscordAPIError +} from './src/rest/mod.ts' +export * from './src/rest/mod.ts' +export * from './src/cache/adapter.ts' export { Command, CommandBuilder, @@ -16,16 +20,16 @@ export { CommandsManager, CategoriesManager, CommandsLoader -} from './src/models/command.ts' -export type { CommandContext, CommandOptions } from './src/models/command.ts' +} from './src/commands/command.ts' +export type { CommandContext, CommandOptions } from './src/commands/command.ts' export { Extension, ExtensionCommands, ExtensionsManager -} from './src/models/extensions.ts' -export { SlashModule } from './src/models/slashModule.ts' -export { CommandClient, command } from './src/models/commandClient.ts' -export type { CommandClientOptions } from './src/models/commandClient.ts' +} from './src/commands/extension.ts' +export { SlashModule } from './src/interactions/slashModule.ts' +export { CommandClient, command } from './src/commands/client.ts' +export type { CommandClientOptions } from './src/commands/client.ts' export { BaseManager } from './src/managers/base.ts' export { BaseChildManager } from './src/managers/baseChild.ts' export { ChannelsManager } from './src/managers/channels.ts' @@ -45,7 +49,7 @@ export { RolesManager } from './src/managers/roles.ts' export { UsersManager } from './src/managers/users.ts' export { InviteManager } from './src/managers/invites.ts' export { Application } from './src/structures/application.ts' -// export { ImageURL } from './src/structures/cdn.ts' +export { ImageURL } from './src/structures/cdn.ts' export { Channel, GuildChannel } from './src/structures/channel.ts' export type { EditOverwriteOptions } from './src/structures/channel.ts' export { DMChannel } from './src/structures/dmChannel.ts' @@ -63,7 +67,11 @@ export { NewsChannel } from './src/structures/guildNewsChannel.ts' export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' export { Invite } from './src/structures/invite.ts' export * from './src/structures/member.ts' -export { Message, MessageAttachment } from './src/structures/message.ts' +export { + Message, + MessageAttachment, + MessageInteraction +} from './src/structures/message.ts' export { MessageMentions } from './src/structures/messageMentions.ts' export { Presence, @@ -88,7 +96,7 @@ export { Intents } from './src/utils/intents.ts' export * from './src/utils/permissions.ts' export { UserFlagsManager } from './src/utils/userFlags.ts' export { HarmonyEventEmitter } from './src/utils/events.ts' -export type { EveryChannelTypes } from './src/utils/getChannelByType.ts' +export type { EveryChannelTypes } from './src/utils/channel.ts' export * from './src/utils/bitfield.ts' export type { ActivityGame, @@ -96,7 +104,15 @@ export type { ClientStatus, StatusType } from './src/types/presence.ts' -export { ChannelTypes } from './src/types/channel.ts' +export { + ChannelTypes, + OverwriteType, + OverrideType +} from './src/types/channel.ts' +export type { + OverwriteAsOptions, + OverwritePayload +} from './src/types/channel.ts' export type { ApplicationPayload } from './src/types/application.ts' export type { ImageFormats, ImageSize } from './src/types/cdn.ts' export type { @@ -110,9 +126,18 @@ export type { GuildVoiceChannelPayload, GroupDMChannelPayload, MessageOptions, + MessagePayload, + MessageInteractionPayload, + MessageReference, + MessageActivity, + MessageActivityTypes, + MessageApplication, + MessageFlags, + MessageStickerFormatTypes, + MessageStickerPayload, + MessageTypes, OverwriteAsArg, - Overwrite, - OverwriteAsOptions + Overwrite } from './src/types/channel.ts' export type { EmojiPayload } from './src/types/emoji.ts' export { Verification } from './src/types/guild.ts' @@ -145,7 +170,9 @@ export type { UserPayload } from './src/types/user.ts' export { UserFlags } from './src/types/userFlags.ts' export type { VoiceStatePayload } from './src/types/voice.ts' export type { WebhookPayload } from './src/types/webhook.ts' -export * from './src/models/collectors.ts' +export * from './src/client/collectors.ts' +export type { Dict } from './src/utils/dict.ts' +export * from './src/cache/redis.ts' export { ColorUtil } from './src/utils/colorutil.ts' export type { Colors } from './src/utils/colorutil.ts' -export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts' \ No newline at end of file +export { StageVoiceChannel } from './src/structures/guildVoiceStageChannel.ts' diff --git a/src/cache/adapter.ts b/src/cache/adapter.ts new file mode 100644 index 0000000..a29bd89 --- /dev/null +++ b/src/cache/adapter.ts @@ -0,0 +1,22 @@ +/** + * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. + * + * Methods can return Promises too. + */ +export interface ICacheAdapter { + /** Gets a key from a Cache */ + get: (cacheName: string, key: string) => Promise | any + /** Sets a key to value in a Cache Name with optional expire value in MS */ + set: ( + cacheName: string, + key: string, + value: any, + expire?: number + ) => Promise | any + /** Deletes a key from a Cache */ + delete: (cacheName: string, key: string) => Promise | boolean + /** Gets array of all values in a Cache */ + array: (cacheName: string) => undefined | any[] | Promise + /** Entirely deletes a Cache */ + deleteCache: (cacheName: string) => any +} diff --git a/src/cache/default.ts b/src/cache/default.ts new file mode 100644 index 0000000..401c1c2 --- /dev/null +++ b/src/cache/default.ts @@ -0,0 +1,50 @@ +import { Collection } from '../utils/collection.ts' +import type { ICacheAdapter } from './adapter.ts' + +/** Default Cache Adapter for in-memory caching. */ +export class DefaultCacheAdapter implements ICacheAdapter { + data: { + [name: string]: Collection + } = {} + + async get(cacheName: string, key: string): Promise { + const cache = this.data[cacheName] + if (cache === undefined) return + return cache.get(key) + } + + async set( + cacheName: string, + key: string, + value: any, + expire?: number + ): Promise { + let cache = this.data[cacheName] + if (cache === undefined) { + this.data[cacheName] = new Collection() + cache = this.data[cacheName] + } + cache.set(key, value) + if (expire !== undefined) + setTimeout(() => { + cache.delete(key) + }, expire) + } + + async delete(cacheName: string, key: string): Promise { + const cache = this.data[cacheName] + if (cache === undefined) return false + return cache.delete(key) + } + + async array(cacheName: string): Promise { + const cache = this.data[cacheName] + if (cache === undefined) return + return cache.array() + } + + async deleteCache(cacheName: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + return delete this.data[cacheName] + } +} diff --git a/src/cache/mod.ts b/src/cache/mod.ts new file mode 100644 index 0000000..cd30b7b --- /dev/null +++ b/src/cache/mod.ts @@ -0,0 +1,4 @@ +export * from './adapter.ts' +export * from './default.ts' +// Not exported by default +// export * from './redis.ts' diff --git a/src/models/cacheAdapter.ts b/src/cache/redis.ts similarity index 55% rename from src/models/cacheAdapter.ts rename to src/cache/redis.ts index b9f5193..8184f82 100644 --- a/src/models/cacheAdapter.ts +++ b/src/cache/redis.ts @@ -1,176 +1,110 @@ -import { Collection } from '../utils/collection.ts' -import { connect, Redis, RedisConnectOptions } from '../../deps.ts' - -/** - * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. - * - * Methods can return Promises too. - */ -export interface ICacheAdapter { - /** Gets a key from a Cache */ - get: (cacheName: string, key: string) => Promise | any - /** Sets a key to value in a Cache Name with optional expire value in MS */ - set: ( - cacheName: string, - key: string, - value: any, - expire?: number - ) => Promise | any - /** Deletes a key from a Cache */ - delete: (cacheName: string, key: string) => Promise | boolean - /** Gets array of all values in a Cache */ - array: (cacheName: string) => undefined | any[] | Promise - /** Entirely deletes a Cache */ - deleteCache: (cacheName: string) => any -} - -/** Default Cache Adapter for in-memory caching. */ -export class DefaultCacheAdapter implements ICacheAdapter { - data: { - [name: string]: Collection - } = {} - - async get(cacheName: string, key: string): Promise { - const cache = this.data[cacheName] - if (cache === undefined) return - return cache.get(key) - } - - async set( - cacheName: string, - key: string, - value: any, - expire?: number - ): Promise { - let cache = this.data[cacheName] - if (cache === undefined) { - this.data[cacheName] = new Collection() - cache = this.data[cacheName] - } - cache.set(key, value) - if (expire !== undefined) - setTimeout(() => { - cache.delete(key) - }, expire) - } - - async delete(cacheName: string, key: string): Promise { - const cache = this.data[cacheName] - if (cache === undefined) return false - return cache.delete(key) - } - - async array(cacheName: string): Promise { - const cache = this.data[cacheName] - if (cache === undefined) return - return cache.array() - } - - async deleteCache(cacheName: string): Promise { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - return delete this.data[cacheName] - } -} - -/** Redis Cache Adapter for using Redis as a cache-provider. */ -export class RedisCacheAdapter implements ICacheAdapter { - _redis: Promise - redis?: Redis - ready: boolean = false - readonly _expireIntervalTimer: number = 5000 - private _expireInterval?: number - - constructor(options: RedisConnectOptions) { - this._redis = connect(options) - this._redis.then( - (redis) => { - this.redis = redis - this.ready = true - this._startExpireInterval() - }, - () => { - // TODO: Make error for this - } - ) - } - - private _startExpireInterval(): void { - this._expireInterval = setInterval(() => { - this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { - for (const name of names) { - this.redis?.hvals(name).then((vals) => { - for (const val of vals) { - const expireVal: { - name: string - key: string - at: number - } = JSON.parse(val) - const expired = new Date().getTime() > expireVal.at - if (expired) this.redis?.hdel(expireVal.name, expireVal.key) - } - }) - } - }) - }, this._expireIntervalTimer) - } - - async _checkReady(): Promise { - if (!this.ready) await this._redis - } - - async get(cacheName: string, key: string): Promise { - await this._checkReady() - const cache = await this.redis?.hget(cacheName, key) - if (cache === undefined) return - try { - return JSON.parse(cache) - } catch (e) { - return cache - } - } - - async set( - cacheName: string, - key: string, - value: any, - expire?: number - ): Promise { - await this._checkReady() - const result = await this.redis?.hset( - cacheName, - key, - typeof value === 'object' ? JSON.stringify(value) : value - ) - if (expire !== undefined) { - await this.redis?.hset( - `${cacheName}:expires`, - key, - JSON.stringify({ - name: cacheName, - key, - at: new Date().getTime() + expire - }) - ) - } - return result - } - - async delete(cacheName: string, key: string): Promise { - await this._checkReady() - const exists = await this.redis?.hexists(cacheName, key) - if (exists === 0) return false - await this.redis?.hdel(cacheName, key) - return true - } - - async array(cacheName: string): Promise { - await this._checkReady() - const data = await this.redis?.hvals(cacheName) - return data?.map((e: string) => JSON.parse(e)) - } - - async deleteCache(cacheName: string): Promise { - await this._checkReady() - return (await this.redis?.del(cacheName)) !== 0 - } -} +import { ICacheAdapter } from './adapter.ts' +// Not in deps.ts to allow optional dep loading +import { + connect, + Redis, + RedisConnectOptions +} from 'https://deno.land/x/redis@v0.14.1/mod.ts' + +/** Redis Cache Adapter for using Redis as a cache-provider. */ +export class RedisCacheAdapter implements ICacheAdapter { + _redis: Promise + redis?: Redis + ready: boolean = false + readonly _expireIntervalTimer: number = 5000 + private _expireInterval?: number + + constructor(options: RedisConnectOptions) { + this._redis = connect(options) + this._redis.then( + (redis) => { + this.redis = redis + this.ready = true + this._startExpireInterval() + }, + () => { + // TODO: Make error for this + } + ) + } + + private _startExpireInterval(): void { + this._expireInterval = setInterval(() => { + this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => { + for (const name of names) { + this.redis?.hvals(name).then((vals) => { + for (const val of vals) { + const expireVal: { + name: string + key: string + at: number + } = JSON.parse(val) + const expired = new Date().getTime() > expireVal.at + if (expired) this.redis?.hdel(expireVal.name, expireVal.key) + } + }) + } + }) + }, this._expireIntervalTimer) + } + + async _checkReady(): Promise { + if (!this.ready) await this._redis + } + + async get(cacheName: string, key: string): Promise { + await this._checkReady() + const cache = await this.redis?.hget(cacheName, key) + if (cache === undefined) return + try { + return JSON.parse(cache) + } catch (e) { + return cache + } + } + + async set( + cacheName: string, + key: string, + value: any, + expire?: number + ): Promise { + await this._checkReady() + const result = await this.redis?.hset( + cacheName, + key, + typeof value === 'object' ? JSON.stringify(value) : value + ) + if (expire !== undefined) { + await this.redis?.hset( + `${cacheName}:expires`, + key, + JSON.stringify({ + name: cacheName, + key, + at: new Date().getTime() + expire + }) + ) + } + return result + } + + async delete(cacheName: string, key: string): Promise { + await this._checkReady() + const exists = await this.redis?.hexists(cacheName, key) + if (exists === 0) return false + await this.redis?.hdel(cacheName, key) + return true + } + + async array(cacheName: string): Promise { + await this._checkReady() + const data = await this.redis?.hvals(cacheName) + return data?.map((e: string) => JSON.parse(e)) + } + + async deleteCache(cacheName: string): Promise { + await this._checkReady() + return (await this.redis?.del(cacheName)) !== 0 + } +} diff --git a/src/models/client.ts b/src/client/client.ts similarity index 83% rename from src/models/client.ts rename to src/client/client.ts index 83846cd..457d77b 100644 --- a/src/models/client.ts +++ b/src/client/client.ts @@ -1,494 +1,440 @@ -/* eslint-disable @typescript-eslint/method-signature-style */ -import { User } from '../structures/user.ts' -import { GatewayIntents } from '../types/gateway.ts' -import { Gateway } from '../gateway/index.ts' -import { RESTManager, RESTOptions, TokenType } from './rest.ts' -import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' -import { UsersManager } from '../managers/users.ts' -import { GuildManager } from '../managers/guilds.ts' -import { ChannelsManager } from '../managers/channels.ts' -import { ClientPresence } from '../structures/presence.ts' -import { EmojisManager } from '../managers/emojis.ts' -import { ActivityGame, ClientActivity } from '../types/presence.ts' -import { Extension } from './extensions.ts' -import { SlashClient } from './slashClient.ts' -import { Interaction } from '../structures/slash.ts' -import { SlashModule } from './slashModule.ts' -import { ShardManager } from './shard.ts' -import { Application } from '../structures/application.ts' -import { Invite } from '../structures/invite.ts' -import { INVITE } from '../types/endpoint.ts' -import { ClientEvents } from '../gateway/handlers/index.ts' -import type { Collector } from './collectors.ts' -import { HarmonyEventEmitter } from '../utils/events.ts' -import { VoiceRegion } from '../types/voice.ts' -import { fetchAuto } from '../../deps.ts' -import { DMChannel } from '../structures/dmChannel.ts' -import { Template } from '../structures/template.ts' - -/** OS related properties sent with Gateway Identify */ -export interface ClientProperties { - os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string - browser?: 'harmony' | string - device?: 'harmony' | string -} - -/** Some Client Options to modify behaviour */ -export interface ClientOptions { - /** ID of the Client/Application to initialize Slash Client REST */ - id?: string - /** Token of the Bot/User */ - token?: string - /** Gateway Intents */ - intents?: GatewayIntents[] - /** Cache Adapter to use, defaults to Collections one */ - cache?: ICacheAdapter - /** Force New Session and don't use cached Session (by persistent caching) */ - forceNewSession?: boolean - /** Startup presence of client */ - presence?: ClientPresence | ClientActivity | ActivityGame - /** Force all requests to Canary API */ - canary?: boolean - /** Time till which Messages are to be cached, in MS. Default is 3600000 */ - messageCacheLifetime?: number - /** Time till which Message Reactions are to be cached, in MS. Default is 3600000 */ - reactionCacheLifetime?: number - /** Whether to fetch Uncached Message of Reaction or not? */ - fetchUncachedReactions?: boolean - /** Client Properties */ - clientProperties?: ClientProperties - /** Enable/Disable Slash Commands Integration (enabled by default) */ - enableSlash?: boolean - /** Disable taking token from env if not provided (token is taken from env if present by default) */ - disableEnvToken?: boolean - /** Override REST Options */ - restOptions?: RESTOptions - /** Whether to fetch Gateway info or not */ - fetchGatewayInfo?: boolean - /** ADVANCED: Shard ID to launch on */ - shard?: number - /** ADVACNED: Shard count. */ - shardCount?: number | 'auto' -} - -/** - * Discord Client. - */ -export class Client extends HarmonyEventEmitter { - /** REST Manager - used to make all requests */ - rest: RESTManager - /** User which Client logs in to, undefined until logs in */ - user?: User - /** WebSocket ping of Client */ - ping = 0 - /** Token of the Bot/User */ - token?: string - /** Cache Adapter */ - cache: ICacheAdapter = new DefaultCacheAdapter() - /** Gateway Intents */ - intents?: GatewayIntents[] - /** Whether to force new session or not */ - forceNewSession?: boolean - /** Time till messages to stay cached, in MS. */ - messageCacheLifetime: number = 3600000 - /** Time till messages to stay cached, in MS. */ - reactionCacheLifetime: number = 3600000 - /** Whether to fetch Uncached Message of Reaction or not? */ - fetchUncachedReactions: boolean = false - /** Client Properties */ - clientProperties: ClientProperties - /** Slash-Commands Management client */ - slash: SlashClient - /** Whether to fetch Gateway info or not */ - fetchGatewayInfo: boolean = true - - /** Users Manager, containing all Users cached */ - users: UsersManager = new UsersManager(this) - /** Guilds Manager, providing cache & API interface to Guilds */ - guilds: GuildManager = new GuildManager(this) - /** Channels Manager, providing cache interface to Channels */ - channels: ChannelsManager = new ChannelsManager(this) - /** Channels Manager, providing cache interface to Channels */ - emojis: EmojisManager = new EmojisManager(this) - - /** Last READY timestamp */ - upSince?: Date - - /** Client's presence. Startup one if set before connecting */ - presence: ClientPresence = new ClientPresence() - _decoratedEvents?: { - [name: string]: (...args: any[]) => void - } - - _decoratedSlash?: Array<{ - name: string - guild?: string - parent?: string - group?: string - handler: (interaction: Interaction) => any - }> - - _id?: string - - /** Shard on which this Client is */ - shard?: number - /** Shard Count */ - shardCount: number | 'auto' = 'auto' - /** Shard Manager of this Client if Sharded */ - shards: ShardManager - /** Collectors set */ - collectors: Set = new Set() - - /** Since when is Client online (ready). */ - get uptime(): number { - if (this.upSince === undefined) return 0 - else { - const dif = Date.now() - this.upSince.getTime() - if (dif < 0) return 0 - else return dif - } - } - - get gateway(): Gateway { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return this.shards.list.get('0') as Gateway - } - - applicationID?: string - applicationFlags?: number - - constructor(options: ClientOptions = {}) { - super() - this._id = options.id - this.token = options.token - this.intents = options.intents - this.shards = new ShardManager(this) - this.forceNewSession = options.forceNewSession - if (options.cache !== undefined) this.cache = options.cache - if (options.presence !== undefined) - this.presence = - options.presence instanceof ClientPresence - ? options.presence - : new ClientPresence(options.presence) - if (options.messageCacheLifetime !== undefined) - this.messageCacheLifetime = options.messageCacheLifetime - if (options.reactionCacheLifetime !== undefined) - this.reactionCacheLifetime = options.reactionCacheLifetime - if (options.fetchUncachedReactions === true) - this.fetchUncachedReactions = true - - if ( - this._decoratedEvents !== undefined && - Object.keys(this._decoratedEvents).length !== 0 - ) { - Object.entries(this._decoratedEvents).forEach((entry) => { - this.on(entry[0] as keyof ClientEvents, entry[1].bind(this)) - }) - this._decoratedEvents = undefined - } - - this.clientProperties = - options.clientProperties === undefined - ? { - os: Deno.build.os, - browser: 'harmony', - device: 'harmony' - } - : options.clientProperties - - if (options.shard !== undefined) this.shard = options.shard - if (options.shardCount !== undefined) this.shardCount = options.shardCount - - this.fetchGatewayInfo = options.fetchGatewayInfo ?? true - - if (this.token === undefined) { - try { - const token = Deno.env.get('DISCORD_TOKEN') - if (token !== undefined) { - this.token = token - this.debug('Info', 'Found token in ENV') - } - } catch (e) {} - } - - const restOptions: RESTOptions = { - token: () => this.token, - tokenType: TokenType.Bot, - canary: options.canary, - client: this - } - - if (options.restOptions !== undefined) - Object.assign(restOptions, options.restOptions) - this.rest = new RESTManager(restOptions) - - this.slash = new SlashClient({ - id: () => this.getEstimatedID(), - client: this, - enabled: options.enableSlash - }) - } - - /** - * Sets Cache Adapter - * - * Should NOT be set after bot is already logged in or using current cache. - * Please look into using `cache` option. - */ - setAdapter(adapter: ICacheAdapter): Client { - this.cache = adapter - return this - } - - /** Changes Presence of Client */ - setPresence(presence: ClientPresence | ClientActivity | ActivityGame): void { - if (presence instanceof ClientPresence) { - this.presence = presence - } else this.presence = new ClientPresence(presence) - this.gateway?.sendPresence(this.presence.create()) - } - - /** Emits debug event */ - debug(tag: string, msg: string): void { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.emit('debug', `[${tag}] ${msg}`) - } - - getEstimatedID(): string { - if (this.user !== undefined) return this.user.id - else if (this.token !== undefined) { - try { - return atob(this.token.split('.')[0]) - } catch (e) { - return this._id ?? 'unknown' - } - } else { - return this._id ?? 'unknown' - } - } - - /** Fetch Application of the Client */ - async fetchApplication(): Promise { - const app = await this.rest.api.oauth2.applications['@me'].get() - return new Application(this, app) - } - - /** Fetch an Invite */ - async fetchInvite(id: string): Promise { - return await new Promise((resolve, reject) => { - this.rest - .get(INVITE(id)) - .then((data) => { - resolve(new Invite(this, data)) - }) - .catch((e) => reject(e)) - }) - } - - /** - * This function is used for connecting to discord. - * @param token Your token. This is required if not given in ClientOptions. - * @param intents Gateway intents in array. This is required if not given in ClientOptions. - */ - async connect(token?: string, intents?: GatewayIntents[]): Promise { - token ??= this.token - if (token === undefined) throw new Error('No Token Provided') - this.token = token - if (intents !== undefined && this.intents !== undefined) { - this.debug( - 'client', - 'Intents were set in both client and connect function. Using the one in the connect function...' - ) - } else if (intents === undefined && this.intents !== undefined) { - intents = this.intents - } else if (intents !== undefined && this.intents === undefined) { - this.intents = intents - } else throw new Error('No Gateway Intents were provided') - - this.rest.token = token - if (this.shard !== undefined) { - if (typeof this.shardCount === 'number') - this.shards.cachedShardCount = this.shardCount - await this.shards.launch(this.shard) - } else await this.shards.connect() - return this.waitFor('ready', () => true).then(() => this) - } - - /** Destroy the Gateway connection */ - async destroy(): Promise { - this.gateway.initialized = false - this.gateway.sequenceID = undefined - this.gateway.sessionID = undefined - await this.gateway.cache.delete('seq') - await this.gateway.cache.delete('session_id') - this.gateway.close() - this.user = undefined - this.upSince = undefined - return this - } - - /** Attempt to Close current Gateway connection and Resume */ - async reconnect(): Promise { - this.gateway.close() - this.gateway.initWebsocket() - return this.waitFor('ready', () => true).then(() => this) - } - - /** Add a new Collector */ - addCollector(collector: Collector): boolean { - if (this.collectors.has(collector)) return false - else { - this.collectors.add(collector) - return true - } - } - - /** Remove a Collector */ - removeCollector(collector: Collector): boolean { - if (!this.collectors.has(collector)) return false - else { - this.collectors.delete(collector) - return true - } - } - - async emit(event: keyof ClientEvents, ...args: any[]): Promise { - const collectors: Collector[] = [] - for (const collector of this.collectors.values()) { - if (collector.event === event) collectors.push(collector) - } - if (collectors.length !== 0) { - this.collectors.forEach((collector) => collector._fire(...args)) - } - // TODO(DjDeveloperr): Fix this ts-ignore - // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error - // @ts-ignore - return super.emit(event, ...args) - } - - /** Returns an array of voice region objects that can be used when creating servers. */ - async fetchVoiceRegions(): Promise { - return this.rest.api.voice.regions.get() - } - - /** Modify current (Client) User. */ - async editUser(data: { - username?: string - avatar?: string - }): Promise { - if (data.username === undefined && data.avatar === undefined) - throw new Error( - 'Either username or avatar or both must be specified to edit' - ) - - if (data.avatar?.startsWith('http') === true) { - data.avatar = await fetchAuto(data.avatar) - } - - await this.rest.api.users['@me'].patch({ - username: data.username, - avatar: data.avatar - }) - return this - } - - /** Change Username of the Client User */ - async setUsername(username: string): Promise { - return await this.editUser({ username }) - } - - /** Change Avatar of the Client User */ - async setAvatar(avatar: string): Promise { - return await this.editUser({ avatar }) - } - - /** Create a DM Channel with a User */ - async createDM(user: User | string): Promise { - const id = typeof user === 'object' ? user.id : user - const dmPayload = await this.rest.api.users['@me'].channels.post({ - recipient_id: id - }) - await this.channels.set(dmPayload.id, dmPayload) - return (this.channels.get(dmPayload.id) as unknown) as DMChannel - } - - /** Returns a template object for the given code. */ - async fetchTemplate(code: string): Promise