mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8b85e60ec | |||
| 8333ab2d44 | |||
| 1aa0fbb9e2 | |||
| be82580f90 | |||
| 63755fe17a | |||
| 93b7fe2ed5 | |||
| 3e2b0f24bf | |||
| a57cfc6528 | |||
| 0dbd14da04 | |||
| 8811dab6ec | |||
| 486387e905 | |||
| e6e4fd1a02 | |||
| 8943b5a06e | |||
| 202255d4ff | |||
| fc7f6548a4 | |||
| 2f20325c03 | |||
| 7a31b09a0d | |||
| 57d86e0e1c | |||
| 3bd0a0899b | |||
| 5f038026d3 | |||
| e85083035e | |||
| 13b25ca7d2 | |||
| fbdff25c14 | |||
| 7848ea0082 | |||
| 119ddfefd1 | |||
| f3b1546d54 | |||
| 5b1fa15e66 | |||
| f419e82579 | |||
| 17646e7f36 | |||
| 56e10f043a | |||
| 57a787e7ec | |||
| 230aee97fa | |||
| a346d56f9b | |||
| 72483a5070 |
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: CI (template)
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
@@ -4,14 +4,15 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
# - run: yarn test
|
||||
- run: yarn publish --access public
|
||||
# Pull repo
|
||||
- uses: actions/checkout@v2
|
||||
# Install bun
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
# Run tests
|
||||
# TODO: Add tests
|
||||
# Build
|
||||
- run: bun build
|
||||
# Publish
|
||||
- run: bun publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/_authToken=${{ secrets.NPM_TOKEN }}
|
||||
|
||||
+174
-2
@@ -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
|
||||
.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
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,22 +1,33 @@
|
||||
<div align="center">
|
||||
<h2>discord.ts</h2>
|
||||
<p>Build good bots fast</p>
|
||||
<img src="https://raw.githubusercontent.com/jaronpate/unicord/master/assets/unicord.png" width="150" height="150">
|
||||
<h2>unicord</h2>
|
||||
<p><em>Build good bots fast</em></p>
|
||||
</div>
|
||||
|
||||
> 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.
|
||||
|
||||
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: '<token>',
|
||||
application_id: '<application_id>',
|
||||
intents: [
|
||||
@@ -24,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`
|
||||
);
|
||||
});
|
||||
|
||||
```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);
|
||||
```
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1012 KiB |
+28
-17
@@ -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. Web server like and easy to use.",
|
||||
"version": "0.0.4",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"main": "out/index.js",
|
||||
"types": "out/index.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
"out"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "npx ts-node-dev src/index.ts",
|
||||
"start:watch": "npx ts-node-dev --respawn src/index.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=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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
+18
-89
@@ -1,89 +1,18 @@
|
||||
import { ComponentStyle, Context, Gateway, Guilds, Message } from './lib';
|
||||
import { Command, CommandFuntion, Intent, Presence } from './types';
|
||||
export { ComponentStyle, Context, Gateway, Guilds, Message, Command, CommandFuntion, 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<string, Command | CommandFuntion> = 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 });
|
||||
}
|
||||
|
||||
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 = {
|
||||
register: (command: Command | string, handler: CommandFuntion) => {
|
||||
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 Command
|
||||
? command.execute(this, context, args.slice(1))
|
||||
: command(this, context, args.slice(1));
|
||||
}
|
||||
},
|
||||
delete: (command_name: string) => {
|
||||
this.client_commands.delete(command_name);
|
||||
}
|
||||
};
|
||||
}
|
||||
// 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';
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Context, Gateway } from '.';
|
||||
|
||||
export class ObjectCache<T> {
|
||||
cache = new Map<string, T>();
|
||||
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<string, any>) => {
|
||||
this.cache.set(e.id, e as T);
|
||||
});
|
||||
}
|
||||
|
||||
async fetch(object_id: string): Promise<T> {
|
||||
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<T> {
|
||||
if (this.cache.has(object_id)) {
|
||||
return Promise.resolve(this.cache.get(object_id)!);
|
||||
} else {
|
||||
return this.fetch(object_id);
|
||||
}
|
||||
}
|
||||
|
||||
list(): Map<string, T> {
|
||||
return this.cache;
|
||||
}
|
||||
}
|
||||
@@ -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<Message> }) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -1,337 +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,
|
||||
CommandFuntion,
|
||||
Interaction,
|
||||
InteractionFunction,
|
||||
InteractionContext
|
||||
} from '../types';
|
||||
import { Context } from './context';
|
||||
|
||||
export class Gateway {
|
||||
socket: WebSocket;
|
||||
axios: AxiosInstance;
|
||||
// Config
|
||||
readonly token: string;
|
||||
readonly client: Client;
|
||||
private intents: number = 513;
|
||||
private commandNotFound: CommandFuntion = null;
|
||||
// Discord gateway data
|
||||
private readonly url: string = 'wss://gateway.discord.gg/?v=10&encoding=json';
|
||||
private heartbeat_interval: number | null = null;
|
||||
private session: Session = null;
|
||||
public user: User = null;
|
||||
// Utils
|
||||
public event_handlers: Map<string, Array<Handler | HandlerFunction>> = new Map();
|
||||
public interaction_handlers: Map<string, Array<Interaction | InteractionFunction>> = new Map();
|
||||
|
||||
constructor({
|
||||
token,
|
||||
intents,
|
||||
client,
|
||||
commandNotFound
|
||||
}: {
|
||||
token: string;
|
||||
intents?: Intent[];
|
||||
client: Client;
|
||||
commandNotFound?: CommandFuntion;
|
||||
}) {
|
||||
// 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 = () => {
|
||||
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<unknown, Record<string, any>>(url, { params });
|
||||
},
|
||||
post: (url: string, data?: any) => {
|
||||
return this.axios.post<unknown, Record<string, any>>(url, data);
|
||||
},
|
||||
patch: (url: string, data?: any) => {
|
||||
return this.axios.patch<unknown, Record<string, any>>(url, data);
|
||||
},
|
||||
delete: (url: string, data?: any) => {
|
||||
return this.axios.delete<unknown, Record<string, any>>(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>) => {
|
||||
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');
|
||||
}
|
||||
setInterval(() => {
|
||||
this.send({
|
||||
op: 1,
|
||||
d: {}
|
||||
});
|
||||
}, this.heartbeat_interval);
|
||||
};
|
||||
|
||||
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) => {
|
||||
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);
|
||||
// Is this a command?
|
||||
if (payload.d.content.startsWith(this.client.prefix)) {
|
||||
// Parse command
|
||||
const args = payload.d.content
|
||||
.slice(this.client.prefix.length)
|
||||
.trim()
|
||||
.split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g);
|
||||
// Check if command exists
|
||||
const handler = this.client.commands.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');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onMessage = (packet: Data): void => {
|
||||
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: %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');
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Gateway, ObjectCache } from ".";
|
||||
import { Guild } from "../types";
|
||||
|
||||
export class Guilds extends ObjectCache<Guild> {
|
||||
constructor ({ token, gateway }: { token: string, gateway: Gateway }) {
|
||||
super({token, gateway, type: 'guilds', events: ['GUILD_CREATE', 'GUILD_UPDATE']});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from './gateway';
|
||||
export * from './cache';
|
||||
export * from './guilds';
|
||||
export * from './context';
|
||||
export * from './message';
|
||||
@@ -1,153 +0,0 @@
|
||||
export type Embed = {
|
||||
|
||||
}
|
||||
|
||||
export type Component = Partial<Button | SelectMenu> & {
|
||||
type: ComponentType;
|
||||
content?: string;
|
||||
components?: Component[];
|
||||
};
|
||||
|
||||
// {
|
||||
// content?: string;
|
||||
// type?: ComponentType;
|
||||
// description?: string;
|
||||
// label?: string;
|
||||
// value?: string;
|
||||
// style?: ComponentStyle;
|
||||
// custom_id?: string;
|
||||
// url?: string;
|
||||
// disabled?: boolean;
|
||||
// required?: boolean;
|
||||
// emoji?: {
|
||||
// name: string;
|
||||
// id?: string;
|
||||
// animated?: boolean;
|
||||
// }
|
||||
// options?: Component[];
|
||||
// placeholder?: string;
|
||||
// min_values?: number;
|
||||
// max_values?: number;
|
||||
// components?: Component[];
|
||||
// }
|
||||
|
||||
export type Emoji = {
|
||||
name: string | null;
|
||||
id: string | null;
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
export type Button = {
|
||||
style: ComponentStyle;
|
||||
label: string;
|
||||
emoji?: Emoji
|
||||
custom_id?: string;
|
||||
url?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type SelectMenu = {
|
||||
custom_id: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
emoji?: Emoji
|
||||
default?: boolean;
|
||||
}[];
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
}
|
||||
|
||||
export enum ComponentStyle {
|
||||
Primary = 1,
|
||||
Secondary,
|
||||
Success,
|
||||
Danger,
|
||||
Link
|
||||
}
|
||||
|
||||
export enum ComponentType {
|
||||
ActionRow = 1,
|
||||
Button,
|
||||
SelectMenu,
|
||||
TextInput
|
||||
}
|
||||
|
||||
export class Message {
|
||||
id?: string;
|
||||
channel_id?: string;
|
||||
guild_id?: string;
|
||||
public content?: string;
|
||||
public embeds: Embed[] = [];
|
||||
public components: Component[] = [];
|
||||
public message_reference?: {
|
||||
message_id?: string;
|
||||
channel_id?: string;
|
||||
guild_id?: string;
|
||||
fail_if_not_exists?: boolean;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
public static button(config: Button): Component {
|
||||
return {
|
||||
type: ComponentType.Button,
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
public static selectMenu(config: SelectMenu): Component {
|
||||
return {
|
||||
type: ComponentType.SelectMenu,
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
public setContent = (content: string) => {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public addEmbed = (embed: Embed) => {
|
||||
this.embeds.push(embed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public addComponent = (content: Component | string, ...components: Component[]) => {
|
||||
if (typeof content === 'string') {
|
||||
console.log('string');
|
||||
this.components.push({
|
||||
type: ComponentType.ActionRow,
|
||||
content,
|
||||
components
|
||||
});
|
||||
} else {
|
||||
this.components.push({
|
||||
type: ComponentType.ActionRow,
|
||||
components: [content, ...components]
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public setReference = (message: Partial<Message>) => {
|
||||
this.message_reference = {
|
||||
message_id: message.id,
|
||||
channel_id: message.channel_id,
|
||||
guild_id: message.guild_id,
|
||||
fail_if_not_exists: false
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public toJSON = () => {
|
||||
return {
|
||||
content: this.content,
|
||||
embeds: this.embeds,
|
||||
components: this.components,
|
||||
message_reference: this.message_reference
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { exists, log } from "../utils";
|
||||
import type { Client } from "./client";
|
||||
|
||||
export class API {
|
||||
private readonly base_url: string = 'https://discord.com/api/v10';
|
||||
|
||||
constructor(private client: Client) {}
|
||||
|
||||
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
|
||||
const url = `${this.base_url}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||
const headers = {
|
||||
Authorization: `Bot ${this.client.token}`,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// console.error(`Failed to fetch: ${response.statusText} ${response.url} ${response.status}`);
|
||||
console.log('Failed Request Response: ', await response.text());
|
||||
throw new Error(`Failed to fetch: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
|
||||
// console.log(`API Request: ${method} ${url}`);
|
||||
// console.log(`API Body: ${JSON.stringify(data, null, 4)}`);
|
||||
// console.log('API Response:', JSON.stringify(body, null, 4));
|
||||
|
||||
// Extract rate limit headers
|
||||
const remaining = response.headers.get('X-RateLimit-Remaining');
|
||||
const limit = response.headers.get('X-RateLimit-Limit');
|
||||
const reset = response.headers.get('X-RateLimit-Reset');
|
||||
const resetSeconds = response.headers.get('X-RateLimit-Reset-After');
|
||||
const limitBucket = response.headers.get('X-RateLimit-Bucket');
|
||||
|
||||
// Log rate limit info for debugging
|
||||
log(`Rate Limit: ${remaining}/${limit} - Reset: ${reset} (${resetSeconds}s) - Bucket: ${limitBucket}`);
|
||||
|
||||
// If status is 429, we are being rate limited
|
||||
if (response.status === 429) {
|
||||
// Look for the retry-after header
|
||||
const retryAfter = response.headers.get('Retry-After');
|
||||
if (exists(retryAfter)) {
|
||||
// Wait for the retry-after time
|
||||
await new Promise(resolve => setTimeout(resolve, parseInt(retryAfter) * 1000));
|
||||
} else {
|
||||
// TODO: Handle this case better
|
||||
// Maybe wait some arbitrary time??
|
||||
throw new Error('Rate limited without Retry-After header');
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
async get<T = any>(path: string): Promise<T> {
|
||||
return this.request('GET', path);
|
||||
}
|
||||
|
||||
async post<T = any>(path: string, data: any): Promise<T> {
|
||||
return this.request('POST', path, data);
|
||||
}
|
||||
|
||||
async patch<T = any>(path: string, data: any): Promise<T> {
|
||||
return this.request('PATCH', path, data);
|
||||
}
|
||||
|
||||
async delete<T = any>(path: string): Promise<T> {
|
||||
return this.request('DELETE', path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Context } from '../types/context';
|
||||
|
||||
const EventBus = new EventEmitter() as Emitter;
|
||||
|
||||
export type Emitter = EventEmitter & {
|
||||
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean
|
||||
on: (event_name: string, listener: Function) => Emitter
|
||||
}
|
||||
|
||||
export { EventBus };
|
||||
@@ -0,0 +1,71 @@
|
||||
import { fromDiscord } from "../../types/common";
|
||||
import type { Context } from "../../types/context";
|
||||
import type { API } from "../api";
|
||||
import type { Processor } from "../processor";
|
||||
|
||||
|
||||
export class ObjectCache<T, K extends T> {
|
||||
cache = new Map<string, K>();
|
||||
requests = new Map<string, Promise<K>>();
|
||||
readonly type: string;
|
||||
|
||||
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[]) {
|
||||
this.type = type;
|
||||
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
|
||||
this.cache.set(e.id, factory[fromDiscord](e));
|
||||
});
|
||||
}
|
||||
|
||||
set(object_id: string, object: K): void {
|
||||
this.cache.set(object_id, object);
|
||||
}
|
||||
|
||||
// update(object_id: string, object: Partial<T>): void {
|
||||
// const existing = this.cache.get(object_id);
|
||||
|
||||
// if (exists(existing)) {
|
||||
// // TODO: Idk wtf happens if this is called on two classes
|
||||
// this.cache.set(object_id, { ...existing, ...object });
|
||||
// } else {
|
||||
// this.cache.set(object_id, object as T);
|
||||
// }
|
||||
// }
|
||||
|
||||
async fetch(object_id: string, ..._args: any[]): Promise<K> {
|
||||
if (this.requests.has(object_id)) {
|
||||
return this.requests.get(object_id)!;
|
||||
// } else if (this.cache.has(object_id)) {
|
||||
// return Promise.resolve(this.cache.get(object_id)!);
|
||||
} else {
|
||||
// Dispatch the request
|
||||
const req = this.api.get(`/${this.type}/${object_id}`).then((data) => {
|
||||
// Create the object
|
||||
const object = this.factory[fromDiscord](data);
|
||||
// Save the object
|
||||
this.cache.set(object_id, object);
|
||||
// Return the object
|
||||
return object;
|
||||
});
|
||||
// Save the request
|
||||
this.requests.set(object_id, req);
|
||||
// Remove the request from the cache if it succeeds
|
||||
req.then(() => this.requests.delete(object_id));
|
||||
// Remove the request from the cache if it fails
|
||||
req.catch(() => this.requests.delete(object_id));
|
||||
// Return the request
|
||||
return req;
|
||||
}
|
||||
}
|
||||
|
||||
async get(object_id: string, ..._args: any[]): Promise<K> {
|
||||
if (this.cache.has(object_id)) {
|
||||
return await Promise.resolve(this.cache.get(object_id)!);
|
||||
} else {
|
||||
return this.fetch(object_id);
|
||||
}
|
||||
}
|
||||
|
||||
list(): Map<string, Promise<T> | T> {
|
||||
return this.cache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Guild } from "../../types/guild";
|
||||
import type { API } from "../api";
|
||||
import type { Processor } from "../processor";
|
||||
import { ObjectCache } from "./cache";
|
||||
|
||||
export class Guilds extends ObjectCache<Guild, Guild> {
|
||||
constructor (api: API, processor: Processor) {
|
||||
super(api, processor, 'guilds', Guild, ['GUILD_CREATE', 'GUILD_UPDATE']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { fromDiscord } from "../../types/common";
|
||||
import { Message, type MessagePayload } from "../../types/message";
|
||||
import type { API } from "../api";
|
||||
import type { Client } from "../client";
|
||||
import type { Processor } from "../processor";
|
||||
import { ObjectCache } from "./cache";
|
||||
|
||||
|
||||
export class Messages extends ObjectCache<Message, MessagePayload> {
|
||||
constructor (protected client: Client, api: API, processor: Processor) {
|
||||
super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
|
||||
}
|
||||
|
||||
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
|
||||
if (this.requests.has(message_id)) {
|
||||
return this.requests.get(message_id)!;
|
||||
} else {
|
||||
// Dispatch the request
|
||||
const req = this.api.get(`/channels/${channel_id}/messages/${message_id}`).then((data) => {
|
||||
const message = Message[fromDiscord](data);
|
||||
this.cache.set(message_id, message);
|
||||
return message;
|
||||
});
|
||||
// Save the request
|
||||
this.requests.set(message_id, req);
|
||||
// Remove the request from the cache if it succeeds
|
||||
req.then(() => this.requests.delete(message_id));
|
||||
// Remove the request from the cache if it fails
|
||||
req.catch(() => this.requests.delete(message_id));
|
||||
// Return the request
|
||||
return req;
|
||||
}
|
||||
}
|
||||
|
||||
async get(channel_id: string, message_id: string): Promise<MessagePayload> {
|
||||
if (this.cache.has(message_id)) {
|
||||
return await Promise.resolve(this.cache.get(message_id)!);
|
||||
} else {
|
||||
return this.fetch(channel_id, message_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { User } from "../../types/user";
|
||||
import type { API } from "../api";
|
||||
import type { Processor } from "../processor";
|
||||
import { ObjectCache } from "./cache";
|
||||
|
||||
|
||||
export class Users extends ObjectCache<User, User> {
|
||||
constructor (api: API, processor: Processor) {
|
||||
super(api, processor, 'users', User, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { API } from './api';
|
||||
import { Gateway } from './connectors/gateway';
|
||||
import { Processor } from './processor';
|
||||
import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
|
||||
import { exists, isNil } from '../utils/index';
|
||||
import { type DiscordMessage, Message } from '../types/message';
|
||||
import { Guilds } from './caches/guilds';
|
||||
import { Messages } from './caches/messages';
|
||||
import { Users } from './caches/users';
|
||||
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||
import { EventBus, type Emitter } from './bus';
|
||||
|
||||
export class Client {
|
||||
readonly token: string;
|
||||
readonly application_id: string;
|
||||
readonly intents: Intent[] = [Intent.DEFAULT];
|
||||
prefix: string;
|
||||
// Internal services
|
||||
protected bus: Emitter;
|
||||
protected api: API;
|
||||
protected processor: Processor;
|
||||
protected gateway: Gateway;
|
||||
guilds: Guilds;
|
||||
users: Users;
|
||||
messages: Messages;
|
||||
chatCommands: Processor[HandlerType.ChatCommands];
|
||||
applicationCommands: Processor[HandlerType.ApplicationCommands];
|
||||
interactions: Processor[HandlerType.Interactions];
|
||||
|
||||
constructor(config: ClientConfig) {
|
||||
// Validate the configuration
|
||||
Client.validateConfig(config);
|
||||
// Save the configuration
|
||||
this.token = config.token;
|
||||
this.application_id = config.application_id;
|
||||
this.intents = config.intents ?? this.intents;
|
||||
this.prefix = config.prefix ?? '!';
|
||||
// Initialize internal services
|
||||
this.bus = EventBus;
|
||||
this.api = new API(this);
|
||||
this.processor = new Processor(this, this.api, this.bus);
|
||||
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
||||
this.guilds = new Guilds(this.api, this.processor);
|
||||
this.users = new Users(this.api, this.processor);
|
||||
this.messages = new Messages(this, this.api, this.processor);
|
||||
// Save references to command and event handlers
|
||||
this.chatCommands = this.processor[HandlerType.ChatCommands];
|
||||
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
|
||||
this.interactions = this.processor[HandlerType.Interactions];
|
||||
}
|
||||
|
||||
private static validateConfig(config: ClientConfig) {
|
||||
if (isNil(config.token)) {
|
||||
throw new Error('Token is required');
|
||||
}
|
||||
|
||||
if (isNil(config.application_id)) {
|
||||
throw new Error('Application ID is required');
|
||||
}
|
||||
|
||||
if (exists(config.prefix) && typeof config.prefix !== 'string') {
|
||||
throw new Error('Prefix must be a string');
|
||||
}
|
||||
|
||||
if (exists(config.intents)) {
|
||||
if (!Array.isArray(config.intents)) {
|
||||
throw new Error('Intents must be an array');
|
||||
}
|
||||
|
||||
for (const intent of config.intents) {
|
||||
if (!Intent[intent]) {
|
||||
throw new Error(`Invalid intent: ${intent}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get self() {
|
||||
return this.gateway.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrates a given data object based on the provided expectations.
|
||||
*
|
||||
* @template T - The type of the data object to be hydrated. Must extend `Hydrateable`.
|
||||
* @template K - An array of `Expectation` types that specify what properties should be hydrated.
|
||||
*
|
||||
* @param data - The data object to be hydrated. Can be of type `Context` or `Message`.
|
||||
* @param expectations - An array of expectations that specify what properties should be hydrated.
|
||||
*
|
||||
* @returns A promise that resolves to the hydrated data object.
|
||||
*
|
||||
* @throws Will throw an error if an expectation cannot be resolved.
|
||||
*/
|
||||
hydrate = async <T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>, K extends Array<Expectation>>(data: T, expectations: K) => {
|
||||
return hydrate<T, K>(data, expectations, this, this.api);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper function that attempts to hydrate a given data object and returns a type guard function.
|
||||
*
|
||||
* @template T - The type of the data object to be hydrated. Must extend `Hydrateable`.
|
||||
* @template K - An array of `Expectation` types that specify what properties should be hydrated.
|
||||
*
|
||||
* @param data - The data object to be hydrated. Can be of type `Context` or `Message`.
|
||||
* @param expectations - An array of expectations that specify what properties should be hydrated.
|
||||
*
|
||||
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
|
||||
*/
|
||||
hydrator = async<T extends Hydrateable, K extends Expectation[]>(data: T, expectations: K) => {
|
||||
return hydrator<T, K>(data, expectations, this, this.api);
|
||||
}
|
||||
|
||||
sendMessage = async (channel_id: string, message: Message | string) => {
|
||||
if (typeof message === 'string') {
|
||||
message = new Message().setContent(message);
|
||||
}
|
||||
|
||||
return Message.fromDiscord(await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()));
|
||||
}
|
||||
|
||||
// chatCommands = {
|
||||
// register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
|
||||
// unregister: (name: string | string[]) => this.processor.chat_commands.unregister(name),
|
||||
// has: (name: string) => this.processor.chat_commands.has(name)
|
||||
// }
|
||||
|
||||
// applicationCommands = {
|
||||
// register: (name: string, type: ApplicationCommandType, handler: Handler) => this.processor.application_commands.register(name, type, handler),
|
||||
// unregister: (name: string) => this.processor.application_commands.unregister(name),
|
||||
// has: (name: string) => this.processor.application_commands.has(name)
|
||||
// }
|
||||
|
||||
on = (event: string, handler: (...args: any[]) => void) => this.bus.on(event, handler);
|
||||
|
||||
once = (event: string, handler: (...args: any[]) => void) => this.bus.once(event, handler);
|
||||
|
||||
off = (event: string, handler: (...args: any[]) => void) => this.bus.off(event, handler);
|
||||
|
||||
connect = () => this.gateway.connect();
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import { WebSocket, type Data } from 'ws';
|
||||
import type { Client } from "../client";
|
||||
import { fromDiscord, HandlerType, type Payload } from "../../types/common";
|
||||
import { exists, isNil, isObject, log } from "../../utils";
|
||||
import type { API } from "../api";
|
||||
import type { Processor } from '../processor';
|
||||
import { Context } from '../../types/context';
|
||||
import { type DiscordMessage, Message } from '../../types/message';
|
||||
import { DiscordUser, User } from '../../types/user';
|
||||
import { Guild } from '../../types/guild';
|
||||
import type { Emitter } from '../bus';
|
||||
import type { InteractionCommandData, InteractionCommpoentData, InteractionPayload } from '../../types/handler';
|
||||
|
||||
export class Gateway {
|
||||
private socket: WebSocket | null = null;
|
||||
private heartbeat_interval: number | null = null;
|
||||
private heartbeat_timeout: Timer | null = null;
|
||||
private sequence: number | null = null;
|
||||
// Internal state
|
||||
user: User | null = null;
|
||||
|
||||
constructor(private client: Client, private processor: Processor, private api: API, private bus: Emitter) {}
|
||||
|
||||
private onOpen = () => {
|
||||
log('Connected to gateway');
|
||||
}
|
||||
|
||||
private onMessage = async (packet: Data) => {
|
||||
// Allocate a variable to store the parsed payload
|
||||
let data: Payload | null = null;
|
||||
|
||||
try {
|
||||
// Attempt to parse the packet data
|
||||
data = JSON.parse(packet.toString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (exists(data)) {
|
||||
// NOTE: Uncomment for debugging recieved event data
|
||||
// log('='.repeat(25));
|
||||
// log('received: ', data.op);
|
||||
// log('received event: ', data.t ?? 'no event');
|
||||
// log('received data: ', exists(data.d) ? Object.keys(data.d) : 'no data');
|
||||
// log('='.repeat(25));
|
||||
// If the data is successfully parsed check the opcode
|
||||
const code = data.op;
|
||||
if (code === 0) {
|
||||
// Event received
|
||||
await this.parseEvent(data);
|
||||
} else if (code === 1) {
|
||||
// Heartbeat requested
|
||||
this.send({ op: 1, d: this.sequence });
|
||||
} else if (code === 9) {
|
||||
// Reconnect requested
|
||||
} else if (code === 10) {
|
||||
// Hello
|
||||
if (!isObject(data.d)) {
|
||||
throw new Error('Invalid HELLO payload received');
|
||||
}
|
||||
// Store the heartbeat interval
|
||||
this.heartbeat_interval = data.d?.heartbeat_interval;
|
||||
// Kick off the heartbeat loop
|
||||
this.heartbeat();
|
||||
// Identify with the gateway
|
||||
// TODO: Is there a way identify without the timeout? Or waiting for the heartbeat?
|
||||
// We need to wait a bit before sending the identify payload so that the heartbeat can be established
|
||||
setTimeout(this.indentify, 2500);
|
||||
} else if (code === 11) {
|
||||
// Heartbeat ACK
|
||||
log('Heartbeat ACK');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private heartbeat() {
|
||||
if (exists(this.heartbeat_interval)) {
|
||||
// If a previous heartbeat timeout exists, clear it
|
||||
if (exists(this.heartbeat_timeout)) {
|
||||
clearTimeout(this.heartbeat_timeout);
|
||||
}
|
||||
// Set a timeout to ensure the heartbeat is sent within the interval
|
||||
this.heartbeat_timeout = setTimeout(() => {
|
||||
this.send({ op: 1, d: this.sequence });
|
||||
this.heartbeat();
|
||||
}, this.heartbeat_interval * Math.random());
|
||||
} else {
|
||||
throw new Error('Heartbeat interval not set');
|
||||
}
|
||||
}
|
||||
|
||||
protected send = (payload: Partial<Payload>) => {
|
||||
if (isNil(this.socket)) {
|
||||
throw new Error('Socket is not connected');
|
||||
}
|
||||
|
||||
// log('='.repeat(25));
|
||||
// log('sending: ', JSON.stringify(payload, null, 4));
|
||||
// log('='.repeat(25));
|
||||
|
||||
this.socket.send(JSON.stringify(payload));
|
||||
};
|
||||
|
||||
private indentify = () => {
|
||||
this.send({
|
||||
op: 2,
|
||||
d: {
|
||||
token: this.client.token,
|
||||
intents: this.client.intents.reduce((acc, intent) => acc | intent, 0),
|
||||
properties: {
|
||||
os: process.platform,
|
||||
browser: 'unicord',
|
||||
device: 'unicord'
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private async parseEvent(payload: Payload) {
|
||||
if (isNil(payload.t)) {
|
||||
throw new Error('Event name is required');
|
||||
}
|
||||
|
||||
// log('Event:', payload.t);
|
||||
|
||||
// Extract the event name and data
|
||||
const event_name = payload.t;
|
||||
const event_data = payload.d;
|
||||
|
||||
// Simple validation on the event data
|
||||
if (!isObject(event_data)) {
|
||||
throw new Error('Invalid event data received');
|
||||
}
|
||||
|
||||
// Check if this is a known event
|
||||
if (event_name === 'READY') {
|
||||
this.user = User[fromDiscord](DiscordUser.fromAPIResponse(event_data.user));
|
||||
} else if (event_name === 'INTERACTION_CREATE') {
|
||||
this.handleInteractionCreate(payload);
|
||||
} else if (event_name === 'MESSAGE_CREATE') {
|
||||
await this.handleMessageCreate(payload as Payload);
|
||||
} else if (event_name === 'MESSAGE_UPDATE') {
|
||||
// this.handleMessageCreate(payload);
|
||||
}
|
||||
|
||||
// Allocate a var to store the context
|
||||
let context = null;
|
||||
|
||||
// If the event is a message event, create a context from the message
|
||||
if (['MESSAGE_CREATE', 'MESSAGE_UPDATE'].includes(event_name)) {
|
||||
// If the event is a message event, create a message object
|
||||
const message = Message[fromDiscord](event_data as DiscordMessage);
|
||||
// And then generate the context
|
||||
context = new Context(this.client, this.api, { message });
|
||||
}
|
||||
|
||||
// Call event handlers
|
||||
this.bus.emit(event_name, context, event_data);
|
||||
// this.processor.events.execute(event_name, context, event_data);
|
||||
}
|
||||
|
||||
private handleMessageCreate = async (payload: Payload) => {
|
||||
if (isNil(this.user)) {
|
||||
throw new Error('Cannot handle MESSAGE_CREATE event without a user');
|
||||
}
|
||||
|
||||
if (!isObject(payload.d)) {
|
||||
throw new Error('Invalid MESSAGE_CREATE payload received');
|
||||
}
|
||||
|
||||
// Ignore bots
|
||||
if (payload.d.author.bot) {
|
||||
return;
|
||||
}
|
||||
// Ignore self
|
||||
if (payload.d.author.id === this.user.id) {
|
||||
return;
|
||||
}
|
||||
// Extract the author
|
||||
const author = User.fromDiscord(DiscordUser.fromAPIResponse(payload.d.author));
|
||||
// Store the author in the cache
|
||||
this.client.users.set(author.id, author);
|
||||
// Extract the content
|
||||
const content = payload.d.content.trim();
|
||||
// Is this a command?
|
||||
if (content.startsWith(this.client.prefix) || content.startsWith(`<@${this.user.id}>`)) {
|
||||
// Determine the prefix
|
||||
const prefix = content.startsWith(this.client.prefix) ? this.client.prefix : `<@${this.user.id}> `;
|
||||
// Parse command
|
||||
const args = content
|
||||
.slice(prefix.length)
|
||||
.trim()
|
||||
.split(/\s+(?=(?:[^\'"]*[\'"][^\'"]*[\'"])*[^\'"]*$)/g);
|
||||
// Ignore empty commands
|
||||
if (args.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Extract the command name
|
||||
// TODO: Case insensitive config?
|
||||
const command = args.shift()!;
|
||||
// Create a new message object from the payload
|
||||
const message = Message.fromDiscord(payload.d as DiscordMessage);
|
||||
// Store the message in the cache
|
||||
this.client.messages.set(message.id, message);
|
||||
// Generate context
|
||||
const context = new Context(this.client, this.api, { message });
|
||||
// Dispatch event for the command
|
||||
this.bus.emit(`${HandlerType.ChatCommands}:${command}`, context, args);
|
||||
// // Check if command exists
|
||||
// if (this.processor[HandlerType.ChatCommands].has(command)) {
|
||||
// // Execute the handler
|
||||
// this.processor[HandlerType.ChatCommands].execute(command, context, args);
|
||||
// } else {
|
||||
// // TODO: Custom unknown command handler
|
||||
// // context.reply(`Unknown command: ${command}`);
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
private handleInteractionCreate = (payload: Payload) => {
|
||||
if (isNil(payload.d)) {
|
||||
throw new Error('Invalid INTERACTION_CREATE payload received');
|
||||
}
|
||||
|
||||
if (!isObject(payload.d)) {
|
||||
throw new Error('Invalid INTERACTION_CREATE data received');
|
||||
}
|
||||
|
||||
if (typeof payload.d.type !== 'number') {
|
||||
throw new Error('Invalid INTERACTION_CREATE type received');
|
||||
}
|
||||
|
||||
if (payload.d.type === 2) {
|
||||
this.handleShashInteraction(payload as Payload & { d: InteractionPayload & { data: InteractionCommandData } });
|
||||
} else if (payload.d.type === 3) {
|
||||
this.handleChatInteraction(payload as Payload & { d: InteractionPayload & { data: InteractionCommpoentData } });
|
||||
} else {
|
||||
// this.handleChatInteraction(payload);
|
||||
}
|
||||
};
|
||||
|
||||
private handleShashInteraction = (payload: Payload & { d: InteractionPayload & { data: InteractionCommandData } }) => {
|
||||
if (isNil(payload.d.data)) {
|
||||
throw new Error('Invalid INTERACTION_CREATE data received');
|
||||
}
|
||||
|
||||
const command = payload.d.data.name;
|
||||
const args = payload.d.data.options ?? [];
|
||||
|
||||
const context = new Context(this.client, this.api, { interaction: payload.d });
|
||||
|
||||
if (payload.d.guild) {
|
||||
const guild = Guild.fromDiscord(payload.d.guild);
|
||||
this.client.guilds.set(guild.id, guild);
|
||||
}
|
||||
|
||||
if (payload.d.member) {
|
||||
const member = User.fromDiscord(payload.d.member?.user);
|
||||
this.client.users.set(member.id, member);
|
||||
}
|
||||
|
||||
if (payload.d.user) {
|
||||
const user = User.fromDiscord(payload.d.user);
|
||||
this.client.users.set(user.id, user);
|
||||
}
|
||||
|
||||
// TODO: Fix when channel class is implemented
|
||||
// if (payload.d.data.channel) {
|
||||
// const channel = Channel[fromDiscord](payload.d.data.channel);
|
||||
// this.client.channels.set(channel.id, channel);
|
||||
// }
|
||||
|
||||
if (this.processor.application_commands.has(command)) {
|
||||
this.processor.application_commands.execute(command, context, args?.map(arg => arg.value));
|
||||
} else {
|
||||
// TODO: Custom unknown command handler
|
||||
context.reply(`Unknown command: ${command}`);
|
||||
}
|
||||
};
|
||||
|
||||
private handleChatInteraction = (payload: Payload & { d: InteractionPayload & { data: InteractionCommpoentData } }) => {
|
||||
const context = new Context(this.client, this.api, { interaction: payload.d });
|
||||
|
||||
if (exists(payload.d.data)) {
|
||||
if (this.processor[HandlerType.Interactions].has(payload.d.data.custom_id)) {
|
||||
this.processor[HandlerType.Interactions].execute(payload.d.data.custom_id, context, payload.d.data);
|
||||
} else {
|
||||
context.reply(`Unknown interaction: \`${payload.d.data.custom_id}\``);
|
||||
}
|
||||
} else {
|
||||
context.reply('Invalid interaction data received');
|
||||
}
|
||||
};
|
||||
|
||||
async connect() {
|
||||
// Fetch the Discord gateway URL
|
||||
const response = await this.api.request('GET', '/gateway/bot');
|
||||
// Append the query parameters to the URL
|
||||
const url = new URL(response.url);
|
||||
url.searchParams.append('v', '10');
|
||||
url.searchParams.append('encoding', 'json');
|
||||
// Open a new WebSocket connection to the Discord gateway
|
||||
this.socket = new WebSocket(url);
|
||||
// Register event listeners
|
||||
this.socket.on('open', this.onOpen);
|
||||
this.socket.on('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Expectation } from "../types/common";
|
||||
import { Context, type HydratedContext } from "../types/context";
|
||||
import { Message, type HydratedMessage } from "../types/message";
|
||||
import { exists } from "../utils";
|
||||
import type { API } from "./api";
|
||||
import type { Client } from "./client";
|
||||
|
||||
export type GatewayObject = Message;
|
||||
export type Hydrateable = Context | GatewayObject
|
||||
export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
|
||||
T extends Message ? HydratedMessage<K> :
|
||||
T extends Context ? HydratedContext<K> : never;
|
||||
|
||||
/**
|
||||
* Hydrates a given data object based on the provided expectations.
|
||||
*
|
||||
* @template T - The type of the data object to be hydrated. Must extend `Hydrateable`.
|
||||
* @template K - An array of `Expectation` types that specify what properties should be hydrated.
|
||||
*
|
||||
* @param data - The data object to be hydrated. Can be of type `Context` or `Message`.
|
||||
* @param expectations - An array of expectations that specify what properties should be hydrated.
|
||||
* @param client - The client instance used to fetch additional data required for hydration.
|
||||
* @param api - The API instance used to fetch additional data required for hydration.
|
||||
*
|
||||
* @returns A promise that resolves to the hydrated data object.
|
||||
*
|
||||
* @throws Will throw an error if an expectation cannot be resolved.
|
||||
*/
|
||||
export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
|
||||
data: T,
|
||||
expectations: K,
|
||||
client: Client,
|
||||
api: API
|
||||
): Promise<T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
|
||||
// TODO: add clone trait. Make it a requirement for hydrateable objects.
|
||||
// Then clone the object and return the hydrated object.
|
||||
let hydrated: Hydrated<T, K>;
|
||||
|
||||
if (data instanceof Context) {
|
||||
for (const expectation of expectations) {
|
||||
if (expectation === Expectation.Message && exists(data.message_id)) {
|
||||
let context = data as unknown as HydratedContext<[Expectation.Message, ...Expectation[]]>;
|
||||
context.message = await client.messages.get(data.channel_id, data.message_id);
|
||||
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||
let context = data as unknown as HydratedContext<[Expectation.Guild, ...Expectation[]]>;
|
||||
context.guild = await client.guilds.get(data.guild_id);
|
||||
} else {
|
||||
throw new Error(`Invalid expectation requested for Context: ${expectation}`);
|
||||
}
|
||||
}
|
||||
} else if (data instanceof Message) {
|
||||
for (const expectation of expectations) {
|
||||
if (expectation === Expectation.Message && exists(data.message_reference?.id)) {
|
||||
let message = data as unknown as HydratedMessage<[Expectation.Message, ...Expectation[]]>;
|
||||
message.reference = await client.messages.get(data.message_reference.channel_id, data.message_reference.id);
|
||||
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||
let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>;
|
||||
message.guild = await client.guilds.get(data.guild_id);
|
||||
} else {
|
||||
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data as unknown as (T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper function that attempts to hydrate a given data object and returns a type guard function.
|
||||
*
|
||||
* @template T - The type of the data object to be hydrated. Must extend `Hydrateable`.
|
||||
* @template K - An array of `Expectation` types that specify what properties should be hydrated.
|
||||
*
|
||||
* @param data - The data object to be hydrated. Can be of type `Context` or `Message`.
|
||||
* @param expectations - An array of expectations that specify what properties should be hydrated.
|
||||
* @param client - The client instance used to fetch additional data required for hydration.
|
||||
* @param api - The API instance used to fetch additional data required for hydration.
|
||||
*
|
||||
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
|
||||
*/
|
||||
export async function hydrator<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
|
||||
data: T,
|
||||
expectations: K,
|
||||
client: Client,
|
||||
api: API
|
||||
): Promise<(data: unknown) => data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
|
||||
let hydrated = true;
|
||||
|
||||
try {
|
||||
// TODO: When clone is implemented we will receive a clone of the object here
|
||||
// So in hydrator we will need to copy the properties from the clone to the original object
|
||||
await hydrate(data, expectations, client, api);
|
||||
} catch (error) {
|
||||
hydrated = false;
|
||||
}
|
||||
|
||||
// BUG: Resolves to never after hydrations on an already hydrated object
|
||||
return function (data: unknown): data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K> {
|
||||
return hydrated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import type { Client } from "./client";
|
||||
import type { Context } from "../types/context";
|
||||
import { HandlerType, Trait, type EventPayload } from "../types/common";
|
||||
import { CommandHandler, type Handler, type ArgsFromOptions, OptionConstructorMap, type InteractionData } from "../types/handler";
|
||||
import type { API } from "./api";
|
||||
import { ApplicationCommandOptionType, ApplicationCommandType, type ApplicationCommandOption } from "../types/applicationCommand";
|
||||
import type { Emitter } from "./bus";
|
||||
|
||||
export class Processor {
|
||||
private handlers = {
|
||||
[HandlerType.Events]: new Map<string, Handler[]>(),
|
||||
[HandlerType.ChatCommands]: new Map<string, Handler[]>(),
|
||||
[HandlerType.ApplicationCommands]: new Map<string, Handler[]>(),
|
||||
[HandlerType.Interactions]: new Map<string, Handler[]>()
|
||||
};
|
||||
|
||||
constructor(private client: Client, private api: API, private bus: Emitter) {};
|
||||
|
||||
public register(type: HandlerType, event: string | string[], handler: Handler) {
|
||||
// Normalize events into an array
|
||||
const events = Array.isArray(event) ? event : [event];
|
||||
// Register the handler for each event
|
||||
for (const e of events) {
|
||||
const handlers = this.handlers[type].get(e) || [];
|
||||
handlers.push(handler);
|
||||
this.handlers[type].set(e, handlers);
|
||||
this.bus.on(`${type}:${e}`, async (context: Context | null, payload: any) => { this.execute(type, e, context, payload) });
|
||||
}
|
||||
};
|
||||
|
||||
public unregister(type: HandlerType, event: string | string[], handler?: Handler) {
|
||||
// Normalize events into an array
|
||||
const events = Array.isArray(event) ? event : [event];
|
||||
// Unregister the handler for each event
|
||||
for (const e of events) {
|
||||
if (handler) {
|
||||
// Get the handlers for the event
|
||||
const handlers = this.handlers[type].get(e);
|
||||
if (handlers) {
|
||||
// Find the index of the handler
|
||||
const index = handlers.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
// Remove the handler from the array
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no handler is provided, clear all handlers for the event
|
||||
this.handlers[type].clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Fix overloads
|
||||
// public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
|
||||
// public async execute(type: HandlerType.Interactions, event: string, context: Context | null, payload: InteractionData): Promise<void>
|
||||
// public async execute(type: Omit<HandlerType, HandlerType.Events | HandlerType.Interactions>, event: string, context: Context, args: any[]): Promise<void>
|
||||
public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
|
||||
const handlers = this.handlers[type].get(event);
|
||||
|
||||
if (handlers) {
|
||||
for (const handler of handlers) {
|
||||
if (Processor.isCommandHandler(handler)) {
|
||||
// TODO: Enforce this?
|
||||
if (type === HandlerType.Events) {
|
||||
throw new Error('Event handlers cannot be a CommandHandler');
|
||||
}
|
||||
// Validate and resolve the args
|
||||
// TODO: There's no reason this can't use the hydrator system
|
||||
// Hydrator should have a more genric subcomponent that can be used anywhere and is more dumb.
|
||||
// Basically a function that accepts id's and returns values.
|
||||
const resolvedArgs = await this.validateAndResolveArgs(argsOrPayload, handler.args);
|
||||
// Execute the handler
|
||||
await Promise.resolve(handler[Trait.execute](context!, resolvedArgs));
|
||||
} else {
|
||||
// TODO: Fix ts error
|
||||
// @ts-ignore - Haven't figured out how to type narrow this yet
|
||||
await Promise.resolve(handler(context, argsOrPayload));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateAndResolveArgs = async <T extends readonly ApplicationCommandOption[]>(args: any[], definition: T): Promise<ArgsFromOptions<T>> => {
|
||||
// Zip the args and definition together
|
||||
const zipped: [ApplicationCommandOption, any][] = args.map((arg, i) => [definition[i], arg]);
|
||||
// Validate the args
|
||||
const validated: Record<string, any> = {};
|
||||
// TODO: Attempt some type coresion/resolution here (mostly for text commands)
|
||||
for (const [def, arg] of zipped) {
|
||||
if (def.choices) {
|
||||
// If the definition has choices, validate the arg against them
|
||||
const choice = def.choices.find((choice: any) => choice.value === arg);
|
||||
if (!choice) {
|
||||
throw new Error(`Invalid choice for ${def.name}: expected one of ${def.choices.map((c: any) => c.value).join(', ')}, got ${arg}`);
|
||||
}
|
||||
validated[def.name] = choice.value;
|
||||
} else if (def.type === ApplicationCommandOptionType.User) {
|
||||
// If the definition is a user, resolve the user
|
||||
// Check if the arg is a string
|
||||
if (typeof arg === 'string') {
|
||||
// Parse out the user ID as a mention or raw ID
|
||||
const userId = arg.match(/<@!?(\d+)>/)?.[1] ?? arg.match(/\d+/)?.[0];
|
||||
if (!userId) {
|
||||
throw new Error(`Invalid user for ${def.name}: expected a user mention or snowflake id, got ${arg}`);
|
||||
}
|
||||
validated[def.name] = await this.client.users.get(userId);
|
||||
} else if (typeof arg === 'object') {
|
||||
// Check if the object has an ID
|
||||
if (arg.id) {
|
||||
validated[def.name] = await this.client.users.get(arg.id);
|
||||
} else {
|
||||
throw new Error(`Invalid user for ${def.name}: expected a user object with an ID, got ${arg}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid user for ${def.name}: expected a user mention, snowflake id, or object, got ${arg}`);
|
||||
}
|
||||
} else {
|
||||
// Otherwise just check the type
|
||||
if (arg.value instanceof OptionConstructorMap[def.type]) {
|
||||
// TODO: Coerce the type if possible
|
||||
throw new Error(`Invalid type for ${def.name}: expected ${def.type}, got ${typeof arg}`);
|
||||
} else {
|
||||
validated[def.name] = arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Fix types so we don't need to cast here
|
||||
return Promise.resolve(validated as ArgsFromOptions<T>);
|
||||
};
|
||||
|
||||
[HandlerType.Events] = {
|
||||
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Events, event, handler),
|
||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Events, event, handler),
|
||||
execute: async (event: string, context: Context | null, payload: EventPayload) => this.execute(HandlerType.Events, event, context, payload),
|
||||
has: (event: string) => this.handlers[HandlerType.Events].has(event) && this.handlers[HandlerType.Events].get(event)?.length! > 0
|
||||
};
|
||||
|
||||
[HandlerType.ChatCommands] = {
|
||||
register: (event: string | string[], handler: Handler) => this.register(HandlerType.ChatCommands, event, handler),
|
||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ChatCommands, event, handler),
|
||||
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ChatCommands, event, context, args),
|
||||
has: (event: string) => this.handlers[HandlerType.ChatCommands].has(event) ?? this.handlers[HandlerType.ChatCommands].get(event)?.length! > 0
|
||||
};
|
||||
|
||||
[HandlerType.ApplicationCommands] = {
|
||||
register: (name: string | string[], type: ApplicationCommandType, handler: Handler) => {
|
||||
const names = Array.isArray(name) ? name : [name];
|
||||
|
||||
// This is an application command so we need to register it with the API
|
||||
// Ensure the handler is a CommandHandler
|
||||
// TODO: Don't enforce this?
|
||||
if (!Processor.isCommandHandler(handler)) {
|
||||
throw new Error('Handler must be a CommandHandler');
|
||||
}
|
||||
|
||||
// Ensure the command name is valid
|
||||
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
||||
// TODO: can this sus bind be removed? Is the only way to kill the arrow function?
|
||||
const commandNameRegex = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
|
||||
const commandNameTest = commandNameRegex.test.bind(commandNameRegex)
|
||||
if (!names.every(commandNameTest)) {
|
||||
throw new Error('Invalid application command name');
|
||||
};
|
||||
|
||||
for (const n of names) {
|
||||
// Register the command with the API
|
||||
const command: Record<string, any> = handler.toJSON();
|
||||
command.name = n;
|
||||
command.type = type;
|
||||
this.api.post(`/applications/${this.client.application_id}/commands`, command);
|
||||
}
|
||||
|
||||
this.register(HandlerType.ApplicationCommands, names, handler)
|
||||
},
|
||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ApplicationCommands, event, handler),
|
||||
execute: async (event: string, context: Context, args: any[]) => this.execute(HandlerType.ApplicationCommands, event, context, args),
|
||||
has: (event: string) => this.handlers[HandlerType.ApplicationCommands].has(event) ?? this.handlers[HandlerType.ApplicationCommands].get(event)?.length! > 0,
|
||||
};
|
||||
|
||||
[HandlerType.Interactions] = {
|
||||
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler),
|
||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.Interactions, event, handler),
|
||||
execute: async (event: string, context: Context, args: InteractionData) => this.execute(HandlerType.Interactions, event, context, args),
|
||||
has: (event: string) => this.handlers[HandlerType.Interactions].has(event) ?? this.handlers[HandlerType.Interactions].get(event)?.length! > 0
|
||||
};
|
||||
|
||||
static isCommandHandler = (handler: Handler): handler is CommandHandler<ApplicationCommandOption[]> => {
|
||||
if (typeof handler === 'object') {
|
||||
return 'args' in handler && Trait.execute in handler && handler instanceof CommandHandler;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { Trait } from "./common";
|
||||
|
||||
// Enum for Application Command Types
|
||||
export enum ApplicationCommandType {
|
||||
Chat = 1,
|
||||
User = 2,
|
||||
Message = 3,
|
||||
PrimaryEntryPoint = 4,
|
||||
}
|
||||
|
||||
// Enum for Application Command Option Types
|
||||
export enum ApplicationCommandOptionType {
|
||||
SubCommand = 1,
|
||||
SubCommandGroup = 2,
|
||||
String = 3,
|
||||
Integer = 4,
|
||||
Boolean = 5,
|
||||
User = 6,
|
||||
Channel = 7,
|
||||
Role = 8,
|
||||
Mentionable = 9,
|
||||
Number = 10,
|
||||
Attachment = 11,
|
||||
}
|
||||
|
||||
// Enum for Application Command Contexts
|
||||
export enum ApplicationCommandContext {
|
||||
Guild = 0,
|
||||
DM = 1,
|
||||
PrivateChannel = 2,
|
||||
}
|
||||
|
||||
// Enum for Channel Types (if applicable for options)
|
||||
export enum ChannelType {
|
||||
GuildText = 0,
|
||||
DM = 1,
|
||||
GuildVoice = 2,
|
||||
GroupDm = 3,
|
||||
GuildCategory = 4,
|
||||
GuildNews = 5,
|
||||
GuildStore = 6,
|
||||
GuildNewsThread = 10,
|
||||
GuildPublicThread = 11,
|
||||
GuildPrivateThread = 12,
|
||||
GuildStageVoice = 13,
|
||||
GuildDirectory = 14,
|
||||
GuildForum = 15,
|
||||
}
|
||||
|
||||
// Enum for Application Command Handler Types
|
||||
export enum ApplicationCommandHandlerType {
|
||||
AppHandler = 1,
|
||||
LaunchActivity = 2,
|
||||
}
|
||||
|
||||
// Define the ApplicationCommandOption type
|
||||
export type ApplicationCommandOption = {
|
||||
type: ApplicationCommandOptionType;
|
||||
name: string;
|
||||
id: string;
|
||||
name_localizations?: Record<string, string> | null;
|
||||
description: string;
|
||||
description_localizations?: Record<string, string> | null;
|
||||
required?: boolean;
|
||||
choices?: Array<{ name: string; value: string | number }>;
|
||||
options?: ApplicationCommandOption[]; // Recursive structure for subcommands
|
||||
channel_types?: ChannelType[];
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
min_length?: number;
|
||||
max_length?: number;
|
||||
autocomplete?: boolean;
|
||||
};
|
||||
|
||||
export type ApplicationCommandOptionResult = {
|
||||
value: any,
|
||||
type: number,
|
||||
name: string
|
||||
};
|
||||
|
||||
// Define the DiscordApplicationCommand type
|
||||
export type DiscordApplicationCommand = {
|
||||
id: string;
|
||||
type?: ApplicationCommandType;
|
||||
application_id: string;
|
||||
guild_id?: string;
|
||||
name: string;
|
||||
name_localizations?: Record<string, string> | null;
|
||||
description: string;
|
||||
description_localizations?: Record<string, string> | null;
|
||||
options?: ApplicationCommandOption[];
|
||||
default_member_permissions?: string | null;
|
||||
dm_permission?: boolean;
|
||||
default_permission?: boolean;
|
||||
nsfw?: boolean;
|
||||
integration_types?: number[];
|
||||
contexts?: string;
|
||||
version: string;
|
||||
handler?: ApplicationCommandHandlerType;
|
||||
};
|
||||
|
||||
// Define the ApplicationCommand class
|
||||
export class ApplicationCommand {
|
||||
id: string;
|
||||
type?: ApplicationCommandType;
|
||||
application_id: string;
|
||||
guild_id?: string;
|
||||
name: string;
|
||||
name_localizations?: Record<string, string> | null;
|
||||
description: string;
|
||||
description_localizations?: Record<string, string> | null;
|
||||
options?: ApplicationCommandOption[];
|
||||
default_member_permissions?: string | null;
|
||||
dm_permission?: boolean;
|
||||
default_permission?: boolean;
|
||||
nsfw?: boolean;
|
||||
integration_types?: number[];
|
||||
contexts?: ApplicationCommandContext[];
|
||||
version: string;
|
||||
handler?: ApplicationCommandHandlerType;
|
||||
|
||||
constructor(command: DiscordApplicationCommand) {
|
||||
this.id = command.id;
|
||||
this.type = command.type;
|
||||
this.application_id = command.application_id;
|
||||
this.guild_id = command.guild_id;
|
||||
this.name = command.name;
|
||||
this.name_localizations = command.name_localizations;
|
||||
this.description = command.description;
|
||||
this.description_localizations = command.description_localizations;
|
||||
this.options = command.options;
|
||||
this.default_member_permissions = command.default_member_permissions;
|
||||
this.dm_permission = command.dm_permission;
|
||||
this.default_permission = command.default_permission;
|
||||
this.nsfw = command.nsfw;
|
||||
this.integration_types = command.integration_types;
|
||||
this.contexts = command.contexts?.split(',').map((context) => parseInt(context) as ApplicationCommandContext);
|
||||
this.version = command.version;
|
||||
this.handler = command.handler;
|
||||
}
|
||||
|
||||
static [Trait.fromDiscord](command: DiscordApplicationCommand): ApplicationCommand {
|
||||
return new ApplicationCommand(command);
|
||||
}
|
||||
|
||||
public toJSON = (): DiscordApplicationCommand => {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
application_id: this.application_id,
|
||||
guild_id: this.guild_id,
|
||||
name: this.name,
|
||||
name_localizations: this.name_localizations,
|
||||
description: this.description,
|
||||
description_localizations: this.description_localizations,
|
||||
options: this.options,
|
||||
default_member_permissions: this.default_member_permissions,
|
||||
dm_permission: this.dm_permission,
|
||||
default_permission: this.default_permission,
|
||||
nsfw: this.nsfw,
|
||||
integration_types: this.integration_types,
|
||||
contexts: this.contexts?.join(','),
|
||||
version: this.version,
|
||||
handler: this.handler
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Trait } from "./common";
|
||||
|
||||
export abstract class GatewayObject {
|
||||
constructor() {}
|
||||
|
||||
// TODO: Add a clone method to subclasses
|
||||
// public abstract [Trait.clone]<T extends GatewayObject>(this: new (data: any) => T): T;
|
||||
|
||||
// public get clone() {
|
||||
// return this[Trait.clone];
|
||||
// }
|
||||
|
||||
static [Trait.fromDiscord]<T extends GatewayObject>(this: new (...args: any[]) => T, data: any): T {
|
||||
throw new Error('Method should be overridden - you should not see this. If you do, please report it.');
|
||||
};
|
||||
|
||||
static get fromDiscord() {
|
||||
return this[Trait.fromDiscord];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export class Channel {
|
||||
version: number;
|
||||
type: number;
|
||||
position: number;
|
||||
permission_overwrites: any[];
|
||||
name: string;
|
||||
id: string;
|
||||
flags: number;
|
||||
topic?: string | null;
|
||||
rate_limit_per_user?: number;
|
||||
parent_id?: string;
|
||||
last_message_id?: string | null;
|
||||
user_limit?: number;
|
||||
rtc_region?: string | null;
|
||||
bitrate?: number;
|
||||
last_pin_timestamp?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
this.version = data.version;
|
||||
this.type = data.type;
|
||||
this.position = data.position;
|
||||
this.permission_overwrites = data.permission_overwrites;
|
||||
this.name = data.name;
|
||||
this.id = data.id;
|
||||
this.flags = data.flags;
|
||||
this.topic = data.topic;
|
||||
this.rate_limit_per_user = data.rate_limit_per_user;
|
||||
this.parent_id = data.parent_id;
|
||||
this.last_message_id = data.last_message_id;
|
||||
this.user_limit = data.user_limit;
|
||||
this.rtc_region = data.rtc_region;
|
||||
this.bitrate = data.bitrate;
|
||||
this.last_pin_timestamp = data.last_pin_timestamp;
|
||||
}
|
||||
|
||||
static fromAPIResponse(data: any): Channel {
|
||||
return new Channel(data);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Client } from "..";
|
||||
import { Context } from "../lib";
|
||||
|
||||
export abstract class Command {
|
||||
abstract name: string;
|
||||
abstract description: string;
|
||||
abstract execute: CommandFuntion;
|
||||
}
|
||||
|
||||
export type CommandFuntion = (client: Client, context: Context, ...args: any[]) => void;
|
||||
+115
-130
@@ -1,79 +1,64 @@
|
||||
export type ClientConfig = {
|
||||
token: string;
|
||||
application_id: string;
|
||||
prefix?: string;
|
||||
intents?: Intent[];
|
||||
}
|
||||
|
||||
export enum Expectation {
|
||||
Guild = 'guild',
|
||||
Channel = 'channel',
|
||||
User = 'user',
|
||||
Message = 'message',
|
||||
}
|
||||
|
||||
export enum HandlerType {
|
||||
Events = 'events',
|
||||
ChatCommands = 'chat_commands',
|
||||
ApplicationCommands = 'application_commands',
|
||||
Interactions = 'interactions'
|
||||
}
|
||||
|
||||
export type Expects<T extends Array<Expectation>, K extends Expectation> = Extract<K, T[number]> extends never ? false : true;
|
||||
|
||||
export const clone: unique symbol = Symbol('clone')
|
||||
export const fromDiscord: unique symbol = Symbol('fromDiscord')
|
||||
export const execute: unique symbol = Symbol('execute')
|
||||
|
||||
export const Trait: {
|
||||
clone: typeof clone
|
||||
fromDiscord: typeof fromDiscord
|
||||
execute: typeof execute
|
||||
} = {
|
||||
clone,
|
||||
fromDiscord,
|
||||
execute
|
||||
}
|
||||
|
||||
export type EventPayload = any;
|
||||
|
||||
export type Primitive = string | number | boolean | null;
|
||||
|
||||
/**
|
||||
* Represents a payload structure used in the Discord API.
|
||||
*
|
||||
* @typedef {Object} Payload
|
||||
* @property {number} op - The opcode of the payload.
|
||||
* @property {Record<string, any>} d - The data associated with the payload.
|
||||
* @property {number | null} s - The sequence number, or null if not applicable.
|
||||
* @property {string | null} t - The event name, or null if not applicable.
|
||||
*/
|
||||
export type Payload = {
|
||||
op: number; // opcode
|
||||
d: Record<string, any>; // data
|
||||
s: number | null; // sequence number
|
||||
t: string | null; // event name
|
||||
op: number;
|
||||
d: Record<string, any> | Primitive | null;
|
||||
s: number | null;
|
||||
t: string | null;
|
||||
};
|
||||
|
||||
export type Session = {
|
||||
s: number; // sequence number
|
||||
session_id: string;
|
||||
session_type: string;
|
||||
resume_gateway_url: string;
|
||||
};
|
||||
|
||||
export type Guild = Record<string, any>;
|
||||
|
||||
export type User = Record<string, any>;
|
||||
|
||||
export enum Status {
|
||||
Online = "online",
|
||||
DND = "dnd",
|
||||
Idle = "idle",
|
||||
Invisible = "invisible",
|
||||
Offline = "offline"
|
||||
}
|
||||
|
||||
export type Presence = {
|
||||
since: number | null;
|
||||
activities: Activity[];
|
||||
status: Status;
|
||||
afk: boolean;
|
||||
}
|
||||
|
||||
export type Activity = {
|
||||
name: string;
|
||||
type: number;
|
||||
created_at: number;
|
||||
url?: string;
|
||||
timestamps?: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
application_id?: string;
|
||||
details?: string;
|
||||
state?: string;
|
||||
emoji?: {
|
||||
name: string;
|
||||
id?: string;
|
||||
animated?: boolean;
|
||||
};
|
||||
party?: {
|
||||
id?: string;
|
||||
size?: [number, number];
|
||||
};
|
||||
assets?: {
|
||||
large_image?: string;
|
||||
large_text?: string;
|
||||
small_image?: string;
|
||||
small_text?: string;
|
||||
};
|
||||
secrets?: {
|
||||
join?: string;
|
||||
spectate?: string;
|
||||
match?: string;
|
||||
};
|
||||
instance?: boolean;
|
||||
flags?: number;
|
||||
}
|
||||
|
||||
export enum ActivityType {
|
||||
Game,
|
||||
Streaming,
|
||||
Listening,
|
||||
Watching,
|
||||
Custom ,
|
||||
Competing
|
||||
export enum InteractionResponseType {
|
||||
Pong = 1,
|
||||
Message = 4,
|
||||
DeferredMessage = 5
|
||||
}
|
||||
|
||||
export enum Intent {
|
||||
@@ -97,107 +82,107 @@ export enum Intent {
|
||||
ALL = 32767
|
||||
}
|
||||
|
||||
export const IntentGroup : {[key in Intent]: [string[], number]} = {
|
||||
export const IntentGroup: { [key in Intent]?: [string[], number] } = {
|
||||
[Intent.GUILDS]: [[
|
||||
"GUILD_CREATE",
|
||||
"GUILD_UPDATE",
|
||||
"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"
|
||||
"GUILD_CREATE",
|
||||
"GUILD_UPDATE",
|
||||
"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"
|
||||
], 0],
|
||||
|
||||
[Intent.GUILD_MEMBERS]: [[
|
||||
"GUILD_MEMBER_ADD",
|
||||
"GUILD_MEMBER_UPDATE",
|
||||
"GUILD_MEMBER_REMOVE",
|
||||
"THREAD_MEMBERS_UPDATE *",
|
||||
"GUILD_MEMBER_ADD",
|
||||
"GUILD_MEMBER_UPDATE",
|
||||
"GUILD_MEMBER_REMOVE",
|
||||
"THREAD_MEMBERS_UPDATE *",
|
||||
], 1],
|
||||
|
||||
[Intent.GUILD_BANS]: [[
|
||||
"GUILD_BAN_ADD",
|
||||
"GUILD_BAN_REMOVE",
|
||||
"GUILD_BAN_ADD",
|
||||
"GUILD_BAN_REMOVE",
|
||||
], 2],
|
||||
|
||||
[Intent.GUILD_EMOJIS_AND_STICKERS]: [[
|
||||
"GUILD_EMOJIS_UPDATE",
|
||||
"GUILD_STICKERS_UPDATE",
|
||||
"GUILD_EMOJIS_UPDATE",
|
||||
"GUILD_STICKERS_UPDATE",
|
||||
], 3],
|
||||
|
||||
[Intent.GUILD_INTEGRATIONS]: [[
|
||||
"GUILD_INTEGRATIONS_UPDATE",
|
||||
"INTEGRATION_CREATE",
|
||||
"INTEGRATION_UPDATE",
|
||||
"INTEGRATION_DELETE",
|
||||
"GUILD_INTEGRATIONS_UPDATE",
|
||||
"INTEGRATION_CREATE",
|
||||
"INTEGRATION_UPDATE",
|
||||
"INTEGRATION_DELETE",
|
||||
], 4],
|
||||
|
||||
[Intent.GUILD_WEBHOOKS]: [[
|
||||
"WEBHOOKS_UPDATE"
|
||||
"WEBHOOKS_UPDATE"
|
||||
], 5],
|
||||
|
||||
[Intent.GUILD_INVITES]: [[
|
||||
"INVITE_CREATE",
|
||||
"INVITE_DELETE",
|
||||
"INVITE_CREATE",
|
||||
"INVITE_DELETE",
|
||||
], 6],
|
||||
|
||||
[Intent.GUILD_VOICE_STATES]: [[
|
||||
"VOICE_STATE_UPDATE"
|
||||
"VOICE_STATE_UPDATE"
|
||||
], 7],
|
||||
|
||||
[Intent.GUILD_PRESENCES]: [[
|
||||
"PRESENCE_UPDATE"
|
||||
"PRESENCE_UPDATE"
|
||||
], 8],
|
||||
|
||||
[Intent.GUILD_MESSAGES]: [[
|
||||
"MESSAGE_CREATE",
|
||||
"MESSAGE_UPDATE",
|
||||
"MESSAGE_DELETE",
|
||||
"MESSAGE_DELETE_BULK",
|
||||
"MESSAGE_CREATE",
|
||||
"MESSAGE_UPDATE",
|
||||
"MESSAGE_DELETE",
|
||||
"MESSAGE_DELETE_BULK",
|
||||
], 9],
|
||||
|
||||
[Intent.GUILD_MESSAGE_REACTIONS]: [[
|
||||
"MESSAGE_REACTION_ADD",
|
||||
"MESSAGE_REACTION_REMOVE",
|
||||
"MESSAGE_REACTION_REMOVE_ALL",
|
||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
"MESSAGE_REACTION_ADD",
|
||||
"MESSAGE_REACTION_REMOVE",
|
||||
"MESSAGE_REACTION_REMOVE_ALL",
|
||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
], 10],
|
||||
|
||||
[Intent.GUILD_MESSAGE_TYPING]: [[
|
||||
"TYPING_START"
|
||||
"TYPING_START"
|
||||
], 11],
|
||||
|
||||
[Intent.DIRECT_MESSAGES]: [[
|
||||
"MESSAGE_CREATE",
|
||||
"MESSAGE_UPDATE",
|
||||
"MESSAGE_DELETE",
|
||||
"CHANNEL_PINS_UPDATE",
|
||||
"MESSAGE_CREATE",
|
||||
"MESSAGE_UPDATE",
|
||||
"MESSAGE_DELETE",
|
||||
"CHANNEL_PINS_UPDATE",
|
||||
], 12],
|
||||
|
||||
[Intent.DIRECT_MESSAGE_REACTIONS]: [[
|
||||
"MESSAGE_REACTION_ADD",
|
||||
"MESSAGE_REACTION_REMOVE",
|
||||
"MESSAGE_REACTION_REMOVE_ALL",
|
||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
"MESSAGE_REACTION_ADD",
|
||||
"MESSAGE_REACTION_REMOVE",
|
||||
"MESSAGE_REACTION_REMOVE_ALL",
|
||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
], 13],
|
||||
|
||||
[Intent.DIRECT_MESSAGE_TYPING]: [[
|
||||
"TYPING_START"
|
||||
"TYPING_START"
|
||||
], 14],
|
||||
|
||||
[Intent.MESSAGE_CONTENT]: [[
|
||||
"MESSAGE_CONTENT"
|
||||
"MESSAGE_CONTENT"
|
||||
], 15]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { API } from "../services/api";
|
||||
import type { Client } from "../services/client";
|
||||
import { hydrate, hydrator, type Hydrateable, type Hydrated } from "../services/hydrator";
|
||||
import { isNil, exists } from "../utils";
|
||||
import { fromDiscord, InteractionResponseType, type Expectation } from "./common";
|
||||
import type { Guild } from "./guild";
|
||||
import type { InteractionPayload } from "./handler";
|
||||
import { type DiscordMessage, Message, type MessagePayload } from "./message";
|
||||
/**
|
||||
* Represents the context of a message in a Discord channel.
|
||||
* Provides methods to interact with the message and the channel it was sent in.
|
||||
*
|
||||
* @class Context
|
||||
*
|
||||
* @property {string} message_id - The ID of the message that this context is associated with.
|
||||
* @property {string} channel_id - The ID of the channel where the message was sent.
|
||||
* @property {string} [guild_id] - The ID of the guild where the message was sent, if applicable.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Client} client - The client instance that is interacting with the Discord API.
|
||||
* @param {API} api - The API instance used to make requests to the Discord API.
|
||||
* @param {Message} message - The message object that this context is based on.
|
||||
*
|
||||
* @throws {Error} If the message ID is not provided.
|
||||
* @throws {Error} If the channel ID is not provided.
|
||||
*/
|
||||
|
||||
export type HydratedContext<T extends Array<Expectation>> = Context & {
|
||||
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
||||
message: Extract<Expectation.Message, T[number]> extends never ? undefined : Message;
|
||||
};
|
||||
|
||||
export type ContextData =
|
||||
| { channel_id?: string; message_id?: string; guild_id?: string; message?: MessagePayload, interaction: InteractionPayload }
|
||||
| { channel_id?: string; message_id?: string; guild_id?: string; message: MessagePayload, interaction?: InteractionPayload };
|
||||
|
||||
export class Context<D extends ContextData = ContextData> {
|
||||
// Public properties
|
||||
message_id?: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
message?: D extends { message: MessagePayload } ? MessagePayload : undefined;
|
||||
interaction?: D extends { interaction: InteractionPayload } ? InteractionPayload : undefined;
|
||||
|
||||
constructor(private client: Client, private api: API, data: D) {
|
||||
const channel_id = data.channel_id ?? data.message?.channel_id ?? data.interaction?.channel_id;
|
||||
|
||||
if (isNil(channel_id)) {
|
||||
throw new Error('Channel ID is required to create a context');
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
this.message_id = data.message_id ?? data.message?.id;
|
||||
this.channel_id = channel_id;
|
||||
this.guild_id = data.guild_id ?? data.message?.guild_id ?? data.interaction?.guild_id;
|
||||
this.message = data.message as D extends { message: MessagePayload } ? MessagePayload : undefined;
|
||||
this.interaction = data.interaction as D extends { interaction: InteractionPayload } ? InteractionPayload : undefined;
|
||||
}
|
||||
|
||||
get self() {
|
||||
return this.client.self!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrates a given data object based on the provided expectations.
|
||||
*
|
||||
* @template T - The type of the data object to be hydrated. Must extend `Hydrateable`.
|
||||
* @template K - An array of `Expectation` types that specify what properties should be hydrated.
|
||||
*
|
||||
* @param data - The data object to be hydrated. Can be of type `Context` or `Message`.
|
||||
* @param expectations - An array of expectations that specify what properties should be hydrated.
|
||||
*
|
||||
* @returns A promise that resolves to the hydrated data object.
|
||||
*
|
||||
* @throws Will throw an error if an expectation cannot be resolved.
|
||||
*/
|
||||
public hydrate = async <
|
||||
T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>,
|
||||
K extends Array<Expectation>
|
||||
>(data: T, expectations: K) => {
|
||||
return hydrate<T, K>(data, expectations, this.client, this.api);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper function that attempts to hydrate a given data object and returns a type guard function.
|
||||
*
|
||||
* @template T - The type of the data object to be hydrated. Must extend `Hydrateable`.
|
||||
* @template K - An array of `Expectation` types that specify what properties should be hydrated.
|
||||
*
|
||||
* @param data - The data object to be hydrated. Can be of type `Context` or `Message`.
|
||||
* @param expectations - An array of expectations that specify what properties should be hydrated.
|
||||
*
|
||||
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
|
||||
*/
|
||||
public hydrator = async <T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>, K extends Array<Expectation>>(data: T, expectations: K) => {
|
||||
return hydrator<T, K>(data, expectations, this.client, this.api);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = async (message: Message | string, reference: boolean = true): Promise<MessagePayload> => {
|
||||
if (typeof message === 'string') {
|
||||
message = new Message().setContent(message);
|
||||
}
|
||||
|
||||
if (exists(this.interaction)) {
|
||||
return this.ack(InteractionResponseType.Message, message.toJSON());
|
||||
} else if (exists(this.message)) {
|
||||
if (reference) {
|
||||
message.setReference(this.message)
|
||||
}
|
||||
|
||||
return this.client.sendMessage(this.channel_id, message);
|
||||
} else {
|
||||
throw new Error('Cannot reply without a message or interaction payload');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @name sendMessage
|
||||
* @description Sends a message to the channel that this context is associated with.
|
||||
* @param {Message | string} message - The message to send to the channel.
|
||||
* @returns {Promise<Message>} The message that was sent.
|
||||
*/
|
||||
public sendMessage = (message: Message | string) => {
|
||||
if (typeof message === 'string') {
|
||||
message = new Message().setContent(message);
|
||||
}
|
||||
return this.client.sendMessage(this.channel_id, message);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @name delete
|
||||
* @description Deletes a message in the channel that this context is associated with.
|
||||
* @param {string} [message_id] - The ID of the message to delete. If not provided, the message ID of this context will be used.
|
||||
* @returns {Promise<void>} A promise that resolves when the message is deleted.
|
||||
*/
|
||||
public deleteMessage = (message_id: string): Promise<void> => {
|
||||
return this.api.delete(`/channels/${this.channel_id}/messages/${message_id ?? this.message_id}`).then(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @name edit
|
||||
* @description Edits a message in the channel that this context is associated with.
|
||||
* @param {Message | string} message - The updated message content.
|
||||
* @returns {Promise<Message>} The updated message.
|
||||
*/
|
||||
public editMessage = async (message: Message, content?: string) => {
|
||||
const clone = new Message(message);
|
||||
if (content) {
|
||||
clone.setContent(content);
|
||||
}
|
||||
return Message.fromDiscord(await this.api.patch<DiscordMessage>(`/channels/${clone.channel_id}/messages/${clone.id}`, clone));
|
||||
};
|
||||
|
||||
// Interaction methods
|
||||
public ack = async (type: InteractionResponseType = InteractionResponseType.Pong, data?: Record<string, any>) => {
|
||||
if (isNil(this.interaction)) {
|
||||
throw new Error('Cannot acknowledge an interaction without an interaction payload');
|
||||
}
|
||||
|
||||
return this.api.post(`/interactions/${this.interaction.id}/${this.interaction.token}/callback`, {
|
||||
type,
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
public defer = async () => {
|
||||
return this.ack(InteractionResponseType.DeferredMessage);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Message } from './message';
|
||||
|
||||
type EmbedFooter = {
|
||||
text: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
};
|
||||
|
||||
type EmbedImage = {
|
||||
url?: string;
|
||||
proxy_url?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
type EmbedAuthor = {
|
||||
name: string;
|
||||
url?: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
};
|
||||
|
||||
type EmbedVideo = {
|
||||
url?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
type EmbedField = {
|
||||
name: string;
|
||||
value: string;
|
||||
inline?: boolean;
|
||||
};
|
||||
|
||||
export class Embed {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
timestamp?: string;
|
||||
color?: number;
|
||||
footer?: EmbedFooter;
|
||||
image?: EmbedImage;
|
||||
thumbnail?: EmbedImage;
|
||||
video?: EmbedVideo;
|
||||
provider?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
};
|
||||
author?: EmbedImage & {
|
||||
name?: string;
|
||||
};
|
||||
fields: EmbedField[];
|
||||
|
||||
constructor() {
|
||||
this.fields = [];
|
||||
}
|
||||
|
||||
setTitle = (title: string) => {
|
||||
this.title = title;
|
||||
return this;
|
||||
};
|
||||
|
||||
setDescription = (description: string) => {
|
||||
this.description = description;
|
||||
return this;
|
||||
};
|
||||
|
||||
setUrl = (url: string) => {
|
||||
this.url = url;
|
||||
return this;
|
||||
};
|
||||
|
||||
setTimestamp = (timestamp: string) => {
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
};
|
||||
|
||||
setColor = (color: number | string) => {
|
||||
this.color = typeof color === 'string' ? parseInt(`0x${color.replace('#', '')}`) : color;
|
||||
return this;
|
||||
};
|
||||
|
||||
setFooter = (footer: EmbedFooter) => {
|
||||
this.footer = footer;
|
||||
return this;
|
||||
};
|
||||
|
||||
setImage = (image: EmbedImage) => {
|
||||
this.image = image;
|
||||
return this;
|
||||
};
|
||||
|
||||
setThumbnail = (thumbnail: EmbedImage) => {
|
||||
this.thumbnail = thumbnail;
|
||||
return this;
|
||||
};
|
||||
|
||||
setVideo = (video: EmbedVideo) => {
|
||||
this.video = video;
|
||||
return this;
|
||||
};
|
||||
|
||||
setProvider = (provider: { name?: string; url?: string }) => {
|
||||
this.provider = provider;
|
||||
return this;
|
||||
};
|
||||
|
||||
setAuthor = (author: EmbedAuthor) => {
|
||||
this.author = author;
|
||||
return this;
|
||||
};
|
||||
|
||||
addField = (field: EmbedField) => {
|
||||
this.fields.push(field);
|
||||
return this;
|
||||
};
|
||||
|
||||
addFields = (fields: EmbedField[]) => {
|
||||
this.fields.push(...fields);
|
||||
return this;
|
||||
};
|
||||
|
||||
toMessage = () => {
|
||||
return new Message().addEmbed(this);
|
||||
};
|
||||
|
||||
toJSON = () => {
|
||||
return {
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
url: this.url,
|
||||
timestamp: this.timestamp,
|
||||
color: this.color,
|
||||
footer: this.footer,
|
||||
image: this.image,
|
||||
thumbnail: this.thumbnail,
|
||||
video: this.video,
|
||||
provider: this.provider,
|
||||
author: this.author,
|
||||
fields: this.fields
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export class Emoji {
|
||||
version: number;
|
||||
roles: string[];
|
||||
require_colons: boolean;
|
||||
name: string;
|
||||
managed: boolean;
|
||||
id: string;
|
||||
available: boolean;
|
||||
animated: boolean;
|
||||
|
||||
constructor(data: any) {
|
||||
this.version = data.version;
|
||||
this.roles = data.roles;
|
||||
this.require_colons = data.require_colons;
|
||||
this.name = data.name;
|
||||
this.managed = data.managed;
|
||||
this.id = data.id;
|
||||
this.available = data.available;
|
||||
this.animated = data.animated;
|
||||
}
|
||||
|
||||
static fromAPIResponse(data: any): Emoji {
|
||||
return new Emoji(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { GatewayObject } from "./base";
|
||||
import { Channel } from "./channel";
|
||||
import { Trait } from "./common";
|
||||
import { Emoji } from "./emoji";
|
||||
import { Member } from "./member";
|
||||
import { Role } from "./role";
|
||||
|
||||
export class Guild extends GatewayObject {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
banner: string | null;
|
||||
owner_id: string;
|
||||
// emojis: Emoji[];
|
||||
// roles: Role[];
|
||||
|
||||
constructor(data: Guild) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.icon = data.icon;
|
||||
this.banner = data.banner;
|
||||
this.owner_id = data.owner_id;
|
||||
// this.emojis = data.emojis;
|
||||
// this.roles = data.roles;
|
||||
}
|
||||
|
||||
public static [Trait.fromDiscord]<T = Guild>(data: DiscordGuild): T {
|
||||
return new Guild({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: data.icon,
|
||||
banner: data.banner,
|
||||
owner_id: data.owner_id,
|
||||
// emojis: data.emojis,
|
||||
// roles: data.roles,
|
||||
}) as T;
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscordGuild {
|
||||
system_channel_id: string | null;
|
||||
premium_progress_bar_enabled: boolean;
|
||||
region: string;
|
||||
application_id: string | null;
|
||||
premium_tier: number;
|
||||
presences: any[];
|
||||
discovery_splash: string | null;
|
||||
latest_onboarding_question_id: string | null;
|
||||
unavailable: boolean;
|
||||
afk_channel_id: string | null;
|
||||
large: boolean;
|
||||
nsfw: boolean;
|
||||
max_video_channel_users: number;
|
||||
stickers: any[];
|
||||
id: string;
|
||||
premium_subscription_count: number;
|
||||
stage_instances: any[];
|
||||
preferred_locale: string;
|
||||
guild_scheduled_events: any[];
|
||||
member_count: number;
|
||||
name: string;
|
||||
default_message_notifications: number;
|
||||
system_channel_flags: number;
|
||||
hub_type: string | null;
|
||||
lazy: boolean;
|
||||
soundboard_sounds: any[];
|
||||
threads: any[];
|
||||
banner: string | null;
|
||||
description: string | null;
|
||||
clan: string | null;
|
||||
incidents_data: string | null;
|
||||
application_command_counts: any;
|
||||
embedded_activities: any[];
|
||||
icon: string | null;
|
||||
home_header: string | null;
|
||||
joined_at: string;
|
||||
safety_alerts_channel_id: string | null;
|
||||
verification_level: number;
|
||||
max_stage_video_channel_users: number;
|
||||
emojis: Emoji[];
|
||||
owner_id: string;
|
||||
public_updates_channel_id: string | null;
|
||||
features: string[];
|
||||
rules_channel_id: string | null;
|
||||
roles: Role[];
|
||||
afk_timeout: number;
|
||||
inventory_settings: any;
|
||||
activity_instances: any[];
|
||||
max_members: number;
|
||||
channels: Channel[];
|
||||
voice_states: any[];
|
||||
splash: string | null;
|
||||
mfa_level: number;
|
||||
members: Member[];
|
||||
version: number;
|
||||
vanity_url_code: string | null;
|
||||
nsfw_level: number;
|
||||
explicit_content_filter: number;
|
||||
|
||||
constructor(data: any) {
|
||||
this.system_channel_id = data.system_channel_id;
|
||||
this.premium_progress_bar_enabled = data.premium_progress_bar_enabled;
|
||||
this.region = data.region;
|
||||
this.application_id = data.application_id;
|
||||
this.premium_tier = data.premium_tier;
|
||||
this.presences = data.presences;
|
||||
this.discovery_splash = data.discovery_splash;
|
||||
this.latest_onboarding_question_id = data.latest_onboarding_question_id;
|
||||
this.unavailable = data.unavailable;
|
||||
this.afk_channel_id = data.afk_channel_id;
|
||||
this.large = data.large;
|
||||
this.nsfw = data.nsfw;
|
||||
this.max_video_channel_users = data.max_video_channel_users;
|
||||
this.stickers = data.stickers;
|
||||
this.id = data.id;
|
||||
this.premium_subscription_count = data.premium_subscription_count;
|
||||
this.stage_instances = data.stage_instances;
|
||||
this.preferred_locale = data.preferred_locale;
|
||||
this.guild_scheduled_events = data.guild_scheduled_events;
|
||||
this.member_count = data.member_count;
|
||||
this.name = data.name;
|
||||
this.default_message_notifications = data.default_message_notifications;
|
||||
this.system_channel_flags = data.system_channel_flags;
|
||||
this.hub_type = data.hub_type;
|
||||
this.lazy = data.lazy;
|
||||
this.soundboard_sounds = data.soundboard_sounds;
|
||||
this.threads = data.threads;
|
||||
this.banner = data.banner;
|
||||
this.description = data.description;
|
||||
this.clan = data.clan;
|
||||
this.incidents_data = data.incidents_data;
|
||||
this.application_command_counts = data.application_command_counts;
|
||||
this.embedded_activities = data.embedded_activities;
|
||||
this.icon = data.icon;
|
||||
this.home_header = data.home_header;
|
||||
this.joined_at = data.joined_at;
|
||||
this.safety_alerts_channel_id = data.safety_alerts_channel_id;
|
||||
this.verification_level = data.verification_level;
|
||||
this.max_stage_video_channel_users = data.max_stage_video_channel_users;
|
||||
this.emojis = data.emojis.map((emoji: any) => Emoji.fromAPIResponse(emoji));
|
||||
this.owner_id = data.owner_id;
|
||||
this.public_updates_channel_id = data.public_updates_channel_id;
|
||||
this.features = data.features;
|
||||
this.rules_channel_id = data.rules_channel_id;
|
||||
this.roles = data.roles.map((role: any) => Role.fromAPIResponse(role));
|
||||
this.afk_timeout = data.afk_timeout;
|
||||
this.inventory_settings = data.inventory_settings;
|
||||
this.activity_instances = data.activity_instances;
|
||||
this.max_members = data.max_members;
|
||||
this.channels = data.channels.map((channel: any) => Channel.fromAPIResponse(channel));
|
||||
this.voice_states = data.voice_states;
|
||||
this.splash = data.splash;
|
||||
this.mfa_level = data.mfa_level;
|
||||
this.members = data.members.map((member: any) => Member.fromAPIResponse(member));
|
||||
this.version = data.version;
|
||||
this.vanity_url_code = data.vanity_url_code;
|
||||
this.nsfw_level = data.nsfw_level;
|
||||
this.explicit_content_filter = data.explicit_content_filter;
|
||||
}
|
||||
|
||||
static fromAPIResponse(data: any): DiscordGuild {
|
||||
return new DiscordGuild(data);
|
||||
}
|
||||
}
|
||||
+168
-5
@@ -1,8 +1,171 @@
|
||||
import { Client, Context } from '..';
|
||||
import { exists } from "../utils";
|
||||
import { ApplicationCommandContext, ApplicationCommandOptionType, type ApplicationCommandOption } from "./applicationCommand";
|
||||
import { Trait, type EventPayload } from "./common";
|
||||
import type { Context } from "./context";
|
||||
import type { DiscordGuild } from "./guild";
|
||||
import type { DiscordMessage } from "./message";
|
||||
import type { DiscordUser, User } from "./user";
|
||||
|
||||
export abstract class Handler {
|
||||
abstract events: string | string[];
|
||||
abstract execute: HandlerFunction;
|
||||
// TODO: Flesh out InteractionPayload type
|
||||
export type InteractionPayload = {
|
||||
id: string;
|
||||
token: string;
|
||||
type: number;
|
||||
data?: InteractionData;
|
||||
guild?: DiscordGuild;
|
||||
guild_id: string;
|
||||
// TODO: Add channel class
|
||||
// channel?: DiscordChannel;
|
||||
channel_id: string;
|
||||
// TODO: Add member class
|
||||
member?: Record<string, any> & { user: DiscordUser };
|
||||
user?: DiscordUser
|
||||
message?: DiscordMessage;
|
||||
};
|
||||
|
||||
// TODO: Flesh out InteractionData type
|
||||
export type InteractionData = InteractionCommpoentData | InteractionCommandData;
|
||||
|
||||
export type InteractionCommpoentData = {
|
||||
custom_id: string;
|
||||
component_type: number;
|
||||
values?: any[];
|
||||
resolved?: {
|
||||
users?: Record<string, DiscordUser>;
|
||||
members?: Record<string, DiscordUser>;
|
||||
roles?: Record<string, any>;
|
||||
channels?: Record<string, any>;
|
||||
messages?: Record<string, any>;
|
||||
attachments?: Record<string, any>;
|
||||
};
|
||||
};
|
||||
|
||||
export type InteractionCommandData = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
resolved?: {
|
||||
users?: Record<string, DiscordUser>;
|
||||
members?: Record<string, DiscordUser>;
|
||||
roles?: Record<string, any>;
|
||||
channels?: Record<string, any>;
|
||||
messages?: Record<string, any>;
|
||||
attachments?: Record<string, any>;
|
||||
}
|
||||
options?: {
|
||||
name: string;
|
||||
type: ApplicationCommandOptionType;
|
||||
value?: any;
|
||||
options?: InteractionCommandData[];
|
||||
focused?: boolean;
|
||||
}[];
|
||||
guild_id?: string;
|
||||
target_id?: string;
|
||||
}
|
||||
|
||||
export type HandlerFunction = (client: Client, context: Context, data: Record<string, any>) => void;
|
||||
export const OptionConstructorMap = {
|
||||
[ApplicationCommandOptionType.String]: String,
|
||||
[ApplicationCommandOptionType.Integer]: Number,
|
||||
[ApplicationCommandOptionType.Boolean]: Boolean,
|
||||
[ApplicationCommandOptionType.Number]: Number,
|
||||
[ApplicationCommandOptionType.User]: String, // Could be a user ID or object depending on your use case
|
||||
[ApplicationCommandOptionType.Channel]: String, // Could be a channel ID or object
|
||||
[ApplicationCommandOptionType.Role]: String, // Could be a role ID or object
|
||||
[ApplicationCommandOptionType.Mentionable]: String, // Could be a mentionable ID or object
|
||||
[ApplicationCommandOptionType.Attachment]: String, // Could represent attachment IDs
|
||||
[ApplicationCommandOptionType.SubCommand]: String, // Handled by `options`
|
||||
[ApplicationCommandOptionType.SubCommandGroup]: String, // Handled by `options`
|
||||
};
|
||||
|
||||
export type OptionTypeMap = {
|
||||
[ApplicationCommandOptionType.String]: string,
|
||||
[ApplicationCommandOptionType.Integer]: number,
|
||||
[ApplicationCommandOptionType.Boolean]: boolean,
|
||||
[ApplicationCommandOptionType.Number]: number,
|
||||
[ApplicationCommandOptionType.User]: User, // Could be a user ID or object depending on your use case
|
||||
[ApplicationCommandOptionType.Channel]: string, // Could be a channel ID or object
|
||||
[ApplicationCommandOptionType.Role]: string, // Could be a role ID or object
|
||||
[ApplicationCommandOptionType.Mentionable]: string, // Could be a mentionable ID or object
|
||||
[ApplicationCommandOptionType.Attachment]: string, // Could represent attachment IDs
|
||||
[ApplicationCommandOptionType.SubCommand]: string, // Handled by `options`
|
||||
[ApplicationCommandOptionType.SubCommandGroup]: string, // Handled by `options`
|
||||
};
|
||||
|
||||
// Recursive type to map ApplicationCommandOption to arguments
|
||||
export type ArgsFromOptions<T extends readonly ApplicationCommandOption[]> = {
|
||||
[Option in T[number] as Option['name']]: Option extends { options: readonly ApplicationCommandOption[] }
|
||||
? ArgsFromOptions<Option['options']> // Recursive inference for subcommands
|
||||
: Option['choices'] extends readonly { value: infer U }[]
|
||||
? Option['required'] extends true // If required, keep as is; otherwise, make optional
|
||||
? U
|
||||
: U | undefined
|
||||
: Option['type'] extends keyof OptionTypeMap
|
||||
? Option['required'] extends true // If required, keep as is; otherwise, make optional
|
||||
? OptionTypeMap[Option['type']]
|
||||
: OptionTypeMap[Option['type']] | undefined
|
||||
: Option['required'] extends true
|
||||
? unknown
|
||||
: unknown | undefined;
|
||||
};
|
||||
|
||||
export type CommandHandlerExecuteFunction<T extends ApplicationCommandOption[]> = (context: Context, args: ArgsFromOptions<T>) => Promise<any> | any;
|
||||
|
||||
export type CommandHandlerInput<T extends ApplicationCommandOption[]> = {
|
||||
args: T;
|
||||
description: string;
|
||||
contexts?: ApplicationCommandContext[];
|
||||
execute: CommandHandlerExecuteFunction<T>;
|
||||
};
|
||||
|
||||
export class CommandHandler<T extends ApplicationCommandOption[]> {
|
||||
args: T;
|
||||
description: string;
|
||||
contexts?: ApplicationCommandContext[];
|
||||
[Trait.execute]!: CommandHandlerExecuteFunction<T>;
|
||||
|
||||
get execute() {
|
||||
return this[Trait.execute];
|
||||
}
|
||||
|
||||
set execute(fn: CommandHandlerExecuteFunction<T>) {
|
||||
this[Trait.execute] = fn;
|
||||
}
|
||||
|
||||
constructor(input: CommandHandlerInput<T>) {
|
||||
// Validate option names
|
||||
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
||||
for (const name of input.args.map((arg) => arg.name)) {
|
||||
if (/^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u.test(name) === false) {
|
||||
throw new Error(`Invalid option name: ${name}`);
|
||||
}
|
||||
}
|
||||
this.args = input.args;
|
||||
this.description = input.description
|
||||
this.contexts = input.contexts;
|
||||
this[Trait.execute] = input.execute;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
options: this.args,
|
||||
description: this.description,
|
||||
contexts: this.contexts,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to infer type of `args`
|
||||
export function createCommandHandler<T extends ApplicationCommandOption[]>(
|
||||
handler: CommandHandlerInput<T>
|
||||
): CommandHandler<T> {
|
||||
return new CommandHandler(handler);
|
||||
}
|
||||
|
||||
export type CommandHandlerFunction = (context: Context, args: any[]) => Promise<any> | any;
|
||||
export type EventHandlerFunction = (context: Context | null, payload: EventPayload) => Promise<any> | any;
|
||||
export type InteractionHandlerFunction<P = InteractionData> = (context: Context, payload: P) => Promise<any> | any;
|
||||
|
||||
export type HandlerWithoutContext = EventHandlerFunction;
|
||||
export type HanderWithContext = InteractionHandlerFunction | CommandHandlerFunction | CommandHandler<ApplicationCommandOption[]>;
|
||||
|
||||
export type Handler = InteractionHandlerFunction<InteractionCommpoentData> | InteractionHandlerFunction<InteractionCommandData> | EventHandlerFunction | CommandHandlerFunction | CommandHandler<ApplicationCommandOption[]>;
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './common';
|
||||
export * from './command';
|
||||
export * from './handler';
|
||||
export * from './interaction';
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Client, Message } from '..';
|
||||
|
||||
export abstract class Interaction {
|
||||
abstract custom_id: string | string[];
|
||||
abstract execute: InteractionFunction;
|
||||
}
|
||||
|
||||
export type InteractionFunction = (client: Client, context: InteractionContext, data: Record<string, any>) => void;
|
||||
|
||||
export enum InteractionResponseType {
|
||||
Pong = 1,
|
||||
Reply = 4,
|
||||
DeferredReply = 5,
|
||||
DeferredUpdateMessage = 6,
|
||||
UpdateMessage = 7,
|
||||
AutocompleteResult = 8,
|
||||
Modal = 9
|
||||
}
|
||||
|
||||
export class InteractionContext {
|
||||
public client: Client;
|
||||
public interaction: Record<string, any>;
|
||||
constructor({ client, interaction }: { client: Client; interaction: Record<string, any> }) {
|
||||
this.client = client;
|
||||
this.interaction = interaction;
|
||||
}
|
||||
|
||||
ack = (type: InteractionResponseType = InteractionResponseType.Pong, data?: Record<string, any>) => {
|
||||
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);
|
||||
}
|
||||
return this.ack(InteractionResponseType.UpdateMessage, {
|
||||
...message
|
||||
});
|
||||
};
|
||||
|
||||
editOriginal = (message: Message | string) => {
|
||||
if (typeof message === 'string') {
|
||||
message = new Message().setContent(message);
|
||||
}
|
||||
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 });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export class Member {
|
||||
user: any;
|
||||
roles: string[];
|
||||
premium_since: string | null;
|
||||
pending: boolean;
|
||||
nick: string | null;
|
||||
mute: boolean;
|
||||
joined_at: string;
|
||||
flags: number;
|
||||
deaf: boolean;
|
||||
communication_disabled_until: string | null;
|
||||
banner: string | null;
|
||||
avatar: string | null;
|
||||
|
||||
constructor(data: any) {
|
||||
this.user = data.user;
|
||||
this.roles = data.roles;
|
||||
this.premium_since = data.premium_since;
|
||||
this.pending = data.pending;
|
||||
this.nick = data.nick;
|
||||
this.mute = data.mute;
|
||||
this.joined_at = data.joined_at;
|
||||
this.flags = data.flags;
|
||||
this.deaf = data.deaf;
|
||||
this.communication_disabled_until = data.communication_disabled_until;
|
||||
this.banner = data.banner;
|
||||
this.avatar = data.avatar;
|
||||
}
|
||||
|
||||
static fromAPIResponse(data: any): Member {
|
||||
return new Member(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { exists, isNil } from '../utils';
|
||||
import { GatewayObject } from './base';
|
||||
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
|
||||
import type { Embed } from './embed';
|
||||
import type { Guild } from './guild';
|
||||
import { type DiscordUser, User } from './user';
|
||||
|
||||
export type Component = Partial<Button | SelectMenu> & {
|
||||
type: ComponentType;
|
||||
content?: string;
|
||||
components?: Component[];
|
||||
};
|
||||
|
||||
// {
|
||||
// content?: string;
|
||||
// type?: ComponentType;
|
||||
// description?: string;
|
||||
// label?: string;
|
||||
// value?: string;
|
||||
// style?: ComponentStyle;
|
||||
// custom_id?: string;
|
||||
// url?: string;
|
||||
// disabled?: boolean;
|
||||
// required?: boolean;
|
||||
// emoji?: {
|
||||
// name: string;
|
||||
// id?: string;
|
||||
// animated?: boolean;
|
||||
// }
|
||||
// options?: Component[];
|
||||
// placeholder?: string;
|
||||
// min_values?: number;
|
||||
// max_values?: number;
|
||||
// components?: Component[];
|
||||
// }
|
||||
|
||||
export type Emoji = {
|
||||
name: string | null;
|
||||
id: string | null;
|
||||
animated?: boolean;
|
||||
};
|
||||
|
||||
export type Button = {
|
||||
style: ComponentStyle;
|
||||
label: string;
|
||||
emoji?: Emoji;
|
||||
custom_id?: string;
|
||||
url?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type SelectMenu = {
|
||||
custom_id: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
emoji?: Emoji;
|
||||
default?: boolean;
|
||||
}[];
|
||||
placeholder?: string;
|
||||
min_values?: number;
|
||||
max_values?: number;
|
||||
};
|
||||
|
||||
export enum ComponentStyle {
|
||||
Primary = 1,
|
||||
Secondary,
|
||||
Success,
|
||||
Danger,
|
||||
Link
|
||||
}
|
||||
|
||||
export enum ComponentType {
|
||||
ActionRow = 1,
|
||||
Button,
|
||||
SelectMenu,
|
||||
TextInput,
|
||||
UserSelect,
|
||||
RoleSelect,
|
||||
MentionableSelect,
|
||||
ChannelSelect
|
||||
}
|
||||
|
||||
export type HydratedMessage<T extends Array<Expectation>> = MessagePayload & {
|
||||
// channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
||||
reference: Extract<Expectation.Message, T[number]> extends never ? undefined : MessagePayload;
|
||||
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
||||
}
|
||||
|
||||
export type MessagePayload = Message & {
|
||||
id: string;
|
||||
author: User;
|
||||
channel_id: string;
|
||||
content: string;
|
||||
embeds: Embed[];
|
||||
components: Component[];
|
||||
message_reference?: {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
export class Message extends GatewayObject {
|
||||
id?: string;
|
||||
author?: User;
|
||||
channel_id?: string;
|
||||
guild_id?: string;
|
||||
public content: string;
|
||||
public embeds: Embed[] = [];
|
||||
public components: Component[] = [];
|
||||
public message_reference?: {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
timestamp?: Date;
|
||||
|
||||
constructor(data?: Partial<Message>) {
|
||||
super();
|
||||
this.id = data?.id;
|
||||
this.author = data?.author;
|
||||
this.channel_id = data?.channel_id;
|
||||
this.guild_id = data?.guild_id;
|
||||
this.content = data?.content ?? '';
|
||||
this.message_reference = data?.message_reference;
|
||||
this.timestamp = data?.timestamp;
|
||||
};
|
||||
|
||||
// NOTE: Overriden here because the `fromDiscord` method returns a `MessagePayload` object
|
||||
// and the GatewayObject getter overrides the return type to `Message`
|
||||
// TODO: Fix type in GatewayObject so this override is not needed
|
||||
public static get fromDiscord() {
|
||||
return this[Trait.fromDiscord];
|
||||
}
|
||||
|
||||
public static [Trait.fromDiscord]<T = MessagePayload>(data: DiscordMessage): T {
|
||||
return new Message({
|
||||
id: data.id,
|
||||
author: User[fromDiscord](data.author),
|
||||
channel_id: data.channel_id,
|
||||
guild_id: data.guild_id,
|
||||
content: data.content,
|
||||
// TODO: Add embeds
|
||||
// embeds: data.embeds.map(embed => Embed.fromDiscord(embed)),
|
||||
// TODO: Add components
|
||||
// components: data.components,
|
||||
message_reference: exists(data.message_reference) ? {
|
||||
id: data.message_reference.message_id,
|
||||
channel_id: data.message_reference.channel_id,
|
||||
guild_id: data.message_reference.guild_id
|
||||
} : undefined,
|
||||
timestamp: new Date(data.timestamp)
|
||||
}) as T;
|
||||
};
|
||||
|
||||
public static button(config: Button): Component {
|
||||
return {
|
||||
type: ComponentType.Button,
|
||||
...config
|
||||
};
|
||||
};
|
||||
|
||||
public static selectMenu(config: SelectMenu): Component {
|
||||
return {
|
||||
type: ComponentType.SelectMenu,
|
||||
...config
|
||||
};
|
||||
};
|
||||
|
||||
public setContent = (content: string) => {
|
||||
this.content = content;
|
||||
return this;
|
||||
};
|
||||
|
||||
public addEmbed = (embed: Embed) => {
|
||||
this.embeds.push(embed);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addComponent = (content: Component | string, ...components: Component[]) => {
|
||||
if (typeof content === 'string') {
|
||||
this.components.push({
|
||||
type: ComponentType.ActionRow,
|
||||
content,
|
||||
components
|
||||
});
|
||||
} else {
|
||||
this.components.push({
|
||||
type: ComponentType.ActionRow,
|
||||
components: [content, ...components]
|
||||
});
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
public setReference = (message: Message | Partial<Message>) => {
|
||||
if (isNil(message.id) || isNil(message.channel_id)) {
|
||||
throw new Error('Message ID and Channel ID are required');
|
||||
}
|
||||
|
||||
let ref: Message;
|
||||
|
||||
if (!(message instanceof Message)) {
|
||||
ref = new Message(message);
|
||||
} else {
|
||||
ref = message;
|
||||
}
|
||||
|
||||
this.message_reference = {
|
||||
id: ref.id!,
|
||||
channel_id: ref.channel_id!,
|
||||
guild_id: ref.guild_id
|
||||
};
|
||||
return this;
|
||||
};
|
||||
|
||||
public toJSON = () => {
|
||||
return {
|
||||
content: this.content,
|
||||
embeds: this.embeds,
|
||||
components: this.components,
|
||||
message_reference: this.message_reference ? {
|
||||
message_id: this.message_reference.id,
|
||||
channel_id: this.message_reference.channel_id,
|
||||
guild_id: this.message_reference.guild_id
|
||||
} : undefined
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Raw API Object
|
||||
export type DiscordMessage = {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
author: DiscordUser;
|
||||
// member?: DiscordGuildMember;
|
||||
content: string;
|
||||
// ISO8601 timestamp
|
||||
timestamp: string;
|
||||
edited_timestamp: string | null;
|
||||
tts: boolean;
|
||||
mention_everyone: boolean;
|
||||
mentions: DiscordUser[];
|
||||
mention_roles: string[];
|
||||
mention_channels?: { id: string; guild_id: string; type: number; name: string }[];
|
||||
attachments: { id: string; filename: string; url: string; proxy_url: string; size: number }[];
|
||||
// embeds: DiscordEmbed[];
|
||||
reactions?: { count: number; me: boolean; emoji: Emoji }[];
|
||||
nonce?: string | number;
|
||||
pinned: boolean;
|
||||
webhook_id?: string;
|
||||
type: number;
|
||||
activity?: { type: number; party_id?: string };
|
||||
application?: { id: string; cover_image?: string; description: string; icon?: string; name: string };
|
||||
application_id?: string;
|
||||
message_reference?: { message_id: string; channel_id: string; guild_id?: string; fail_if_not_exists?: boolean };
|
||||
flags?: number;
|
||||
referenced_message?: DiscordMessage | null;
|
||||
interaction?: { id: string; type: number; name: string; user: DiscordUser };
|
||||
thread?: { id: string; name: string; type: number };
|
||||
components?: any[];
|
||||
sticker_items?: { id: string; name: string; format_type: number }[];
|
||||
stickers?: {
|
||||
id: string;
|
||||
pack_id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
tags: string;
|
||||
asset: string;
|
||||
preview_asset?: string;
|
||||
format_type: number;
|
||||
}[];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export class Role {
|
||||
version: number;
|
||||
unicode_emoji: string | null;
|
||||
tags: any;
|
||||
position: number;
|
||||
permissions: string;
|
||||
name: string;
|
||||
mentionable: boolean;
|
||||
managed: boolean;
|
||||
id: string;
|
||||
icon: string | null;
|
||||
hoist: boolean;
|
||||
flags: number;
|
||||
color: number;
|
||||
|
||||
constructor(data: any) {
|
||||
this.version = data.version;
|
||||
this.unicode_emoji = data.unicode_emoji;
|
||||
this.tags = data.tags;
|
||||
this.position = data.position;
|
||||
this.permissions = data.permissions;
|
||||
this.name = data.name;
|
||||
this.mentionable = data.mentionable;
|
||||
this.managed = data.managed;
|
||||
this.id = data.id;
|
||||
this.icon = data.icon;
|
||||
this.hoist = data.hoist;
|
||||
this.flags = data.flags;
|
||||
this.color = data.color;
|
||||
}
|
||||
|
||||
static fromAPIResponse(data: any): Role {
|
||||
return new Role(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { exists } from "../utils";
|
||||
import { GatewayObject } from "./base";
|
||||
import { Trait } from "./common";
|
||||
|
||||
export class User extends GatewayObject {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
display_name: string;
|
||||
avatar: string;
|
||||
bot?: boolean;
|
||||
system?: boolean;
|
||||
mfa_enabled?: boolean;
|
||||
banner?: string;
|
||||
accent_color?: number;
|
||||
locale?: string;
|
||||
verified?: boolean;
|
||||
email?: string;
|
||||
// TODO: Add the rest of the properties
|
||||
|
||||
get avatarURL() {
|
||||
// Check if avatar is an animated gif
|
||||
if (this.avatar?.startsWith('a_')) {
|
||||
return `https://cdn.discordapp.com/avatars/${this.id}/${this.avatar}.gif`;
|
||||
} else {
|
||||
return `https://cdn.discordapp.com/avatars/${this.id}/${this.avatar}.png`;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(data: Partial<User> & { id: string; username: string; discriminator: string; display_name: string; avatar: string }) {
|
||||
super();
|
||||
this.id = data.id;
|
||||
this.username = data.username;
|
||||
this.discriminator = data.discriminator;
|
||||
this.display_name = data.display_name;
|
||||
this.avatar = data.avatar;
|
||||
this.bot = data.bot;
|
||||
this.system = data.system;
|
||||
this.mfa_enabled = data.mfa_enabled;
|
||||
this.banner = data.banner;
|
||||
this.accent_color = data.accent_color;
|
||||
this.locale = data.locale;
|
||||
this.verified = data.verified;
|
||||
this.email = data.email;
|
||||
}
|
||||
|
||||
// TODO: Add a clone method to subclasses
|
||||
// public [Trait.clone]<T = User>(data: DiscordUser): T { ... };
|
||||
|
||||
public static [Trait.fromDiscord]<T = User>(data: DiscordUser): T {
|
||||
return new User({
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
discriminator: data.discriminator,
|
||||
display_name: data.global_name,
|
||||
avatar: data.avatar,
|
||||
bot: data.bot,
|
||||
system: data.system,
|
||||
mfa_enabled: data.mfa_enabled,
|
||||
banner: data.banner,
|
||||
accent_color: data.accent_color,
|
||||
locale: data.locale,
|
||||
verified: data.verified,
|
||||
email: data.email
|
||||
}) as T;
|
||||
};
|
||||
}
|
||||
|
||||
// id snowflake the user's id identify
|
||||
// username string the user's username, not unique across the platform identify
|
||||
// discriminator string the user's Discord-tag identify
|
||||
// global_name ?string the user's display name, if it is set. For bots, this is the application name identify
|
||||
// avatar ?string the user's avatar hash identify
|
||||
// bot? boolean whether the user belongs to an OAuth2 application identify
|
||||
// system? boolean whether the user is an Official Discord System user (part of the urgent message system) identify
|
||||
// mfa_enabled? boolean whether the user has two factor enabled on their account identify
|
||||
// banner? ?string the user's banner hash identify
|
||||
// accent_color? ?integer the user's banner color encoded as an integer representation of hexadecimal color code identify
|
||||
// locale? string the user's chosen language option identify
|
||||
// verified? boolean whether the email on this account has been verified email
|
||||
// email? ?string the user's email email
|
||||
// flags? integer the flags on a user's account identify
|
||||
// premium_type? integer the type of Nitro subscription on a user's account identify
|
||||
// public_flags? integer the public flags on a user's account identify
|
||||
// avatar_decoration_data?
|
||||
|
||||
export class DiscordUser {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
global_name: string;
|
||||
avatar: string;
|
||||
bot?: boolean;
|
||||
system?: boolean;
|
||||
mfa_enabled?: boolean;
|
||||
banner?: string;
|
||||
accent_color?: number;
|
||||
locale?: string;
|
||||
verified?: boolean;
|
||||
email?: string;
|
||||
flags?: number;
|
||||
premium_type?: number;
|
||||
public_flags?: number;
|
||||
avatar_decoration_data?: Record<string, any>;
|
||||
|
||||
constructor(data: DiscordUser) {
|
||||
this.id = data.id;
|
||||
this.username = data.username;
|
||||
this.discriminator = data.discriminator;
|
||||
this.global_name = data.global_name;
|
||||
this.avatar = data.avatar;
|
||||
this.bot = data.bot;
|
||||
this.system = data.system;
|
||||
this.mfa_enabled = data.mfa_enabled;
|
||||
this.banner = data.banner;
|
||||
this.accent_color = data.accent_color;
|
||||
this.locale = data.locale;
|
||||
this.verified = data.verified;
|
||||
this.email = data.email;
|
||||
this.flags = data.flags;
|
||||
this.premium_type = data.premium_type;
|
||||
this.public_flags = data.public_flags;
|
||||
this.avatar_decoration_data = data.avatar_decoration_data;
|
||||
}
|
||||
|
||||
static fromAPIResponse(data: any): DiscordUser {
|
||||
return new DiscordUser({
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
discriminator: data.discriminator,
|
||||
global_name: data.global_name,
|
||||
avatar: data.avatar,
|
||||
bot: data.bot,
|
||||
system: data.system,
|
||||
mfa_enabled: data.mfa_enabled,
|
||||
banner: data.banner,
|
||||
accent_color: data.accent_color,
|
||||
locale: data.locale,
|
||||
verified: data.verified,
|
||||
email: data.email,
|
||||
flags: data.flags,
|
||||
premium_type: data.premium_type,
|
||||
public_flags: data.public_flags,
|
||||
avatar_decoration_data: data.avatar_decoration_data
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const isNil = (value: any): value is null | undefined => value === null || value === undefined;
|
||||
export const exists = <T>(value: T | null | undefined): value is NonNullable<T> => !isNil(value);
|
||||
export const isObject = (value: any): value is Record<string, any> => typeof value === 'object' && !Array.isArray(value);
|
||||
export const log = (...args: any[]) => {
|
||||
const LOG_LEVEL = process.env.UNICORD_LOG_LEVEL?.toLocaleLowerCase();
|
||||
|
||||
if (LOG_LEVEL === 'debug') {
|
||||
const timestamp = `\x1b[37m[${new Date().toLocaleString()}]\x1b[0m`;
|
||||
const prefix = `\x1b[35m[unicord]`;
|
||||
console.log(`${timestamp} ${prefix} \x1b[0m`, ...args);
|
||||
}
|
||||
};
|
||||
@@ -1,147 +0,0 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import { Client, ComponentStyle, Intent, Message } from '../src';
|
||||
|
||||
const bot = new Client({
|
||||
token: process.env.BOT_TOKEN!,
|
||||
application_id: process.env.APPLICATION_ID!,
|
||||
intents: [Intent.GUILDS, Intent.GUILD_MESSAGES, Intent.GUILD_MESSAGE_REACTIONS, Intent.MESSAGE_CONTENT]
|
||||
});
|
||||
|
||||
bot.commands.register('ping', (client, context, args) => {
|
||||
context.reply('Pong!', true);
|
||||
});
|
||||
|
||||
bot.commands.register('demo', (_client, context, _args) => {
|
||||
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: '1'
|
||||
}),
|
||||
Message.button({
|
||||
label: 'No me!',
|
||||
style: ComponentStyle.Secondary,
|
||||
custom_id: '2',
|
||||
disabled: true
|
||||
}),
|
||||
Message.button({
|
||||
label: 'Never, me!',
|
||||
style: ComponentStyle.Success,
|
||||
custom_id: '3'
|
||||
}),
|
||||
// Link requires url and can not have custom_id
|
||||
Message.button({
|
||||
label: 'Screw Them.',
|
||||
style: ComponentStyle.Link,
|
||||
url: 'https://google.com'
|
||||
}),
|
||||
Message.button({
|
||||
label: 'Emoji',
|
||||
style: ComponentStyle.Secondary,
|
||||
custom_id: '4',
|
||||
emoji: {
|
||||
name: '👍',
|
||||
id: null,
|
||||
animated: false
|
||||
}
|
||||
})
|
||||
)
|
||||
.addComponent(
|
||||
Message.button({
|
||||
label: 'Bad :(',
|
||||
style: ComponentStyle.Danger,
|
||||
custom_id: '5'
|
||||
}),
|
||||
Message.button({
|
||||
label: 'More Bad :(',
|
||||
style: ComponentStyle.Danger,
|
||||
custom_id: '6'
|
||||
})
|
||||
)
|
||||
.addComponent(
|
||||
Message.selectMenu({
|
||||
custom_id: 'select_1',
|
||||
options: [
|
||||
{
|
||||
label: 'Select me',
|
||||
value: 'select_me',
|
||||
description: 'This is a description',
|
||||
emoji: {
|
||||
name: '👍',
|
||||
id: null,
|
||||
animated: false
|
||||
}
|
||||
}
|
||||
],
|
||||
placeholder: 'Select an option',
|
||||
min_values: 1,
|
||||
max_values: 1
|
||||
})
|
||||
);
|
||||
context.reply(msg);
|
||||
});
|
||||
|
||||
bot.commands.register('test', (_client, context, _args) => {
|
||||
const msg = new Message().setContent('This is a test interaction!').addComponent(
|
||||
'This is an action row',
|
||||
Message.button({
|
||||
label: 'Click me!',
|
||||
style: ComponentStyle.Primary,
|
||||
custom_id: 'click_me',
|
||||
emoji: {
|
||||
name: '🔥',
|
||||
id: null,
|
||||
animated: false
|
||||
}
|
||||
}),
|
||||
Message.button({
|
||||
label: 'Never, me!',
|
||||
style: ComponentStyle.Success,
|
||||
custom_id: 'never_me'
|
||||
}),
|
||||
// Link requires url and can not have custom_id
|
||||
Message.button({
|
||||
label: 'Screw Them.',
|
||||
style: ComponentStyle.Link,
|
||||
url: 'https://google.com'
|
||||
})
|
||||
);
|
||||
context.reply(msg);
|
||||
});
|
||||
|
||||
bot.handlers.register('READY', (client, context, data) => {
|
||||
console.log(`Logged in as ${client.user.tag}`);
|
||||
});
|
||||
|
||||
bot.interactions.register('never_me', async (client, context, interaction) => {
|
||||
const reply = await context.loading();
|
||||
setTimeout(() => {
|
||||
reply.editOriginal(`You clicked me! Thank you ${interaction.member.user.username} <3`);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
bot.interactions.register('click_me', async (client, context, interaction) => {
|
||||
const reply = await context.loading();
|
||||
setTimeout(() => {
|
||||
reply.editOriginal(
|
||||
new Message().addComponent(
|
||||
Message.button({
|
||||
label: 'Click me!',
|
||||
style: ComponentStyle.Primary,
|
||||
custom_id: '1'
|
||||
})
|
||||
)
|
||||
);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
bot.connect();
|
||||
@@ -0,0 +1,243 @@
|
||||
import { ApplicationCommandType, Client, CommandOptionType, ComponentStyle, type Context, createCommandHandler, Expectation, Intent, InteractionCommpoentData, type InteractionData, Message, type MessagePayload } from '../src/index';
|
||||
|
||||
const client = new Client({
|
||||
token: process.env.BOT_TOKEN!,
|
||||
application_id: process.env.APPLICATION_ID!,
|
||||
intents: [
|
||||
Intent.GUILDS,
|
||||
Intent.GUILD_MESSAGES,
|
||||
Intent.GUILD_MESSAGE_REACTIONS,
|
||||
Intent.MESSAGE_CONTENT
|
||||
],
|
||||
prefix: '!!'
|
||||
});
|
||||
|
||||
// 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 chat command
|
||||
client.chatCommands.register('ping', async (context: Context, args: any[]) => {
|
||||
const { message } = await context.hydrate(context, [Expectation.Message]);
|
||||
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`);
|
||||
});
|
||||
|
||||
// Register an application command
|
||||
client.applicationCommands.register('hello', ApplicationCommandType.Chat, createCommandHandler({
|
||||
description: "Introduce yourself",
|
||||
args: [
|
||||
{
|
||||
type: CommandOptionType.String,
|
||||
name: "name",
|
||||
id: "name",
|
||||
description: "Your name",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: CommandOptionType.Integer,
|
||||
name: "age",
|
||||
id: "age",
|
||||
description: "Your age",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: CommandOptionType.String,
|
||||
name: "favorite_animal",
|
||||
id: "favorite_animal",
|
||||
description: "Your favorite animal",
|
||||
required: false,
|
||||
choices: [
|
||||
{ name: "Dog", value: "dog" },
|
||||
{ name: "Cat", value: "cat" },
|
||||
{ name: "Bird", value: "bird" },
|
||||
{ name: "Shark", value: "shark" },
|
||||
{ name: "Elephant", value: "elephant" },
|
||||
{ name: "Tiger", value: "Tiger" },
|
||||
{ name: "Lion", value: "lion" },
|
||||
{ name: "Bear", value: "Bear" }
|
||||
]
|
||||
}
|
||||
],
|
||||
execute: async (context, args) => {
|
||||
context.reply(
|
||||
`\
|
||||
Hello ${args.name}! I see you are ${args.age} years old${!!args.favorite_animal ? ` and your favorite animal is ${args.favorite_animal}.`: '.'}
|
||||
Nice to meet you, I'm ${context.self.username}!`
|
||||
, true);
|
||||
}
|
||||
}));
|
||||
|
||||
// BUG: Repeated hydration does not type narrow correctly. See below.
|
||||
// Repeated hydration
|
||||
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
||||
// Note: Testing repeated hydration
|
||||
const { message } = await context.hydrate(context, [Expectation.Message])
|
||||
const hydrated = await context.hydrate(message, [Expectation.Channel])
|
||||
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
|
||||
rehydrated.guild.name
|
||||
|
||||
const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
|
||||
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 ${hydrated.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);
|
||||
}
|
||||
|
||||
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
|
||||
});
|
||||
|
||||
// Hydrator - hydration function generator
|
||||
client.chatCommands.register('here', async (context: Context, _args: any[]) => {
|
||||
const guild = (await context.hydrator(context, [Expectation.Guild]))(context);
|
||||
|
||||
if (guild) {
|
||||
await context.reply(`You are in ${context.guild.name}`, true);
|
||||
} else {
|
||||
await context.reply('This is not a server!', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Interactions
|
||||
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);
|
||||
});
|
||||
|
||||
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'
|
||||
}),
|
||||
Message.button({
|
||||
label: 'Emoji',
|
||||
style: ComponentStyle.Secondary,
|
||||
custom_id: 'button_4',
|
||||
emoji: {
|
||||
name: '👍',
|
||||
id: null,
|
||||
animated: false
|
||||
}
|
||||
})
|
||||
)
|
||||
.addComponent(
|
||||
Message.button({
|
||||
label: 'Bad :(',
|
||||
style: ComponentStyle.Danger,
|
||||
custom_id: 'button_5'
|
||||
}),
|
||||
Message.button({
|
||||
label: 'More Bad :(',
|
||||
style: ComponentStyle.Danger,
|
||||
custom_id: 'button_6'
|
||||
})
|
||||
)
|
||||
.addComponent(
|
||||
Message.selectMenu({
|
||||
custom_id: 'select_1',
|
||||
options: [
|
||||
{
|
||||
label: 'Select me',
|
||||
value: 'select_me',
|
||||
description: 'This is a description',
|
||||
emoji: {
|
||||
name: '👍',
|
||||
id: null,
|
||||
animated: false
|
||||
}
|
||||
}
|
||||
],
|
||||
placeholder: 'Select an option',
|
||||
min_values: 1,
|
||||
max_values: 1
|
||||
})
|
||||
);
|
||||
|
||||
context.reply(msg);
|
||||
});
|
||||
|
||||
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);
|
||||
client.interactions.register('button_4', buttonInteractionHandler);
|
||||
client.interactions.register('button_5', buttonInteractionHandler);
|
||||
client.interactions.register('button_6', buttonInteractionHandler);
|
||||
|
||||
client.interactions.register('select_1', async (context: Context, data: InteractionCommpoentData) => {
|
||||
if (data?.values !== undefined) {
|
||||
context.reply(`You selected ${data.values[0]}!`);
|
||||
} else {
|
||||
context.reply('You selected nothing!');
|
||||
}
|
||||
});
|
||||
|
||||
client.connect();
|
||||
+30
-9
@@ -1,11 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"declaration": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
"declaration": true,
|
||||
"outDir": "out",
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"emitDeclarationOnly": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@cspotcode/source-map-support@^0.8.0":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/trace-mapping@0.3.9":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
|
||||
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
|
||||
|
||||
"@tsconfig/node12@^1.0.7":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
|
||||
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
|
||||
|
||||
"@tsconfig/node14@^1.0.0":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
|
||||
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
|
||||
|
||||
"@tsconfig/node16@^1.0.2":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
|
||||
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
|
||||
|
||||
"@types/node@*":
|
||||
version "18.7.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154"
|
||||
integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==
|
||||
|
||||
"@types/strip-bom@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
|
||||
integrity sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==
|
||||
|
||||
"@types/strip-json-comments@0.0.30":
|
||||
version "0.0.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1"
|
||||
integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==
|
||||
|
||||
"@types/ws@^8.5.3":
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
|
||||
integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||
|
||||
acorn@^8.4.1:
|
||||
version "8.8.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
||||
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^0.27.2:
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
|
||||
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.9"
|
||||
form-data "^4.0.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
chokidar@^3.5.1:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
dotenv@^16.0.2:
|
||||
version "16.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.2.tgz#0b0f8652c016a3858ef795024508cddc4bffc5bf"
|
||||
integrity sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==
|
||||
|
||||
dynamic-dedupe@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1"
|
||||
integrity sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==
|
||||
dependencies:
|
||||
xtend "^4.0.0"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
follow-redirects@^1.14.9:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.1.1"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
has@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-core-module@^2.9.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
|
||||
integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||
|
||||
is-glob@^4.0.1, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-number@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
minimatch@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
|
||||
|
||||
path-parse@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
resolve@^1.0.0:
|
||||
version "1.22.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
|
||||
dependencies:
|
||||
is-core-module "^2.9.0"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
rimraf@^2.6.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
source-map-support@^0.5.12:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map@^0.6.0:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
strip-bom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||
integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
|
||||
|
||||
strip-json-comments@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
||||
|
||||
supports-preserve-symlinks-flag@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
ts-node-dev@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065"
|
||||
integrity sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==
|
||||
dependencies:
|
||||
chokidar "^3.5.1"
|
||||
dynamic-dedupe "^0.3.0"
|
||||
minimist "^1.2.6"
|
||||
mkdirp "^1.0.4"
|
||||
resolve "^1.0.0"
|
||||
rimraf "^2.6.1"
|
||||
source-map-support "^0.5.12"
|
||||
tree-kill "^1.2.2"
|
||||
ts-node "^10.4.0"
|
||||
tsconfig "^7.0.0"
|
||||
|
||||
ts-node@^10.4.0:
|
||||
version "10.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
|
||||
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "^0.8.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
"@tsconfig/node16" "^1.0.2"
|
||||
acorn "^8.4.1"
|
||||
acorn-walk "^8.1.1"
|
||||
arg "^4.1.0"
|
||||
create-require "^1.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
tsconfig@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7"
|
||||
integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==
|
||||
dependencies:
|
||||
"@types/strip-bom" "^3.0.0"
|
||||
"@types/strip-json-comments" "0.0.30"
|
||||
strip-bom "^3.0.0"
|
||||
strip-json-comments "^2.0.0"
|
||||
|
||||
typescript@^4.8.3:
|
||||
version "4.8.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88"
|
||||
integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^8.8.1:
|
||||
version "8.8.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
|
||||
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
|
||||
|
||||
xtend@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
Reference in New Issue
Block a user