diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b66f63..50d01a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,4 +26,4 @@ jobs: run: yarn test --ci --coverage --maxWorkers=2 - name: Build - run: yarn build + run: yarn build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8ba145a..9b1ee42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + .env -/dist -/node_modules \ No newline at end of file +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b5bca2a..0000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "singleQuote": true, - "printWidth": 120, - "semi": true, - "tabWidth": 4, - "trailingComma": "none" -} \ No newline at end of file diff --git a/README.md b/README.md index 1deca27..f01e1d2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,33 @@
-

discord.ts

-

Build good bots fast

+ unicord +

unicord

+

Build good bots fast

-> This package is in an early Alpha stage and not yet ready for consumption +> This package is in early development and is not yet ready for production use. Please be aware that the API may change at any time. Use at your own risk. -A solid and simple Discord API wrapper. +This library is designed with three pillars in mind: **Simple**, **Maintainable**, and **Scalable**. It isn't guaranteed to be the fastest or the most feature rich, but you will probably find it to be the easiest to use and understand. + +It's typecript first and has some nifty features like hydration and argument type resolution. The goal here is to not have to worry about if you are using the Gateway, Webhooks, or Application commands. Ideally there is a single API that can be used to inferface with Discord. ## Installation + +Install with your package manager of choice. + ``` -yarn add @jaronp/discord.ts +npm install unicord +yarn add unicord +bun add unicord ``` ## Quick Start + A small example implementation. -```ts -import { Client } from '@jaronp/discord.ts'; +```typescript +import { Client, type Context } from 'unicord'; // Initialize and configure -const bot = new Client({ +const client = new Client({ token: '', application_id: '', intents: [ @@ -26,52 +35,149 @@ const bot = new Client({ Intent.GUILD_MESSAGES, Intent.GUILD_MESSAGE_REACTIONS, Intent.MESSAGE_CONTENT - ] + ], + prefix: '??' }); -// Register a listener -bot.handlers.register('READY', (client, context, data) => { - console.log(`Logged in as ${client.user.tag}`); +// Register arbitrary event handler to the client bus +client.on('READY', async (_, payload) => { + console.log(`Logged in as ${payload.user.username}${payload.user.discriminator?.length > 0 ? `#${payload.user.discriminator}` : ''}`); }); // Register a command -bot.commands.register('ping', (client, context, args) => { +client.chatCommands.register('ping', (context: Context, args) => { context.reply('Pong!', true); }); // Connect -bot.connect(); +client.connect(); ``` +## Type Resolution and Saftey + +This library is designed to be type safe and resolve types for you. Because of this you can write code that is both safe and easy to understand. With the `createCommandHandler` utility you can provide types for command arguments. The client will then check and resolve them for you. + +```typescript +import { createCommandHandler, CommandOptionType, type Context } from 'unicord'; + +// <...> + +// Define the command handler with the utility function +const avatarCommand = createCommandHandler({ + description: 'Get a users avatar', + args: [ + { + id: 'user', + name: 'user', + type: CommandOptionType.User, + description: 'The user to get the avatar for', + required: true + } + ], + execute: async (context: Context, args) => { + // Args will be typed here as { user: User } + context.reply(args.user.avatarURL); + } +}); + +// You can then register the command with the client +// The class generated can be used for both chat and application commands +client.chatCommands.register('avatar', avatarCommand); +client.applicationCommands.register('avatar', ApplicationCommandType.Chat, avatarCommand); + +// <...> +``` + +## Hydration + +Hydration allows you to fetch and fill data on Discord objects. This is useful for when you need to fetch additional data about an object. + +```typescript +client.chatCommands.register('ping', async (context: Context, args: any[]) => { + // Hydrate the message id from the context + const { message } = await context.hydrate(context, [Expectation.Message]); + // Send a message to the channel + const response = await context.reply(`Pong!: got ${args.join(', ')}`, true); + // Calculate the time it took to send the message + const time = message.timestamp.getTime() - message.timestamp.getTime(); + // Edit the message to include the time + await context.editMessage(response, `Pong! Latency: ${time}ms`); +}); +``` + +This is a powerful feature that allows you to fetch and use data from Discord objects in a type safe way. It also ensures the minimum amount of API calls are made and that no extra data is fetched or passed. + +You can also generate a hydration function that returns a type narrowing boolean. This can be used to check if the object is hydrated or not. + +```typescript +client.chatCommands.register('me', async (context: Context, _args: any[]) => { + // Hydrate the message id from the context + const hydrate = await context.hydrator(hydrated, [Expectation.Guild]); + // Check if the object is hydrated + const hasGuild = hydrate(hydrated); + + if (hasGuild) { + // Send a message stating the user that they are in a guild + await context.reply(`You are ${message.author.username} and this is ${message.guild.name}`, true); + } else { + // Send a message stating the user that they are not in a guild + await context.reply(`You are ${message.author.username} and we are not in a server`, true); + } +}); +``` + +Additionally you can hydrate repeatedly if you need to fetch multiple objects at different times. + ## Interactions + First create the interactions. -```ts -bot.commands.register('test', (_client, context, _args) => { - // Add components to use interactions - const msg = new Message().setContent('Test interaction!').addComponent( - // This generations an action row which can optionaly contain content - 'This is an action row', - // Create components with helper functions - Message.button({ - label: 'Click me!', - style: ComponentStyle.Primary, - // Assign id's to reference them later - custom_id: 'click_me', - emoji: { - name: '🔥', - id: null, - animated: false - } - }) - ); + +```typescript +client.chatCommands.register('demo', async (context: Context, _args: any[]) => { + const msg = new Message() + .setContent('Hello World!') + .addComponent( + 'This is an action row', + // Max buttons in a row is 5 + // Custom ID is required and must be unique + // Non-Link buttons can not have a URL + // Link buttons can not have a custom ID + Message.button({ + label: 'Click me!', + style: ComponentStyle.Primary, + custom_id: 'button_1' + }), + Message.button({ + label: 'No me!', + style: ComponentStyle.Secondary, + custom_id: 'button_2', + disabled: true + }), + Message.button({ + label: 'Never, me!', + style: ComponentStyle.Success, + custom_id: 'button_3' + }), + // Link requires url and can not have custom_id + Message.button({ + label: 'Piss off.', + style: ComponentStyle.Link, + url: 'https://example.com' + }) + ); + context.reply(msg); }); ``` + Then provide a handler for them. -```ts -bot.interactions.register('click_me', async (client, context, interaction) => { - const response = context.reply( - `You clicked me! Thank you ${interaction.member.user.username} <3` - ); -}); -``` \ No newline at end of file + +```typescript +const buttonInteractionHandler = (context: Context, data: InteractionCommpoentData) => { + context.reply(`You clicked the ${data.custom_id} button!`); +}; + +client.interactions.register('button_1', buttonInteractionHandler); +client.interactions.register('button_2', buttonInteractionHandler); +client.interactions.register('button_3', buttonInteractionHandler); +``` diff --git a/assets/unicord.png b/assets/unicord.png new file mode 100644 index 0000000..8ea57c3 Binary files /dev/null and b/assets/unicord.png differ diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..75023fb Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index 633957f..254d560 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,38 @@ { - "name": "@jaronp/discord.ts", - "version": "0.1.1", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "author": "Jaron Pate", - "license": "Unlicense", - "repository": "https://github.com/jaronpate/discord.ts", + "name": "unicord", + "description": "A simple discord library primarily for TypeScript. REST like and easy to use.", + "version": "0.0.1", + "module": "src/index.ts", + "type": "module", + "main": "out/index.js", + "types": "out/index.d.ts", "files": [ - "/dist" + "out" ], "scripts": { - "start": "npx ts-node test/main.test.ts", - "start:watch": "npx ts-node-dev --respawn test/main.test.ts", - "build": "npx tsc" + "test": "bun run testing/demo.test.ts", + "test:inspect": "bun --inspect testing/demo.test.ts", + + "build": "bun run build:types && bun run build:core", + "build:packages": "bun run build:types && bun run build:core -- --packages external", + "build:nocheck": "bun run build:types:nocheck && bun run build:core", + + "build:core": "bun build src/index.ts --outdir out --drop=console --drop=debugger", + "build:types": "tsc --emitDeclarationOnly --project tsconfig.json", + "build:types:nocheck": "bun run build:types -- --noCheck", + + "build:watch": "concurrently \"bun run build:types -- --watch\" \"bun run build:core -- --watch\"", + "build:packages:watch": "concurrently \"bun run build:types -- --watch\" \"bun run build:core -- --packages external --watch\"" }, "devDependencies": { - "@types/ws": "^8.5.3", - "dotenv": "^16.0.2", - "ts-node-dev": "^2.0.0", - "typescript": "^4.8.3" + "@types/bun": "latest", + "commander": "^13.0.0", + "concurrently": "^9.1.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" }, "dependencies": { - "axios": "^0.27.2", - "ws": "^8.8.1" + "ws": "^8.18.0" } -} +} \ No newline at end of file diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..7657a41 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,92 @@ +#!/usr/bin/env bun +/** + * Not used at the moment, was just playing around with the idea of using the bun JavaScript API to build the project. + */ +import { Command } from 'commander'; +import { build, type BuildConfig } from 'bun'; +import concurrently from 'concurrently'; + +// Initialize Commander to handle CLI commands and options +const program = new Command(); + +// Paths and base TypeScript compilation command +const srcFile = 'src/index.ts'; +const outDir = 'out'; +const tscBaseCmd = 'tsc --emitDeclarationOnly --project tsconfig.json'; + +/** + * Handles the build process with optional watch and type emission. + * + * @param options - Object containing flags for watch, packages, noCheck, and typesOnly. + */ +async function runBuild(options: { + watch?: boolean; + packages?: boolean; + noCheck?: boolean; + typesOnly?: boolean; +}) { + // Handle TypeScript-only builds (e.g., for emitting declaration files without bundling) + if (options.typesOnly) { + // Construct the TypeScript command dynamically based on flags + const tscCmd = `${tscBaseCmd}${options.noCheck ? ' --noCheck' : ''}${options.watch ? ' --watch' : ''}`; + + // Run TypeScript compilation (watch mode if specified) + await Bun.spawn(tscCmd.split(' ')).exited; + return; // Exit early since no Bun build is needed for types-only mode + } + + // Define options for the Bun build process + const buildOptions: BuildConfig = { + entrypoints: [srcFile], + outdir: outDir, + drop: ['console', 'debugger'], // Strip console and debugger statements + packages: options.packages ? 'external' : undefined + }; + + // Handle watch mode: run both TypeScript and Bun build concurrently + if (options.watch) { + concurrently([ + { + // Run TypeScript watch for continuous type emission + command: `${tscBaseCmd} --watch${options.noCheck ? ' --noCheck' : ''}`, + name: 'tsc', // Label the TypeScript process for logging + }, + { + // Run Bun build in watch mode + command: `bun build ${srcFile} --outdir ${outDir} ${ + options.packages ? '--packages external' : '' + } ${options.noCheck ? '--noCheck' : ''} --watch`, + name: 'bun', // Label the Bun process for logging + }, + ], { + + }); + } else { + // Non-watch mode: Run TypeScript once, then trigger Bun build + await Bun.spawn(tscBaseCmd.split(' ')).exited; + + // Perform a standard Bun build + await build(buildOptions); + console.log(`Build completed.`); + } +} + +// Register the build command and attach optional flags +program + .command('build') + .description('Build project with options') + .option('--watch', 'Watch for changes and rebuild') + .option('--packages', 'Bundle external packages') + .option('--nocheck', 'Skip type checking') + .option('--types-only', 'Emit types without compiling the project') + .action((opts) => { + runBuild({ + watch: opts.watch, + packages: opts.packages, + noCheck: opts.nocheck, + typesOnly: opts.typesOnly, + }); + }); + +// Parse and execute CLI input +program.parse(); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5b3ad2b..0bfe8f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,177 +1,18 @@ -import { ComponentStyle, Context, Gateway, Guilds, Message, Embed } from './lib'; -import { SlashCommand, SlashCommandFunction, ChatCommand, Command, CommandFunction, Intent, Presence } from './types'; -export { - ComponentStyle, - Context, - Gateway, - Guilds, - Message, - Embed, - Command, - CommandFunction, - ChatCommand, - Intent, - Presence -}; - -type Config = { - token: string; - application_id: string; - prefix?: string; - intents?: Intent[]; -}; - -export class Client { - readonly prefix: string; - readonly token: string; - readonly application_id: string; - private readonly gateway: Gateway; - private readonly client_commands: Map = new Map(); - private readonly slash_commands: Map = new Map(); - readonly guilds: Guilds; - - constructor({ token, application_id, prefix, intents }: Config) { - this.token = token; - this.application_id = application_id; - this.prefix = prefix ?? '!'; - this.gateway = new Gateway({ token, intents, client: this }); - this.guilds = new Guilds({ token, gateway: this.gateway }); - // Do some default stuff - this.initialize(); - } - - get user() { - return this.gateway.user; - } - - get handlers() { - return this.gateway.handlers; - } - - get interactions() { - return this.gateway.interactions; - } - - get api() { - return this.gateway.api; - } - - public connect = () => { - this.gateway.connect(); - return this; - }; - - public reconnect = () => { - this.gateway.reconnect(); - return this; - }; - - public setPresence = (presence: Presence) => { - this.gateway.setPresence(presence); - return this; - }; - - send = (channel_id: string, message: Message | string) => { - if (typeof message === 'string') { - message = new Message().setContent(message); - } - return this.api.post(`/channels/${channel_id}/messages`, message); - }; - - public commands = { - chat: { - register: (command: ChatCommand | string, handler?: CommandFunction) => { - if (typeof command === 'string') { - this.client_commands.set(command, handler); - } else { - this.client_commands.set(command.name, command); - } - }, - get: (command_name: string) => { - return this.client_commands.get(command_name); - }, - execute: (command_name: string, context: Context, ...args: any[]) => { - const command = this.client_commands.get(command_name); - if (command) { - command instanceof ChatCommand - ? command.execute(this, context, args.slice(1)) - : command(this, context, args.slice(1)); - } - }, - delete: (command_name: string) => { - this.client_commands.delete(command_name); - }, - list: () => { - return Array.from(this.client_commands.entries()); - }, - map: () => { - return this.client_commands; - } - }, - application: { - register: (command: SlashCommand | Record, handler?: SlashCommandFunction) => { - const payload = - command instanceof SlashCommand - ? command.toJSON() - : { - name: command.name, - description: command.description, - type: command.type ?? 1 - }; - this.api - .post(`/applications/${this.application_id}/commands`, payload) - .then((response) => { - console.log(response); - if (command instanceof SlashCommand) { - this.slash_commands.set(command.name, command); - } else { - this.slash_commands.set(command.name, handler); - } - }) - .catch((error) => { - this.log.error('Error registering application command', error); - }); - }, - get: (command_name: string) => { - return this.slash_commands.get(command_name); - }, - list: () => { - return Array.from(this.slash_commands.entries()); - }, - map: () => { - return this.slash_commands; - } - } - }; - - private log = { - error: (error_or_message: Error | string, error?: Error) => { - if (error && typeof error_or_message === 'string') { - console.error(`[discord.ts] ${error_or_message}:`, error); - } else { - console.error('[discord.ts] Error:', error_or_message); - } - } - }; - - private initialize = () => { - // Register default help command - this.commands.chat.register('help', (client, context, _args) => { - const embed = new Embed() - .setTitle('Commands') - .setDescription('This is a list of commands you can use.') - .setColor('#f6c221'); - - const commands = client.commands.chat.list(); - - for (const [name, command] of commands) { - embed.addField({ - name: name, - value: command instanceof ChatCommand ? command.description : 'No description provided' - }); - } - - context.reply(embed.toMessage()); - }); - }; -} +// Base classes +export { Client } from './services/client'; +export { Intent, Trait } from './types/common'; +// Consumable types +export type { ClientConfig } from './types/common'; +export type { Context } from './types/context'; +export { Expectation } from './types/common'; +// Consumable util classes +export { Guild } from './types/guild'; +export { Role } from './types/role'; +export { Member } from './types/member'; +export { User } from './types/user'; +export { Message, type MessagePayload, ComponentStyle, ComponentType } from './types/message'; +export { Embed } from './types/embed'; +export { Emoji } from './types/emoji'; +// Other +export { createCommandHandler, type InteractionData, type InteractionCommpoentData, type InteractionCommandData, type InteractionHandlerFunction } from './types/handler'; +export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand'; \ No newline at end of file diff --git a/src/lib/cache.ts b/src/lib/cache.ts deleted file mode 100644 index c38cefd..0000000 --- a/src/lib/cache.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Context, Gateway } from '.'; - -export class ObjectCache { - cache = new Map(); - private readonly token: string; - private readonly type: string; - private readonly gateway: Gateway; - - constructor({ token, gateway, type, events }: { token: string; gateway: Gateway; type: string; events: string[] }) { - this.token = token; - this.type = type; - this.gateway = gateway; - gateway.handlers.register(events, (_client, _context: Context, e: Record) => { - this.cache.set(e.id, e as T); - }); - } - - async fetch(object_id: string): Promise { - return this.gateway.api.get(`${this.type}/${object_id}`).then((res) => { - const object: T & { id: string } = res.data; - this.cache.set(object.id, object); - return object; - }); - } - - get(object_id: string): T | Promise { - if (this.cache.has(object_id)) { - return Promise.resolve(this.cache.get(object_id)!); - } else { - return this.fetch(object_id); - } - } - - list(): Map { - return this.cache; - } -} diff --git a/src/lib/context.ts b/src/lib/context.ts deleted file mode 100644 index 82fb4b6..0000000 --- a/src/lib/context.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Client } from '..'; -import { Message } from '../lib'; - -export class Context { - private readonly client: Client; - // Public properties - message_id: string; - channel_id: string; - guild_id: string; - - constructor({ client, message }: { client: Client; message: Partial }) { - this.client = client; - this.message_id = message.id; - this.channel_id = message.channel_id; - this.guild_id = message.guild_id; - } - - get guild() { - return this.client.guilds.get(this.guild_id); - } - - /** - * Reply to the message that this command - * @param message The message to send to the channel - * @param reference If true the message will be a reference the the message that triggered the command - * @returns The message that was sent - */ - public reply = (message: Message | string, reference: boolean = false) => { - if (typeof message === 'string') { - message = new Message().setContent(message); - } - if (reference) { - message.message_reference = { - message_id: this.message_id, - channel_id: this.channel_id, - guild_id: this.guild_id - }; - } - return this.client.send(this.channel_id, message); - }; - - public delete = (message_id?: string) => { - return this.client.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`); - }; - - public edit = (message: Message | string) => { - if (typeof message === 'string') { - message = new Message().setContent(message); - } - return this.client.api.patch(`/channels/${this.channel_id}/messages/${this.message_id}`, message); - }; -} diff --git a/src/lib/gateway.ts b/src/lib/gateway.ts deleted file mode 100644 index d545fa4..0000000 --- a/src/lib/gateway.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { WebSocket, Data } from 'ws'; -import axios, { AxiosInstance } from 'axios'; -import { Client } from '..'; -import { - Session, - User, - Payload, - Intent, - Handler, - HandlerFunction, - Command, - Status, - Presence, - ActivityType, - CommandFunction, - Interaction, - InteractionFunction, - SlashCommand -} from '../types'; -import { Context } from './context'; -import { InteractionContext } from './interaction'; - -export class Gateway { - socket: WebSocket; - axios: AxiosInstance; - // Config - readonly token: string; - readonly client: Client; - private intents: number = 513; - private commandNotFound: CommandFunction = null; - // Discord gateway data - private readonly url: string = 'wss://gateway.discord.gg/?v=10&encoding=json'; - private heartbeat_timeout: NodeJS.Timeout; - private heartbeat_function: NodeJS.Timer; - private heartbeat_interval: number | null = null; - private session: Session = null; - public user: User = null; - // Utils - public event_handlers: Map> = new Map(); - public interaction_handlers: Map> = new Map(); - - constructor({ - token, - intents, - client, - commandNotFound - }: { - token: string; - intents?: Intent[]; - client: Client; - commandNotFound?: CommandFunction; - }) { - // Copy config - this.token = token; - this.client = client; - this.commandNotFound = commandNotFound; - // Initialize socket - this.socket = new WebSocket(this.url); - // Initialize axios - this.axios = axios.create({ - headers: { - 'User-Agent': 'DiscordBot (discord.ts, 0.0.1)', - Authorization: `Bot ${this.token}` - }, - baseURL: 'https://discord.com/api/v10' - }); - this.axios.interceptors.response.use((response) => response.data); - // Calculate intents - if (intents) { - this.intents = intents.reduce((acc, intent) => acc | intent, 0); - } - } - - public connect = () => { - // Initialize socket - this.socket = new WebSocket(this.url); - // Register the listeners - this.socket.on('open', this.onOpen); - this.socket.on('message', this.onMessage); - // this.socket.on('close', this.onClose); - // this.socket.on('error', this.onError); - return this; - }; - - public reconnect = () => { - this.socket.terminate(); - return this.connect(); - }; - - public setPresence = (presence: Presence) => { - this.send({ - op: 3, - d: presence - }); - }; - - public handlers = { - register: (events: string | string[], handler: Handler | HandlerFunction) => { - this.registerHandler(events, handler, 'command'); - }, - get: (event: string) => { - return this.event_handlers.get(event); - } - }; - - public interactions = { - register: (custom_ids: string | string[], handler: Interaction | InteractionFunction) => { - this.registerHandler(custom_ids, handler, 'interaction'); - }, - get: (event: string) => { - return this.interaction_handlers.get(event); - } - }; - - public api = { - get: (url: string, params?: any) => { - return this.axios.get>(url, { params }); - }, - post: (url: string, data?: any) => { - return this.axios.post>(url, data); - }, - patch: (url: string, data?: any) => { - return this.axios.patch>(url, data); - }, - delete: (url: string, data?: any) => { - return this.axios.delete>(url, data); - } - }; - - // prettier-ignore - private registerHandler(events: string | string[], handler: Handler | HandlerFunction, type: 'command'): void; - // prettier-ignore - private registerHandler(events: string | string[], handler: Interaction | InteractionFunction, type: 'interaction'): void; - - private registerHandler(events: string | string[], handler: any, type: 'command' | 'interaction') { - const handler_map = type === 'command' ? this.event_handlers : this.interaction_handlers; - if (Array.isArray(events)) { - // If Array register handler for each event - for (const event of events) { - const current_handlers = handler_map.get(event) ?? []; - handler_map.set(event, [...current_handlers, handler]); - } - } else { - // Otherwise register for the single event - const current_handlers = handler_map.get(events) ?? []; - handler_map.set(events, [...current_handlers, handler]); - } - } - private send = (payload: Partial) => { - payload.s = this.session?.s ?? null; - console.log('sending: %s', payload); - this.socket.send(JSON.stringify(payload)); - }; - - private indentify = () => { - this.send({ - op: 2, - d: { - token: this.token, - intents: this.intents, - properties: { - os: process.platform, - browser: 'discord.ts', - device: 'discord.ts' - } - } - }); - }; - - private heartbeat = () => { - if (!this.heartbeat_interval) { - throw new Error('No heartbeat interval set'); - } - if (this.heartbeat_function) { - clearInterval(this.heartbeat_function); - } - if (this.heartbeat_timeout) { - clearTimeout(this.heartbeat_timeout); - } - this.heartbeat_function = setInterval(() => { - this.send({ - op: 1, - d: {} - }); - }, this.heartbeat_interval); - this.heartbeat_timeout = setTimeout(() => { - this.reconnect(); - }, this.heartbeat_interval * 2); - }; - - private initialize = (payload: Payload) => { - this.session = payload.d.session; - this.user = payload.d.user; - this.user.tag = `${this.user.username}#${this.user.discriminator}`; - // Construct presence - const status: Presence = { - status: Status.Online, - since: null, - activities: [ - { - name: payload.d.guilds.length + ' servers', - type: ActivityType.Watching, - created_at: Date.now() - } - ], - afk: false - }; - // Set presence - this.setPresence(status); - // Cache guilds? (these are not whole guilds, just guild ids) - // for (const guild of payload.d.guilds) { - // this.guilds.set(guild.id, guild); - // } - console.log('session: %s', this.session); - console.log('user: %s', this.user); - }; - - private parseEvent = (payload: Payload) => { - // If not a dispatch event, return - if (!payload.t) { - return; - } - // Run default handlers - switch (payload.t) { - case 'READY': - this.initialize(payload); - break; - case 'INTERACTION_CREATE': - this.handleInteractionCreate(payload); - break; - case 'MESSAGE_CREATE': - this.handleMessageCreate(payload); - break; - case 'MESSAGE_UPDATE': - this.handleMessageCreate(payload); - break; - default: - break; - } - // Call event handlers - const handlers = this.handlers.get(payload.t); - const context = new Context({ client: this.client, message: payload.d }); - if (handlers) { - for (const handler of handlers) { - handler instanceof Handler - ? handler.execute(this.client, context, payload.d) - : handler(this.client, context, payload.d); - } - } - }; - - private handleInteractionCreate = (payload: Payload) => { - if (payload.d.type === 2) { - this.handleShashInteraction(payload); - } else { - this.handleChatInteraction(payload); - } - }; - - private handleShashInteraction = (payload: Payload) => { - console.log(payload.d); - const context = new InteractionContext({ client: this.client, interaction: payload.d }); - const handler = this.client.commands.application.get(payload.d.data.name); - if (handler) { - if (handler instanceof SlashCommand) { - handler.execute(this.client, context, payload.d); - } else { - handler(this.client, context, payload.d); - } - } - }; - - private handleChatInteraction = (payload: Payload) => { - const context = new InteractionContext({ client: this.client, interaction: payload.d }); - const handlers = this.client.interactions.get(payload.d.data.custom_id); - if (handlers) { - for (const handler of handlers) { - if (handler instanceof Interaction) { - handler.execute(this.client, context, payload.d); - } else { - handler(this.client, context, payload.d); - } - } - } - }; - - private handleMessageCreate = async (payload: Payload) => { - // Ignore bots - if (payload.d.author.bot) { - return; - } - console.log(payload.d); - const content = payload.d.content.trim(); - // Is this a command? - if (content.startsWith(this.client.prefix) | content.startsWith(`<@${this.client.user.id}>`)) { - // Parse command - const args = content - .slice(this.client.prefix.length) - .trim() - .split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g); - // Check if command exists - const handler = this.client.commands.chat.get(args[0]); - // Generate context - const context = new Context({ client: this.client, message: payload.d }); - if (handler) { - // Execute command - handler instanceof Command - ? handler.execute(this.client, context, args.slice(1)) - : handler(this.client, context, args.slice(1)); - } else { - let reply = null; - // Send error message if command does not exist - if (this.commandNotFound) { - // If custom command not found handler is set, use that - reply = this.commandNotFound(this.client, context); - } else { - // Otherwise send default message - reply = await context.reply('Command not found :(', true); - } - if (reply) { - // If reply is set, delete it after 3 seconds - setTimeout(() => { - context.delete(reply.id); - }, 3000); - } - } - } - }; - - private parseOpCode = (payload: Payload) => { - switch (payload.op) { - case 0: - this.parseEvent(payload); - break; - case 9: - setTimeout(this.reconnect, 500); - break; - case 10: - this.heartbeat_interval = payload.d.heartbeat_interval; - this.heartbeat(); - this.indentify(); - break; - case 11: - console.log('heartbeat acknowledged'); - this.heartbeat_timeout.refresh(); - break; - default: - break; - } - }; - - private onMessage = (packet: Data): void => { - console.log('============================'); - let data: Payload | null = null; - try { - data = JSON.parse(packet.toString()); - } catch (e) { - console.error(e); - } - if (data) { - console.log('received: %s', data); - console.log('received data: %s', data.d); - if (this.session && data.s && data.s > this.session.s) { - this.session.s = data.s; - } else { - this.parseOpCode(data); - } - } - }; - - private onOpen = () => { - console.log('Socket open'); - }; -} diff --git a/src/lib/guilds.ts b/src/lib/guilds.ts deleted file mode 100644 index d587741..0000000 --- a/src/lib/guilds.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Gateway, ObjectCache } from "."; -import { Guild } from "../types"; - -export class Guilds extends ObjectCache { - constructor ({ token, gateway }: { token: string, gateway: Gateway }) { - super({token, gateway, type: 'guilds', events: ['GUILD_CREATE', 'GUILD_UPDATE']}); - } -} \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 7075c9c..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './gateway'; -export * from './cache'; -export * from './guilds'; -export * from './context'; -export * from './message'; -export * from './embed'; -export * from './interaction'; diff --git a/src/lib/interaction.ts b/src/lib/interaction.ts deleted file mode 100644 index 3bdb062..0000000 --- a/src/lib/interaction.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Client } from ".."; -import { InteractionResponseType } from "../types"; -import { Message } from "./message"; - -export class InteractionContext { - public client: Client; - public interaction: Record; - private is_loading: boolean; - constructor({ client, interaction, is_loading = false }: { client: Client; interaction: Record, is_loading?: boolean }) { - this.client = client; - this.interaction = interaction; - this.is_loading = is_loading; - } - - ack = (type: InteractionResponseType = InteractionResponseType.Pong, data?: Record) => { - return this.client.api.post(`/interactions/${this.interaction.id}/${this.interaction.token}/callback`, { - type, - data - }); - }; - - reply = (message: Message | string) => { - if (typeof message === 'string') { - message = new Message().setContent(message); - } - return this.ack(InteractionResponseType.Reply, { - ...message - }); - }; - - edit = (message: Message | string) => { - if (typeof message === 'string') { - message = new Message().setContent(message); - } - if (!this.is_loading) { - return this.ack(InteractionResponseType.UpdateMessage, { - ...message - }); - } else { - return this.client.api.patch( - `/webhooks/${this.client.application_id}/${this.interaction.token}/messages/@original`, - { - type: InteractionResponseType.DeferredUpdateMessage, - ...message - } - ); - } - - }; - - deleteOriginal = () => { - return this.client.api.delete( - `/webhooks/${this.client.application_id}/${this.interaction.token}/messages/@original` - ); - }; - - loading = async () => { - await this.ack(InteractionResponseType.DeferredReply); - return new InteractionContext({ client: this.client, interaction: this.interaction, is_loading: true }); - }; -} diff --git a/src/lib/message.ts b/src/lib/message.ts deleted file mode 100644 index df74202..0000000 --- a/src/lib/message.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Embed } from '.'; - -export type Component = Partial