mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-19 05:57:38 +00:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b1eecd823 | |||
| 5beda9328b | |||
| 898ceec96d | |||
| 40d76aae80 | |||
| f0f526e5c5 | |||
| 2dae0b066f | |||
| ebaf7d711c | |||
| d10a22424e | |||
| e711473b7f | |||
| 6c59334c27 | |||
| 444faea651 | |||
| 012a08cb9e | |||
| 215b10e608 | |||
| 3da661743c | |||
| 7b07c9561c | |||
| 5e2ff898c0 | |||
| fb5509cf79 | |||
| ff6ef6930e | |||
| 91ed529f28 | |||
| 3a6c3acaef | |||
| db30e0d0ec | |||
| 25f7baa1d5 | |||
| b33c32c0d8 | |||
| 969c4a916e | |||
| 2e6ab53d34 | |||
| 6eefe763e3 | |||
| c200bff8d0 | |||
| bd8b7c4fd8 | |||
| f58c14d51c | |||
| f6115b0d17 | |||
| 934db0c285 | |||
| ddfcfa798a | |||
| aab2f7a546 | |||
| 26e9a7addf | |||
| 9f1498b3ad | |||
| e080d368cc | |||
| 6338f3ab0d | |||
| 72f05e762a | |||
| d169adbf3c | |||
| 6c7fc895f8 | |||
| ae4bc3e0a9 | |||
| 713761386c | |||
| fda26a892c | |||
| 16c81c7a80 | |||
| 161b78f097 | |||
| 62254a7576 | |||
| b001b1b756 | |||
| fc77394786 | |||
| 1fca25ef31 | |||
| aaa99f9120 | |||
| 1dc1371e34 | |||
| 211c1ca4c6 | |||
| 69f32aa067 | |||
| 9043c637c0 | |||
| 929cd71881 | |||
| c756bbbc30 | |||
| b4a8f412a1 | |||
| 817e37c4e4 | |||
| 1cafdbdcc8 | |||
| 7f745d9a38 | |||
| 962a1e9909 | |||
| a3514801fe | |||
| 1227580083 | |||
| 835271a4c7 | |||
| 6f385a68e7 | |||
| dbad7168e8 | |||
| 43f83a1964 | |||
| 1480905468 | |||
| fb81a55035 | |||
| 3bb42e3751 | |||
| 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
|
on: workflow_dispatch
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -4,14 +4,15 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
# Pull repo
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/checkout@v2
|
||||||
with:
|
# Install bun
|
||||||
node-version: 14
|
- uses: oven-sh/setup-bun@v2
|
||||||
registry-url: https://registry.npmjs.org/
|
# Run tests
|
||||||
- run: yarn
|
# TODO: Add tests
|
||||||
- run: yarn build
|
# Build
|
||||||
# - run: yarn test
|
- run: bun build
|
||||||
- run: yarn publish --access public
|
# Publish
|
||||||
|
- run: bun publish
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
NPM_CONFIG_REGISTRY: https://registry.npmjs.org/_authToken=${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
+175
-2
@@ -1,3 +1,176 @@
|
|||||||
|
# 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
|
.env
|
||||||
/dist
|
.env.development.local
|
||||||
/node_modules
|
.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
|
||||||
|
.aider*
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
src/services/processor.ts
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 120,
|
|
||||||
"semi": true,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,33 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h2>discord.ts</h2>
|
<img src="https://raw.githubusercontent.com/jaronpate/unicord/master/assets/unicord.png" width="150" height="150">
|
||||||
<p>Build good bots fast</p>
|
<h2>unicord</h2>
|
||||||
|
<p><em>Build good bots fast</em></p>
|
||||||
</div>
|
</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
|
## Installation
|
||||||
|
|
||||||
|
Install with your package manager of choice.
|
||||||
|
|
||||||
```
|
```
|
||||||
yarn add @jaronp/discord.ts
|
npm install unicord
|
||||||
|
yarn add unicord
|
||||||
|
bun add unicord
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
A small example implementation.
|
A small example implementation.
|
||||||
```ts
|
```typescript
|
||||||
import { Client } from '@jaronp/discord.ts';
|
import { Client, type Context } from 'unicord';
|
||||||
|
|
||||||
// Initialize and configure
|
// Initialize and configure
|
||||||
const bot = new Client({
|
const client = new Client({
|
||||||
token: '<token>',
|
token: '<token>',
|
||||||
application_id: '<application_id>',
|
application_id: '<application_id>',
|
||||||
intents: [
|
intents: [
|
||||||
@@ -24,52 +35,192 @@ const bot = new Client({
|
|||||||
Intent.GUILD_MESSAGES,
|
Intent.GUILD_MESSAGES,
|
||||||
Intent.GUILD_MESSAGE_REACTIONS,
|
Intent.GUILD_MESSAGE_REACTIONS,
|
||||||
Intent.MESSAGE_CONTENT
|
Intent.MESSAGE_CONTENT
|
||||||
]
|
],
|
||||||
|
prefix: '??'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register a listener
|
// Register arbitrary event handler to the client bus
|
||||||
bot.handlers.register('READY', (client, context, data) => {
|
client.on('READY', async (_, payload) => {
|
||||||
console.log(`Logged in as ${client.user.tag}`);
|
console.log(`Logged in as ${payload.user.username}${payload.user.discriminator?.length > 0 ? `#${payload.user.discriminator}` : ''}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register a command
|
// Register a command
|
||||||
bot.commands.register('ping', (client, context, args) => {
|
client.chatCommands.register('ping', (context: Context, args) => {
|
||||||
context.reply('Pong!', true);
|
context.reply('Pong!', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect
|
// 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
|
## Interactions
|
||||||
|
|
||||||
First create the interactions.
|
First create the interactions.
|
||||||
```ts
|
|
||||||
bot.commands.register('test', (_client, context, _args) => {
|
```typescript
|
||||||
// Add components to use interactions
|
client.chatCommands.register('demo', async (context: Context, _args: any[]) => {
|
||||||
const msg = new Message().setContent('Test interaction!').addComponent(
|
const msg = new Message()
|
||||||
// This generations an action row which can optionaly contain content
|
.setContent('Hello World!')
|
||||||
|
.addComponent(
|
||||||
'This is an action row',
|
'This is an action row',
|
||||||
// Create components with helper functions
|
// 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({
|
Message.button({
|
||||||
label: 'Click me!',
|
label: 'Click me!',
|
||||||
style: ComponentStyle.Primary,
|
style: ComponentStyle.Primary,
|
||||||
// Assign id's to reference them later
|
custom_id: 'button_1'
|
||||||
custom_id: 'click_me',
|
}),
|
||||||
emoji: {
|
Message.button({
|
||||||
name: '🔥',
|
label: 'No me!',
|
||||||
id: null,
|
style: ComponentStyle.Secondary,
|
||||||
animated: false
|
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);
|
context.reply(msg);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Then provide a handler for them.
|
Then provide a handler for them.
|
||||||
```ts
|
|
||||||
bot.interactions.register('click_me', async (client, context, interaction) => {
|
```typescript
|
||||||
const response = context.reply(
|
const buttonInteractionHandler = (context: Context, data: InteractionCommpoentData) => {
|
||||||
`You clicked me! Thank you ${interaction.member.user.username} <3`
|
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);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Context Support
|
||||||
|
|
||||||
|
This is an example of how a single function can be used for both chat and application commands.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const quotesCommandHandler = createCommandHandler({
|
||||||
|
description: 'Get a list of a users quotes',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
id: 'user',
|
||||||
|
name: 'user',
|
||||||
|
type: CommandOptionType.User,
|
||||||
|
description: 'The user to get quotes for',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
] as const,
|
||||||
|
execute: async (context: Context, args) => {
|
||||||
|
// Lookup user
|
||||||
|
const user = await findUserByDiscordId(args.user.id);
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
return context.reply('User not found', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotes = await getQuotesByUser(user.id);
|
||||||
|
|
||||||
|
if (quotes.length === 0) {
|
||||||
|
return context.reply('No quotes found', true);
|
||||||
|
} else {
|
||||||
|
const embed = new Embed()
|
||||||
|
.setAuthor({ icon_url: args.user.avatarURL, name: args.user.display_name })
|
||||||
|
.setDescription(quotes.map(q => `"${q.quote_content}"`).join('\n\n'))
|
||||||
|
.setColor(0xFFFFFF);
|
||||||
|
|
||||||
|
return context.reply(embed.toMessage(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.chatCommands.register('quotes', quotesCommandHandler);
|
||||||
|
client.applicationCommands.register('quotes', ApplicationCommandType.Chat, quotesCommandHandler);
|
||||||
```
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
+35
-17
@@ -1,27 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "@jaronp/discord.ts",
|
"name": "unicord",
|
||||||
"version": "0.1.1",
|
"description": "A simple discord library primarily for TypeScript. Web server like and easy to use.",
|
||||||
"main": "dist/index.js",
|
"version": "0.0.4",
|
||||||
"types": "dist/index.d.ts",
|
"module": "src/index.ts",
|
||||||
"author": "Jaron Pate",
|
"type": "module",
|
||||||
"license": "Unlicense",
|
"main": "out/index.js",
|
||||||
"repository": "https://github.com/jaronpate/discord.ts",
|
"types": "out/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"/dist"
|
"out"
|
||||||
],
|
],
|
||||||
|
"repository": "github:jaronpate/unicord",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx ts-node-dev src/index.ts",
|
"test": "bun run testing/demo.test.ts",
|
||||||
"start:watch": "npx ts-node-dev --respawn src/index.ts",
|
"test:inspect": "bun --inspect testing/demo.test.ts",
|
||||||
"build": "npx tsc"
|
|
||||||
|
"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": {
|
"devDependencies": {
|
||||||
"@types/ws": "^8.5.3",
|
"@types/bun": "latest",
|
||||||
"dotenv": "^16.0.2",
|
"commander": "^13.0.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"concurrently": "^9.1.0"
|
||||||
"typescript": "^4.8.3"
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"ws": "^8.18.0"
|
||||||
"ws": "^8.8.1"
|
},
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import type { Unicord } from '..';
|
||||||
|
import { Trait } from '../types/common';
|
||||||
|
|
||||||
|
export class UnicordObjectCache<Entity> {
|
||||||
|
cache: LRUCache<string, Entity>;
|
||||||
|
requests = new Map<string, Promise<Entity>>();
|
||||||
|
readonly type: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly self: Unicord,
|
||||||
|
type: string,
|
||||||
|
private readonly factory: { [Trait.fromDiscord]: (data: any) => Entity } & (new (data: any) => Entity),
|
||||||
|
events: string[],
|
||||||
|
maxCapacity: number = 10_000,
|
||||||
|
) {
|
||||||
|
this.cache = new LRUCache<string, Entity>(maxCapacity);
|
||||||
|
this.type = type;
|
||||||
|
for (const event of events) {
|
||||||
|
this.self.onEvent(event, async (_context, e) => {
|
||||||
|
this.cache.set(e.id, factory[Trait.fromDiscord](e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(object_id: string, object: Entity): 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<Entity> {
|
||||||
|
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.self.discordAPI.get(`/${this.type}/${object_id}`).then((data) => {
|
||||||
|
// Create the object
|
||||||
|
const object = this.factory[Trait.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 when it finishes
|
||||||
|
req.finally(() => this.requests.delete(object_id));
|
||||||
|
// Return the request
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(object_id: string, ..._args: any[]): Promise<Entity> {
|
||||||
|
if (this.cache.has(object_id)) {
|
||||||
|
return await Promise.resolve(this.cache.get(object_id)!);
|
||||||
|
} else {
|
||||||
|
return this.fetch(object_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListNode<K, V> {
|
||||||
|
key: K;
|
||||||
|
value: V;
|
||||||
|
prev: ListNode<K, V> | null = null;
|
||||||
|
next: ListNode<K, V> | null = null;
|
||||||
|
|
||||||
|
constructor(key: K, value: V) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LRUCache<K, V> {
|
||||||
|
private capacity: number;
|
||||||
|
private map: Map<K, ListNode<K, V>>;
|
||||||
|
private head: ListNode<K, V>;
|
||||||
|
private tail: ListNode<K, V>;
|
||||||
|
|
||||||
|
constructor(capacity: number) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.map = new Map();
|
||||||
|
|
||||||
|
// Dummy head and tail to simplify boundary conditions
|
||||||
|
this.head = new ListNode<K, V>(null as any, null as any);
|
||||||
|
this.tail = new ListNode<K, V>(null as any, null as any);
|
||||||
|
this.head.next = this.tail;
|
||||||
|
this.tail.prev = this.head;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
if (!this.map.has(key)) return undefined;
|
||||||
|
|
||||||
|
const node = this.map.get(key)!;
|
||||||
|
this.moveToFront(node);
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.map.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): void {
|
||||||
|
if (this.map.has(key)) {
|
||||||
|
// Update existing node value and move it to front
|
||||||
|
const node = this.map.get(key)!;
|
||||||
|
node.value = value;
|
||||||
|
this.moveToFront(node);
|
||||||
|
} else {
|
||||||
|
// Create a new node
|
||||||
|
const newNode = new ListNode(key, value);
|
||||||
|
this.map.set(key, newNode);
|
||||||
|
this.addToFront(newNode);
|
||||||
|
|
||||||
|
// If over capacity, remove the least recently used (LRU) node
|
||||||
|
if (this.map.size > this.capacity) {
|
||||||
|
this.removeLRU();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveToFront(node: ListNode<K, V>): void {
|
||||||
|
this.removeNode(node);
|
||||||
|
this.addToFront(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToFront(node: ListNode<K, V>): void {
|
||||||
|
node.next = this.head.next;
|
||||||
|
node.prev = this.head;
|
||||||
|
this.head.next!.prev = node;
|
||||||
|
this.head.next = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeNode(node: ListNode<K, V>): void {
|
||||||
|
node.prev!.next = node.next;
|
||||||
|
node.next!.prev = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeLRU(): void {
|
||||||
|
const lruNode = this.tail.prev!;
|
||||||
|
this.removeNode(lruNode);
|
||||||
|
this.map.delete(lruNode.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||||
|
let current = this.head.next;
|
||||||
|
while (current && current !== this.tail) {
|
||||||
|
yield [current.key, current.value];
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
import type { Unicord } from "..";
|
||||||
|
import { Channel } from "../types/channel";
|
||||||
|
import { UnicordObjectCache } from "./cache";
|
||||||
|
|
||||||
|
export class UnicordChannelsCache extends UnicordObjectCache<Channel> {
|
||||||
|
constructor(self: Unicord) {
|
||||||
|
super(self, 'channels', Channel, ['CHANNEL_CREATE', 'CHANNEL_UPDATE']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Unicord } from "..";
|
||||||
|
import { Guild } from "../types/guild";
|
||||||
|
import { UnicordObjectCache } from "./cache";
|
||||||
|
|
||||||
|
export class UnicordGuildsCache extends UnicordObjectCache<Guild> {
|
||||||
|
constructor(self: Unicord) {
|
||||||
|
super(self, 'guilds', Guild, ['GUILD_CREATE', 'GUILD_UPDATE']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './users';
|
||||||
|
export * from './channels';
|
||||||
|
export * from './guilds';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Unicord } from '..';
|
||||||
|
import { User } from '../types/user';
|
||||||
|
import { UnicordObjectCache } from './cache';
|
||||||
|
|
||||||
|
export class UnicordUsersCache extends UnicordObjectCache<User> {
|
||||||
|
constructor(self: Unicord) {
|
||||||
|
super(self, 'users', User, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type {
|
||||||
|
ArgumentTypeFromOptions,
|
||||||
|
Unicord,
|
||||||
|
UnicordArgumentDefinition,
|
||||||
|
UnicordCommandHandler,
|
||||||
|
UnicordCommandOptions,
|
||||||
|
UnicordHandler,
|
||||||
|
} from '.';
|
||||||
|
import { UnicordCommandContext } from './context';
|
||||||
|
import type { UnicordEventProcessor } from './eventProcessor';
|
||||||
|
import { Embed } from './types';
|
||||||
|
import { UnicordArgumentType, UnicordEventType } from './types/common';
|
||||||
|
import { log } from './utils';
|
||||||
|
|
||||||
|
type UnicordCommandDefinition<Options extends UnicordCommandOptions> = {
|
||||||
|
options: Options;
|
||||||
|
handler: UnicordCommandHandler<Options>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArgumentConstructorMap = {
|
||||||
|
[UnicordArgumentType.String]: String,
|
||||||
|
[UnicordArgumentType.Integer]: Number,
|
||||||
|
[UnicordArgumentType.Boolean]: Boolean,
|
||||||
|
[UnicordArgumentType.Number]: Number,
|
||||||
|
[UnicordArgumentType.User]: String, // Could be a user ID or object depending on your use case
|
||||||
|
[UnicordArgumentType.Channel]: String, // Could be a channel ID or object
|
||||||
|
[UnicordArgumentType.Role]: String, // Could be a role ID or object
|
||||||
|
[UnicordArgumentType.Mentionable]: String, // Could be a mentionable ID or object
|
||||||
|
[UnicordArgumentType.Attachment]: String, // Could represent attachment IDs
|
||||||
|
[UnicordArgumentType.SubCommand]: String, // Handled by `options`
|
||||||
|
[UnicordArgumentType.SubCommandGroup]: String, // Handled by `options`
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UnicordCommandManager {
|
||||||
|
private handlers = {
|
||||||
|
[UnicordEventType.ChatCommands]: new Map<
|
||||||
|
string,
|
||||||
|
UnicordCommandDefinition<any> & { wrappedHandler: UnicordCommandHandler<any> }
|
||||||
|
>(),
|
||||||
|
[UnicordEventType.ApplicationCommands]: new Map<
|
||||||
|
string,
|
||||||
|
UnicordCommandDefinition<any> & { wrappedHandler: UnicordCommandHandler<any> }
|
||||||
|
>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private readonly self: Unicord, private readonly eventProcessor: UnicordEventProcessor) {
|
||||||
|
this.handlers = {
|
||||||
|
[UnicordEventType.ChatCommands]: new Map(),
|
||||||
|
[UnicordEventType.ApplicationCommands]: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.registerDefaultHelpCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validateAndResolveArgs<const Options extends UnicordCommandOptions>(
|
||||||
|
args: any[],
|
||||||
|
definitions: Options['args'],
|
||||||
|
): Promise<ArgumentTypeFromOptions<Options['args']>> {
|
||||||
|
// Zip the args and definition together
|
||||||
|
const zipped: [UnicordArgumentDefinition, any][] = args.map((arg, i) => [definitions[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 (false) {
|
||||||
|
} else {
|
||||||
|
validated[def.name] = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Fix types so we don't need to cast here
|
||||||
|
return Promise.resolve(validated as ArgumentTypeFromOptions<Options['args']>);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerDefaultHelpCommand() {
|
||||||
|
this.registerChatCommand(
|
||||||
|
'help',
|
||||||
|
async (context) => {
|
||||||
|
// Just list chat commands for now
|
||||||
|
const reply = new Embed().setTitle('Available Commands');
|
||||||
|
|
||||||
|
for (const [command, def] of this.handlers[UnicordEventType.ChatCommands]) {
|
||||||
|
let fieldValue = def.options.description ?? 'No description';
|
||||||
|
fieldValue += '\n';
|
||||||
|
|
||||||
|
if (def.options.args.length > 0) {
|
||||||
|
fieldValue += `\n\`${command}`;
|
||||||
|
for (const arg of def.options.args) {
|
||||||
|
fieldValue += arg.required ? ` <${arg.name}>` : ` <?${arg.name}>`;
|
||||||
|
}
|
||||||
|
fieldValue += '`\n';
|
||||||
|
for (const arg of def.options.args) {
|
||||||
|
fieldValue += `- \`${arg.name}\` (${UnicordArgumentType[arg.type]}${
|
||||||
|
arg.required ? ', required' : ''
|
||||||
|
}): ${arg.description ?? 'No description'}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldValue += '\n';
|
||||||
|
|
||||||
|
reply.fields.push({
|
||||||
|
name: command,
|
||||||
|
value: fieldValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.reply(reply.toMessage());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Lists all available commands',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
all() {
|
||||||
|
return this.handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands, event: string) {
|
||||||
|
return this.handlers[type].has(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
merge(other: UnicordCommandManager) {
|
||||||
|
const handlers = other.all();
|
||||||
|
|
||||||
|
for (const type in handlers) {
|
||||||
|
const typeKey = type as UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands;
|
||||||
|
for (const [event, definition] of handlers[typeKey]) {
|
||||||
|
this.register(typeKey, event, definition.handler, definition.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerDefaultHelpCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands, event: string) {
|
||||||
|
const handler = this.handlers[type].get(event);
|
||||||
|
if (handler) {
|
||||||
|
this.eventProcessor.unregister(`${type}:${event}`, handler.wrappedHandler);
|
||||||
|
this.handlers[type].delete(event);
|
||||||
|
}
|
||||||
|
return this.self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Register application commands with the DiscordAPI
|
||||||
|
register<const Options extends UnicordCommandOptions>(
|
||||||
|
type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands,
|
||||||
|
event: string,
|
||||||
|
handler: UnicordCommandHandler<Options>,
|
||||||
|
options: Options = { args: [] } as unknown as Options,
|
||||||
|
) {
|
||||||
|
if (this.has(type, event)) {
|
||||||
|
this.unregister(type, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrappedHandler = (async (context: UnicordCommandContext, args: any[]) => {
|
||||||
|
const resolvedArgs = await this.validateAndResolveArgs<Options>(args, options.args);
|
||||||
|
return handler(context, resolvedArgs);
|
||||||
|
}) as UnicordCommandHandler<Options>;
|
||||||
|
|
||||||
|
this.handlers[type].set(event, { options, handler, wrappedHandler });
|
||||||
|
|
||||||
|
this.eventProcessor.register(`${type}:${event}`, wrappedHandler);
|
||||||
|
|
||||||
|
return this.self;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerChatCommand = this.register.bind(this, UnicordEventType.ChatCommands);
|
||||||
|
registerApplicationCommand = this.register.bind(this, UnicordEventType.ApplicationCommands);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import type { Unicord } from '.';
|
||||||
|
import { hydrate } from './hydrator';
|
||||||
|
import type { MessagePayload } from './types';
|
||||||
|
import { Message } from './types';
|
||||||
|
import { exists, isNil } from './utils';
|
||||||
|
|
||||||
|
export type UnicordCommandContextInput =
|
||||||
|
| {
|
||||||
|
message: MessagePayload;
|
||||||
|
channel_id?: string;
|
||||||
|
message_id?: string;
|
||||||
|
guild_id?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
channel_id: string;
|
||||||
|
message_id: string;
|
||||||
|
guild_id?: string;
|
||||||
|
message?: MessagePayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Unicord} self - The client instance.
|
||||||
|
* @param {UnicordCommandContextInput} data - The payload data for the context.
|
||||||
|
*
|
||||||
|
* @throws {Error} If the message ID is not provided.
|
||||||
|
* @throws {Error} If the channel ID is not provided.
|
||||||
|
*/
|
||||||
|
export class UnicordCommandContext {
|
||||||
|
// Public properties
|
||||||
|
message_id: string;
|
||||||
|
channel_id: string;
|
||||||
|
guild_id?: string;
|
||||||
|
|
||||||
|
message?: Message;
|
||||||
|
|
||||||
|
constructor(private self: Unicord, data: UnicordCommandContextInput) {
|
||||||
|
if (isNil(data.message)) {
|
||||||
|
if (isNil(data.message_id)) {
|
||||||
|
throw new Error('Message ID is required to create a context');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNil(data.channel_id)) {
|
||||||
|
throw new Error('Channel ID is required to create a context');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.message_id = data.message_id;
|
||||||
|
this.channel_id = data.channel_id;
|
||||||
|
this.guild_id = data.guild_id;
|
||||||
|
} else {
|
||||||
|
this.message = data.message;
|
||||||
|
this.message_id = data.message.id;
|
||||||
|
this.channel_id = data.message.channel_id;
|
||||||
|
this.guild_id = data.message.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 = async (message: Message | string, reference: boolean = true): Promise<Message> => {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
message = new Message().setContent(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exists(this.message)) {
|
||||||
|
if (reference) {
|
||||||
|
message.setReference(this.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.self.sendMessage(this.channel_id, message);
|
||||||
|
} else {
|
||||||
|
throw new Error('Cannot reply without a message payload');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrates a given data object based on the provided expectations.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
get hydrate() {
|
||||||
|
return hydrate.bind(null, this.self);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import type { Unicord } from '.';
|
||||||
|
import { exists, log } from './utils';
|
||||||
|
|
||||||
|
export class UnicordDiscordAPI {
|
||||||
|
private readonly base_url: string = 'https://discord.com/api';
|
||||||
|
private readonly version: number = 10;
|
||||||
|
|
||||||
|
constructor(private self: Unicord) {}
|
||||||
|
|
||||||
|
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
|
||||||
|
const url = `${this.base_url}/v${this.version.toString()}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bot ${this.self.config.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: ${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,22 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import type { Unicord } from '.';
|
||||||
|
import type { UnicordCommandContext } from './context';
|
||||||
|
import type { UnicordCommandOptions, UnicordEventContext, UnicordHandler } from './types/common';
|
||||||
|
|
||||||
|
export class UnicordEventProcessor extends EventEmitter {
|
||||||
|
constructor(private readonly self: Unicord) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
register<const Options extends UnicordCommandOptions>(event: string, handler: UnicordHandler<Options>) {
|
||||||
|
return super.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister<const Options extends UnicordCommandOptions>(event: string, handler: UnicordHandler<Options>) {
|
||||||
|
return super.off(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]): boolean {
|
||||||
|
return super.emit(event, context, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { WebSocket, type Data } from 'ws';
|
||||||
|
import { Unicord } from '.';
|
||||||
|
import { UnicordCommandContext } from './context';
|
||||||
|
import type { DiscordMessage, Payload } from './types';
|
||||||
|
import { fromDiscord, Message, UnicordEventContext, UnicordEventType, User } from './types';
|
||||||
|
import { exists, isNil, isObject, log } from './utils';
|
||||||
|
|
||||||
|
export class UnicordGatewayManager {
|
||||||
|
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 readonly self: Unicord) {}
|
||||||
|
|
||||||
|
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
|
||||||
|
this.indentify();
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
// Send the heartbeat payload
|
||||||
|
this.send({ op: 1, d: this.sequence });
|
||||||
|
// Set a timeout to ensure the heartbeat is sent within the interval
|
||||||
|
this.heartbeat_timeout = setTimeout(() => {
|
||||||
|
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.self.config.token,
|
||||||
|
intents: this.self.config.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');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event_name === 'READY') {
|
||||||
|
this.user = User[fromDiscord](event_data.user);
|
||||||
|
} else if (event_name === 'INTERACTION_CREATE') {
|
||||||
|
// await this.handleInteractionCreate(payload);
|
||||||
|
} else if (event_name === 'MESSAGE_CREATE') {
|
||||||
|
await this.handleMessageCreate(payload);
|
||||||
|
} else if (event_name === 'MESSAGE_UPDATE') {
|
||||||
|
// TODO: setting to call commands again here on message edit
|
||||||
|
// await this.handleMessageUpdate(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make a real UnicordEventContext class
|
||||||
|
this.self.emit(UnicordEventType.SystemEvent, event_name, event_data as UnicordEventContext, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 content
|
||||||
|
const content = payload.d.content.trim();
|
||||||
|
const hasPrefixInConfig = exists(this.self.config.prefix) && this.self.config.prefix.length > 0;
|
||||||
|
const userMentionPrefix = `<@${this.user.id}>`;
|
||||||
|
// Is this a command?
|
||||||
|
if (
|
||||||
|
(hasPrefixInConfig && content.startsWith(this.self.config.prefix)) ||
|
||||||
|
content.startsWith(userMentionPrefix)
|
||||||
|
) {
|
||||||
|
// Determine the prefix
|
||||||
|
const prefix =
|
||||||
|
hasPrefixInConfig && content.startsWith(this.self.config.prefix)
|
||||||
|
? this.self.config.prefix!
|
||||||
|
: userMentionPrefix;
|
||||||
|
// 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()!;
|
||||||
|
// Generate context
|
||||||
|
const message = Message.fromDiscord(payload.d as DiscordMessage);
|
||||||
|
const context: UnicordCommandContext = new UnicordCommandContext(this.self, { message });
|
||||||
|
// Dispatch event for the command
|
||||||
|
this.self.emit(UnicordEventType.ChatCommands, command, context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
// Fetch the Discord gateway URL
|
||||||
|
const response = await this.self.discordAPI.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,70 @@
|
|||||||
|
import type { Expects, Unicord } from '.';
|
||||||
|
import { Expectation } from '.';
|
||||||
|
import { UnicordCommandContext } from './context';
|
||||||
|
import type { Channel, Guild, Message } from './types';
|
||||||
|
import { exists } from './utils';
|
||||||
|
|
||||||
|
export type Hydrateable = UnicordCommandContext;
|
||||||
|
|
||||||
|
export type UnicordHydratedObject<Object, Expectations extends Array<Expectation>> = Object & {
|
||||||
|
channel: Expects<Expectations, Expectation.Channel, Channel>;
|
||||||
|
guild: Expects<Expectations, Expectation.Guild, Guild>;
|
||||||
|
message: Expects<Expectations, Expectation.Message, Message>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Hydrated<Object extends Hydrateable, Expectations extends Array<Expectation>> = UnicordHydratedObject<
|
||||||
|
Object,
|
||||||
|
Expectations
|
||||||
|
>;
|
||||||
|
|
||||||
|
// export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> = T extends UnicordCommandContext
|
||||||
|
// ? HydratedCommandContext<K>
|
||||||
|
// : never;
|
||||||
|
|
||||||
|
// NOTE: Dont forget to update the typedoc comment in src/index.ts and src/context.ts when making changes here
|
||||||
|
/**
|
||||||
|
* Hydrates a given data object based on the provided expectations.
|
||||||
|
*
|
||||||
|
* @param self - The client instance used to fetch additional data required for hydration.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
export async function hydrate<T extends Hydrateable, K extends Array<Expectation>>(
|
||||||
|
self: Unicord,
|
||||||
|
data: T,
|
||||||
|
expectations: K,
|
||||||
|
): Promise<UnicordHydratedObject<T, K>> {
|
||||||
|
// TODO: add clone trait. Make it a requirement for hydrateable objects.
|
||||||
|
// Then clone the object and return the hydrated object.
|
||||||
|
let hydrated: UnicordHydratedObject<T, K>;
|
||||||
|
|
||||||
|
if (data instanceof UnicordCommandContext) {
|
||||||
|
for (const expectation of expectations) {
|
||||||
|
// if (expectation === Expectation.Message && exists(data.message_id)) {
|
||||||
|
// let context = data as unknown as UnicordHydratedObject<[Expectation.Message, ...Expectation[]]>;
|
||||||
|
// context.message = await self.caches.messages.get(data.channel_id, data.message_id);
|
||||||
|
// } else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||||
|
if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||||
|
let context = data as unknown as UnicordHydratedObject<UnicordCommandContext, [Expectation.Guild]>;
|
||||||
|
context.guild = await self.caches.guilds.get(data.guild_id);
|
||||||
|
} else if (expectation === Expectation.Channel && exists(data.channel_id)) {
|
||||||
|
let context = data as unknown as UnicordHydratedObject<UnicordCommandContext, [Expectation.Channel]>;
|
||||||
|
context.channel = await self.caches.channels.get(data.channel_id);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid expectation requested for Context: ${expectation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Hydration for the provided type is not implemented.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new object that preserves all existing properties
|
||||||
|
const result = { ...data } as any;
|
||||||
|
|
||||||
|
// Ensure the result is properly typed with both old and new hydrated properties
|
||||||
|
return result as T & Hydrated<T, K>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { UnicordChannelsCache, UnicordGuildsCache, UnicordUsersCache } from './caches';
|
||||||
|
import { UnicordCommandManager } from './commandManager';
|
||||||
|
import type { UnicordCommandContext } from './context';
|
||||||
|
import { UnicordDiscordAPI } from './discordAPI';
|
||||||
|
import { UnicordEventProcessor } from './eventProcessor';
|
||||||
|
import { UnicordGatewayManager } from './gatewayManager';
|
||||||
|
import { hydrate } from './hydrator';
|
||||||
|
import type {
|
||||||
|
UnicordCommandHandler,
|
||||||
|
UnicordCommandOptions,
|
||||||
|
UnicordConfig,
|
||||||
|
UnicordConfigWithDefaults,
|
||||||
|
UnicordEventContext,
|
||||||
|
UnicordEventHandler,
|
||||||
|
UnicordHandler,
|
||||||
|
} from './types/common';
|
||||||
|
import { Intent, UnicordEventType } from './types/common';
|
||||||
|
import type { DiscordMessage } from './types/message';
|
||||||
|
import { Message } from './types/message';
|
||||||
|
import { setDefaults } from './utils';
|
||||||
|
|
||||||
|
const UNICORD_CONFIG_DEFAULTS = {
|
||||||
|
intents: [Intent.DEFAULT],
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Unicord {
|
||||||
|
readonly config: UnicordConfigWithDefaults;
|
||||||
|
readonly discordAPI: UnicordDiscordAPI;
|
||||||
|
private readonly eventProcessor: UnicordEventProcessor;
|
||||||
|
private readonly gatewayManager: UnicordGatewayManager;
|
||||||
|
private readonly commandManager: UnicordCommandManager;
|
||||||
|
readonly caches: {
|
||||||
|
readonly users: UnicordUsersCache;
|
||||||
|
readonly channels: UnicordChannelsCache;
|
||||||
|
readonly guilds: UnicordGuildsCache;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(userConfig: UnicordConfig = {}) {
|
||||||
|
this.config = setDefaults<UnicordConfigWithDefaults>(userConfig, UNICORD_CONFIG_DEFAULTS);
|
||||||
|
this.discordAPI = new UnicordDiscordAPI(this);
|
||||||
|
this.eventProcessor = new UnicordEventProcessor(this);
|
||||||
|
this.gatewayManager = new UnicordGatewayManager(this);
|
||||||
|
this.commandManager = new UnicordCommandManager(this, this.eventProcessor);
|
||||||
|
this.caches = {
|
||||||
|
users: new UnicordUsersCache(this),
|
||||||
|
channels: new UnicordChannelsCache(this),
|
||||||
|
guilds: new UnicordGuildsCache(this),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrates a given data object based on the provided expectations.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
get hydrate() {
|
||||||
|
return hydrate.bind(null, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private __onEvent<Options extends UnicordCommandOptions>(
|
||||||
|
type: UnicordEventType,
|
||||||
|
event: string,
|
||||||
|
handler: UnicordHandler<Options>,
|
||||||
|
) {
|
||||||
|
return this.eventProcessor.register(`${type}:${event}`, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
use(client: Unicord): this {
|
||||||
|
this.commandManager.merge(client.commandManager);
|
||||||
|
// TODO: inherit event listeners too
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(type: UnicordEventType, event: string, context: UnicordCommandContext | UnicordEventContext, args: any[]) {
|
||||||
|
this.eventProcessor.emit(`${type}:${event}`, context, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: can we automate defining these methods with some TS magic?
|
||||||
|
registerChatCommand<const Options extends UnicordCommandOptions>(
|
||||||
|
event: string,
|
||||||
|
handler: UnicordCommandHandler<Options>,
|
||||||
|
options: Options = { args: [] } as unknown as Options,
|
||||||
|
): this {
|
||||||
|
this.commandManager.registerChatCommand(event, handler, options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerApplicationCommand<const Options extends UnicordCommandOptions>(
|
||||||
|
event: string,
|
||||||
|
handler: UnicordCommandHandler<Options>,
|
||||||
|
options: Options = { args: [] } as unknown as Options,
|
||||||
|
): this {
|
||||||
|
this.commandManager.registerApplicationCommand(event, handler, options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(event: string, handler: UnicordEventHandler): this {
|
||||||
|
this.__onEvent(UnicordEventType.SystemEvent, event, handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage = async (channel_id: string, message: Message | string) => {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
message = new Message().setContent(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Message.fromDiscord(
|
||||||
|
await this.discordAPI.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
login(token: string | undefined = this.config.token): this {
|
||||||
|
if (token === null || token === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
'You must provide a token to login. Either in the Unicord config or when calling `login()`',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config.token = token;
|
||||||
|
|
||||||
|
this.gatewayManager.connect();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './types/common';
|
||||||
|
// export * from './types/message';
|
||||||
|
// export * from './types/channel';
|
||||||
|
// export * from './types/guild';
|
||||||
|
// export * from './types/embed';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Trait } from './common';
|
||||||
|
|
||||||
|
export abstract class UnicordGatewayObject<UnicordClass, DiscordObject> {
|
||||||
|
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 UnicordGatewayObject<any, any>>(
|
||||||
|
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,272 @@
|
|||||||
|
import { UnicordGatewayObject } from './base';
|
||||||
|
import { Trait } from './common';
|
||||||
|
import { User, type DiscordUser } from './user';
|
||||||
|
|
||||||
|
export enum ChannelType {
|
||||||
|
GUILD_TEXT = 0,
|
||||||
|
DM = 1,
|
||||||
|
GUILD_VOICE = 2,
|
||||||
|
GROUP_DM = 3,
|
||||||
|
GUILD_CATEGORY = 4,
|
||||||
|
GUILD_ANNOUNCEMENT = 5,
|
||||||
|
ANNOUNCEMENT_THREAD = 10,
|
||||||
|
PUBLIC_THREAD = 11,
|
||||||
|
PRIVATE_THREAD = 12,
|
||||||
|
GUILD_STAGE_VOICE = 13,
|
||||||
|
GUILD_DIRECTORY = 14,
|
||||||
|
GUILD_FORUM = 15,
|
||||||
|
GUILD_MEDIA = 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChannelFlags {
|
||||||
|
PINNED = 1 << 1,
|
||||||
|
REQUIRE_TAG = 1 << 4,
|
||||||
|
HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoQualityMode {
|
||||||
|
AUTO = 1,
|
||||||
|
FULL = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortOrderType {
|
||||||
|
LATEST_ACTIVITY = 0,
|
||||||
|
CREATION_DATE = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ForumLayoutType {
|
||||||
|
NOT_SET = 0,
|
||||||
|
LIST_VIEW = 1,
|
||||||
|
GALLERY_VIEW = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermissionOverwrite = {
|
||||||
|
id: string;
|
||||||
|
type: 0 | 1; // 0 for role, 1 for member
|
||||||
|
allow: string;
|
||||||
|
deny: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThreadMetadata = {
|
||||||
|
archived: boolean;
|
||||||
|
auto_archive_duration: number;
|
||||||
|
archive_timestamp: string;
|
||||||
|
locked: boolean;
|
||||||
|
invitable?: boolean;
|
||||||
|
create_timestamp?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThreadMember = {
|
||||||
|
id?: string;
|
||||||
|
user_id?: string;
|
||||||
|
join_timestamp: string;
|
||||||
|
flags: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ForumTag = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
moderated: boolean;
|
||||||
|
emoji_id?: string | null;
|
||||||
|
emoji_name?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DefaultReaction = {
|
||||||
|
emoji_id?: string | null; // the id of a guild's custom emoji
|
||||||
|
emoji_name?: string | null; // the unicode character of the emoji
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Channel extends UnicordGatewayObject<Channel, DiscordChannel> {
|
||||||
|
id: string;
|
||||||
|
type: ChannelType;
|
||||||
|
guild_id?: string;
|
||||||
|
position?: number;
|
||||||
|
permission_overwrites?: PermissionOverwrite[];
|
||||||
|
name?: string;
|
||||||
|
topic?: string | null;
|
||||||
|
nsfw?: boolean;
|
||||||
|
last_message_id?: string | null;
|
||||||
|
bitrate?: number;
|
||||||
|
user_limit?: number;
|
||||||
|
rate_limit_per_user?: number;
|
||||||
|
recipients?: User[];
|
||||||
|
icon?: string | null;
|
||||||
|
owner_id?: string;
|
||||||
|
application_id?: string;
|
||||||
|
managed?: boolean;
|
||||||
|
parent_id?: string | null;
|
||||||
|
last_pin_timestamp?: Date | null;
|
||||||
|
rtc_region?: string | null;
|
||||||
|
video_quality_mode?: VideoQualityMode;
|
||||||
|
message_count?: number;
|
||||||
|
member_count?: number;
|
||||||
|
thread_metadata?: ThreadMetadata;
|
||||||
|
member?: ThreadMember;
|
||||||
|
default_auto_archive_duration?: number;
|
||||||
|
permissions?: string;
|
||||||
|
flags?: number;
|
||||||
|
total_message_sent?: number;
|
||||||
|
available_tags?: ForumTag[];
|
||||||
|
applied_tags?: string[];
|
||||||
|
default_reaction_emoji?: DefaultReaction;
|
||||||
|
default_thread_rate_limit_per_user?: number;
|
||||||
|
default_sort_order?: SortOrderType | null;
|
||||||
|
default_forum_layout?: ForumLayoutType;
|
||||||
|
|
||||||
|
constructor(data: DiscordChannel) {
|
||||||
|
super();
|
||||||
|
this.id = data.id!;
|
||||||
|
this.type = data.type!;
|
||||||
|
this.guild_id = data.guild_id;
|
||||||
|
this.position = data.position;
|
||||||
|
this.permission_overwrites = data.permission_overwrites;
|
||||||
|
this.name = data.name;
|
||||||
|
this.topic = data.topic;
|
||||||
|
this.nsfw = data.nsfw;
|
||||||
|
this.last_message_id = data.last_message_id;
|
||||||
|
this.bitrate = data.bitrate;
|
||||||
|
this.user_limit = data.user_limit;
|
||||||
|
this.rate_limit_per_user = data.rate_limit_per_user;
|
||||||
|
this.recipients = data.recipients?.map((u) => User.fromDiscord(u));
|
||||||
|
this.icon = data.icon;
|
||||||
|
this.owner_id = data.owner_id;
|
||||||
|
this.application_id = data.application_id;
|
||||||
|
this.managed = data.managed;
|
||||||
|
this.parent_id = data.parent_id;
|
||||||
|
this.last_pin_timestamp = data.last_pin_timestamp ? new Date(data.last_pin_timestamp) : null;
|
||||||
|
this.rtc_region = data.rtc_region;
|
||||||
|
this.video_quality_mode = data.video_quality_mode;
|
||||||
|
this.message_count = data.message_count;
|
||||||
|
this.member_count = data.member_count;
|
||||||
|
this.thread_metadata = data.thread_metadata;
|
||||||
|
this.member = data.member;
|
||||||
|
this.default_auto_archive_duration = data.default_auto_archive_duration;
|
||||||
|
this.permissions = data.permissions;
|
||||||
|
this.flags = data.flags;
|
||||||
|
this.total_message_sent = data.total_message_sent;
|
||||||
|
this.available_tags = data.available_tags;
|
||||||
|
this.applied_tags = data.applied_tags;
|
||||||
|
this.default_reaction_emoji = data.default_reaction_emoji;
|
||||||
|
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
|
||||||
|
this.default_sort_order = data.default_sort_order;
|
||||||
|
this.default_forum_layout = data.default_forum_layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static [Trait.fromDiscord]<T = Channel>(data: DiscordChannel): T {
|
||||||
|
return new Channel({
|
||||||
|
id: data.id,
|
||||||
|
type: data.type,
|
||||||
|
guild_id: data.guild_id,
|
||||||
|
position: data.position,
|
||||||
|
permission_overwrites: data.permission_overwrites,
|
||||||
|
name: data.name,
|
||||||
|
topic: data.topic,
|
||||||
|
nsfw: data.nsfw,
|
||||||
|
last_message_id: data.last_message_id,
|
||||||
|
bitrate: data.bitrate,
|
||||||
|
user_limit: data.user_limit,
|
||||||
|
rate_limit_per_user: data.rate_limit_per_user,
|
||||||
|
recipients: data.recipients,
|
||||||
|
icon: data.icon,
|
||||||
|
owner_id: data.owner_id,
|
||||||
|
application_id: data.application_id,
|
||||||
|
managed: data.managed,
|
||||||
|
parent_id: data.parent_id,
|
||||||
|
last_pin_timestamp: data.last_pin_timestamp,
|
||||||
|
rtc_region: data.rtc_region,
|
||||||
|
video_quality_mode: data.video_quality_mode,
|
||||||
|
message_count: data.message_count,
|
||||||
|
member_count: data.member_count,
|
||||||
|
thread_metadata: data.thread_metadata,
|
||||||
|
member: data.member,
|
||||||
|
default_auto_archive_duration: data.default_auto_archive_duration,
|
||||||
|
permissions: data.permissions,
|
||||||
|
flags: data.flags,
|
||||||
|
total_message_sent: data.total_message_sent,
|
||||||
|
available_tags: data.available_tags,
|
||||||
|
applied_tags: data.applied_tags,
|
||||||
|
default_reaction_emoji: data.default_reaction_emoji,
|
||||||
|
default_thread_rate_limit_per_user: data.default_thread_rate_limit_per_user,
|
||||||
|
default_sort_order: data.default_sort_order,
|
||||||
|
default_forum_layout: data.default_forum_layout,
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Channel Structure ===
|
||||||
|
|
||||||
|
// | Field | Type | Description |
|
||||||
|
// |-------------------------------------|-----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
// | id | snowflake | the id of this channel |
|
||||||
|
// | type | integer | the [type of channel](/docs/resources/channel#channel-object-channel-types) |
|
||||||
|
// | guild_id? | snowflake | the id of the guild (may be missing for some channel objects received over gateway guild dispatches) |
|
||||||
|
// | position? | integer | sorting position of the channel (channels with the same position are sorted by id) |
|
||||||
|
// | permission_overwrites? | array of [overwrite](/docs/resources/channel#overwrite-object) objects | explicit permission overwrites for members and roles |
|
||||||
|
// | name? | ?string | the name of the channel (1-100 characters) |
|
||||||
|
// | topic? | ?string | the channel topic (0-4096 characters for `GUILD_FORUM` and `GUILD_MEDIA` channels, 0-1024 characters for all others) |
|
||||||
|
// | nsfw? | boolean | whether the channel is nsfw |
|
||||||
|
// | last_message_id? | ?snowflake | the id of the last message sent in this channel (or thread for `GUILD_FORUM` or `GUILD_MEDIA` channels) (may not point to an existing or valid message or thread) |
|
||||||
|
// | bitrate? | integer | the bitrate (in bits) of the voice channel |
|
||||||
|
// | user_limit? | integer | the user limit of the voice channel |
|
||||||
|
// | rate_limit_per_user?\* | integer | amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission `manage_messages` or `manage_channel`, are unaffected |
|
||||||
|
// | recipients? | array of [user](/docs/resources/user#user-object) objects | the recipients of the DM |
|
||||||
|
// | icon? | ?string | icon hash of the group DM |
|
||||||
|
// | owner_id? | snowflake | id of the creator of the group DM or thread |
|
||||||
|
// | application_id? | snowflake | application id of the group DM creator if it is bot-created |
|
||||||
|
// | managed? | boolean | for group DM channels: whether the channel is managed by an application via the `gdm.join` OAuth2 scope |
|
||||||
|
// | parent_id? | ?snowflake | for guild channels: id of the parent category for a channel (each parent category can contain up to 50 channels), for threads: id of the text channel this thread was created |
|
||||||
|
// | last_pin_timestamp? | ?ISO8601 timestamp | when the last pinned message was pinned. This may be `null` in events such as `GUILD_CREATE` when a message is not pinned. |
|
||||||
|
// | rtc_region? | ?string | [voice region](/docs/resources/voice#voice-region-object) id for the voice channel, automatic when set to null |
|
||||||
|
// | video_quality_mode? | integer | the camera [video quality mode](/docs/resources/channel#channel-object-video-quality-modes) of the voice channel, 1 when not present |
|
||||||
|
// | message_count?\*\* | integer | number of messages (not including the initial message or deleted messages) in a thread. |
|
||||||
|
// | member_count? | integer | an approximate count of users in a thread, stops counting at 50 |
|
||||||
|
// | thread_metadata? | a [thread metadata](/docs/resources/channel#thread-metadata-object) object | thread-specific fields not needed by other channels |
|
||||||
|
// | member? | a [thread member](/docs/resources/channel#thread-member-object) object | thread member object for the current user, if they have joined the thread, only included on certain API endpoints |
|
||||||
|
// | default_auto_archive_duration? | integer | default duration, copied onto newly created threads, in minutes, threads will stop showing in the channel list after the specified period of inactivity, can be set to: 60, 1440, 4320, 10080 |
|
||||||
|
// | permissions? | string | computed permissions for the invoking user in the channel, including overwrites, only included when part of the `resolved` data received on an interaction. This does not include [implicit permissions](/docs/topics/permissions#implicit-permissions), which may need to be checked separately |
|
||||||
|
// | flags? | integer | [channel flags](/docs/resources/channel#channel-object-channel-flags) combined as a [bitfield](https://en.wikipedia.org/wiki/Bit_field) |
|
||||||
|
// | total_message_sent? | integer | number of messages ever sent in a thread, it's similar to `message_count` on message creation, but will not decrement the number when a message is deleted |
|
||||||
|
// | available_tags? | array of [tag](/docs/resources/channel#forum-tag-object) objects | the set of tags that can be used in a `GUILD_FORUM` or a `GUILD_MEDIA` channel |
|
||||||
|
// | applied_tags? | array of snowflakes | the IDs of the set of tags that have been applied to a thread in a `GUILD_FORUM` or a `GUILD_MEDIA` channel |
|
||||||
|
// | default_reaction_emoji? | ?[default reaction](/docs/resources/channel#default-reaction-object) object | the emoji to show in the add reaction button on a thread in a `GUILD_FORUM` or a `GUILD_MEDIA` channel |
|
||||||
|
// | default_thread_rate_limit_per_user? | integer | the initial `rate_limit_per_user` to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update. |
|
||||||
|
// | default_sort_order? | ?integer | the [default sort order type](/docs/resources/channel#channel-object-sort-order-types) used to order posts in `GUILD_FORUM` and `GUILD_MEDIA` channels. Defaults to `null`, which indicates a preferred sort order hasn't been set by a channel admin |
|
||||||
|
// | default_forum_layout? | integer | the [default forum layout view](/docs/resources/channel#channel-object-forum-layout-types) used to display posts in `GUILD_FORUM` channels. Defaults to `0`, which indicates a layout view has not been set by a channel admin |
|
||||||
|
|
||||||
|
export interface DiscordChannel {
|
||||||
|
id: string;
|
||||||
|
type: number;
|
||||||
|
guild_id?: string;
|
||||||
|
position?: number;
|
||||||
|
permission_overwrites?: PermissionOverwrite[];
|
||||||
|
name?: string;
|
||||||
|
topic?: string | null;
|
||||||
|
nsfw?: boolean;
|
||||||
|
last_message_id?: string | null;
|
||||||
|
bitrate?: number;
|
||||||
|
user_limit?: number;
|
||||||
|
rate_limit_per_user?: number;
|
||||||
|
recipients?: DiscordUser[];
|
||||||
|
icon?: string | null;
|
||||||
|
owner_id?: string;
|
||||||
|
application_id?: string;
|
||||||
|
managed?: boolean;
|
||||||
|
parent_id?: string | null;
|
||||||
|
last_pin_timestamp?: string | null;
|
||||||
|
rtc_region?: string | null;
|
||||||
|
video_quality_mode?: number;
|
||||||
|
message_count?: number;
|
||||||
|
member_count?: number;
|
||||||
|
thread_metadata?: ThreadMetadata;
|
||||||
|
member?: ThreadMember;
|
||||||
|
default_auto_archive_duration?: number;
|
||||||
|
permissions?: string;
|
||||||
|
flags?: number;
|
||||||
|
total_message_sent?: number;
|
||||||
|
available_tags?: ForumTag[];
|
||||||
|
applied_tags?: string[];
|
||||||
|
default_reaction_emoji?: DefaultReaction;
|
||||||
|
default_thread_rate_limit_per_user?: number;
|
||||||
|
default_sort_order?: number | null;
|
||||||
|
default_forum_layout?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
import { UnicordCommandContext } from '../context';
|
||||||
|
|
||||||
|
export type Primitive = string | number | boolean | null;
|
||||||
|
|
||||||
|
export interface UnicordConfig {
|
||||||
|
application_id?: string;
|
||||||
|
token?: string;
|
||||||
|
prefix?: string;
|
||||||
|
intents?: Intent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnicordConfigWithDefaults extends UnicordConfig {
|
||||||
|
application_id: string;
|
||||||
|
token: string;
|
||||||
|
intents: Intent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Brand<T, K> = T & { __brand: K };
|
||||||
|
|
||||||
|
export type EventName = Brand<string, 'event-name'>;
|
||||||
|
|
||||||
|
export enum UnicordApplicationCommandType {
|
||||||
|
Chat = 1,
|
||||||
|
User = 2,
|
||||||
|
Message = 3,
|
||||||
|
PrimaryEntryPoint = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UnicordArgumentType {
|
||||||
|
SubCommand = 1,
|
||||||
|
SubCommandGroup = 2,
|
||||||
|
String = 3,
|
||||||
|
Integer = 4,
|
||||||
|
Boolean = 5,
|
||||||
|
User = 6,
|
||||||
|
Channel = 7,
|
||||||
|
Role = 8,
|
||||||
|
Mentionable = 9,
|
||||||
|
Number = 10,
|
||||||
|
Attachment = 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UnicordChannelType {
|
||||||
|
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,
|
||||||
|
GuildMedia = 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old 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 interface UnicordArgumentDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: UnicordArgumentType;
|
||||||
|
required: boolean;
|
||||||
|
choices?: Array<{ name: string; value: string | number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnicordCommandOptions {
|
||||||
|
description?: string;
|
||||||
|
args: UnicordArgumentDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UnicordEventType {
|
||||||
|
SystemEvent = 'system',
|
||||||
|
ChatCommands = 'chat_commands',
|
||||||
|
ApplicationCommands = 'application_commands',
|
||||||
|
Interactions = 'interactions',
|
||||||
|
Custom = 'custom',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnicordBaseContext extends Record<string, any> {
|
||||||
|
type: UnicordEventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnicordEventContext extends UnicordBaseContext {}
|
||||||
|
|
||||||
|
export type DefinitionTypeMap = {
|
||||||
|
[UnicordArgumentType.String]: string;
|
||||||
|
[UnicordArgumentType.Integer]: number;
|
||||||
|
[UnicordArgumentType.Boolean]: boolean;
|
||||||
|
[UnicordArgumentType.Number]: number;
|
||||||
|
// [UnicordArgumentType.User]: User, // Could be a user ID or object depending on your use case
|
||||||
|
[UnicordArgumentType.Channel]: string; // Could be a channel ID or object
|
||||||
|
[UnicordArgumentType.Role]: string; // Could be a role ID or object
|
||||||
|
[UnicordArgumentType.Mentionable]: string; // Could be a mentionable ID or object
|
||||||
|
[UnicordArgumentType.Attachment]: string; // Could represent attachment IDs
|
||||||
|
[UnicordArgumentType.SubCommand]: string; // Handled by `options`
|
||||||
|
[UnicordArgumentType.SubCommandGroup]: string; // Handled by `options`
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ArgumentTypeFromOptions<T extends readonly UnicordArgumentDefinition[]> = {
|
||||||
|
[Definition in T[number] as Definition['name']]: Definition['type'] extends keyof DefinitionTypeMap
|
||||||
|
? Definition['choices'] extends readonly { value: infer U }[]
|
||||||
|
? Definition['required'] extends true // If required, keep as is; otherwise, make optional
|
||||||
|
? U
|
||||||
|
: U | undefined
|
||||||
|
: Definition['required'] extends true // If required, keep as is; otherwise, make optional
|
||||||
|
? DefinitionTypeMap[Definition['type']]
|
||||||
|
: DefinitionTypeMap[Definition['type']] | undefined
|
||||||
|
: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnicordCommandHandler<Options extends UnicordCommandOptions> = (
|
||||||
|
context: UnicordCommandContext,
|
||||||
|
args: ArgumentTypeFromOptions<Options['args']>,
|
||||||
|
) => any;
|
||||||
|
|
||||||
|
// TODO: Fill out signature
|
||||||
|
export type UnicordEventHandler = (context: UnicordEventContext, payload: any) => any;
|
||||||
|
|
||||||
|
export type UnicordHandler<Options extends UnicordCommandOptions> =
|
||||||
|
| UnicordCommandHandler<Options>
|
||||||
|
| UnicordEventHandler;
|
||||||
|
|
||||||
|
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, R = true> = Extract<K, T[number]> extends never
|
||||||
|
? undefined
|
||||||
|
: R;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
d: Record<string, any> | Primitive | null;
|
||||||
|
s: number | null;
|
||||||
|
t: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum InteractionResponseType {
|
||||||
|
Pong = 1,
|
||||||
|
Message = 4,
|
||||||
|
DeferredMessage = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Intent {
|
||||||
|
GUILDS = 1 << 0,
|
||||||
|
GUILD_MEMBERS = 1 << 1,
|
||||||
|
GUILD_BANS = 1 << 2,
|
||||||
|
GUILD_EMOJIS_AND_STICKERS = 1 << 3,
|
||||||
|
GUILD_INTEGRATIONS = 1 << 4,
|
||||||
|
GUILD_WEBHOOKS = 1 << 5,
|
||||||
|
GUILD_INVITES = 1 << 6,
|
||||||
|
GUILD_VOICE_STATES = 1 << 7,
|
||||||
|
GUILD_PRESENCES = 1 << 8,
|
||||||
|
GUILD_MESSAGES = 1 << 9,
|
||||||
|
GUILD_MESSAGE_REACTIONS = 1 << 10,
|
||||||
|
GUILD_MESSAGE_TYPING = 1 << 11,
|
||||||
|
DIRECT_MESSAGES = 1 << 12,
|
||||||
|
DIRECT_MESSAGE_REACTIONS = 1 << 13,
|
||||||
|
DIRECT_MESSAGE_TYPING = 1 << 14,
|
||||||
|
MESSAGE_CONTENT = 1 << 15,
|
||||||
|
DEFAULT = 513,
|
||||||
|
ALL = 32767,
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
|
||||||
|
[Intent.GUILD_MEMBERS]: [
|
||||||
|
['GUILD_MEMBER_ADD', 'GUILD_MEMBER_UPDATE', 'GUILD_MEMBER_REMOVE', 'THREAD_MEMBERS_UPDATE *'],
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
|
||||||
|
[Intent.GUILD_BANS]: [['GUILD_BAN_ADD', 'GUILD_BAN_REMOVE'], 2],
|
||||||
|
|
||||||
|
[Intent.GUILD_EMOJIS_AND_STICKERS]: [['GUILD_EMOJIS_UPDATE', 'GUILD_STICKERS_UPDATE'], 3],
|
||||||
|
|
||||||
|
[Intent.GUILD_INTEGRATIONS]: [
|
||||||
|
['GUILD_INTEGRATIONS_UPDATE', 'INTEGRATION_CREATE', 'INTEGRATION_UPDATE', 'INTEGRATION_DELETE'],
|
||||||
|
4,
|
||||||
|
],
|
||||||
|
|
||||||
|
[Intent.GUILD_WEBHOOKS]: [['WEBHOOKS_UPDATE'], 5],
|
||||||
|
|
||||||
|
[Intent.GUILD_INVITES]: [['INVITE_CREATE', 'INVITE_DELETE'], 6],
|
||||||
|
|
||||||
|
[Intent.GUILD_VOICE_STATES]: [['VOICE_STATE_UPDATE'], 7],
|
||||||
|
|
||||||
|
[Intent.GUILD_PRESENCES]: [['PRESENCE_UPDATE'], 8],
|
||||||
|
|
||||||
|
[Intent.GUILD_MESSAGES]: [['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',
|
||||||
|
],
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
|
||||||
|
[Intent.GUILD_MESSAGE_TYPING]: [['TYPING_START'], 11],
|
||||||
|
|
||||||
|
[Intent.DIRECT_MESSAGES]: [['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',
|
||||||
|
],
|
||||||
|
13,
|
||||||
|
],
|
||||||
|
|
||||||
|
[Intent.DIRECT_MESSAGE_TYPING]: [['TYPING_START'], 14],
|
||||||
|
|
||||||
|
[Intent.MESSAGE_CONTENT]: [['MESSAGE_CONTENT'], 15],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordAPIEvent =
|
||||||
|
| 'READY'
|
||||||
|
| 'RESUMED'
|
||||||
|
| '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'
|
||||||
|
| 'GUILD_MEMBER_ADD'
|
||||||
|
| 'GUILD_MEMBER_UPDATE'
|
||||||
|
| 'GUILD_MEMBER_REMOVE'
|
||||||
|
| 'GUILD_MEMBERS_CHUNK *'
|
||||||
|
| 'GUILD_BAN_ADD'
|
||||||
|
| 'GUILD_BAN_REMOVE'
|
||||||
|
| 'GUILD_EMOJIS_UPDATE'
|
||||||
|
| 'GUILD_STICKERS_UPDATE'
|
||||||
|
| 'GUILD_INTEGRATIONS_UPDATE'
|
||||||
|
| 'INTEGRATION_CREATE'
|
||||||
|
| 'INTEGRATION_UPDATE'
|
||||||
|
| 'INTEGRATION_DELETE'
|
||||||
|
| 'WEBHOOKS_UPDATE'
|
||||||
|
| 'INVITE_CREATE'
|
||||||
|
| 'INVITE_DELETE'
|
||||||
|
| 'VOICE_STATE_UPDATE'
|
||||||
|
| 'MESSAGE_CREATE'
|
||||||
|
| 'MESSAGE_UPDATE'
|
||||||
|
| 'MESSAGE_DELETE'
|
||||||
|
| 'MESSAGE_DELETE_BULK'
|
||||||
|
| 'MESSAGE_REACTION_ADD'
|
||||||
|
| 'MESSAGE_REACTION_REMOVE'
|
||||||
|
| 'MESSAGE_REACTION_REMOVE_ALL'
|
||||||
|
| 'MESSAGE_REACTION_REMOVE_EMOJI'
|
||||||
|
| 'TYPING_START'
|
||||||
|
| 'PRESENCE_UPDATE'
|
||||||
|
| 'APPLICATION_COMMAND_CREATE'
|
||||||
|
| 'APPLICATION_COMMAND_UPDATE'
|
||||||
|
| 'APPLICATION_COMMAND_DELETE';
|
||||||
|
|
||||||
|
export enum DiscordAPIEvents {
|
||||||
|
READY = 'READY',
|
||||||
|
RESUMED = 'RESUMED',
|
||||||
|
CHANNEL_CREATE = 'CHANNEL_CREATE',
|
||||||
|
CHANNEL_UPDATE = 'CHANNEL_UPDATE',
|
||||||
|
CHANNEL_DELETE = 'CHANNEL_DELETE',
|
||||||
|
CHANNEL_PINS_UPDATE = 'CHANNEL_PINS_UPDATE',
|
||||||
|
THREAD_CREATE = 'THREAD_CREATE',
|
||||||
|
THREAD_UPDATE = 'THREAD_UPDATE',
|
||||||
|
THREAD_DELETE = 'THREAD_DELETE',
|
||||||
|
THREAD_LIST_SYNC = 'THREAD_LIST_SYNC',
|
||||||
|
THREAD_MEMBER_UPDATE = 'THREAD_MEMBER_UPDATE',
|
||||||
|
THREAD_MEMBERS_UPDATE = 'THREAD_MEMBERS_UPDATE *',
|
||||||
|
STAGE_INSTANCE_CREATE = 'STAGE_INSTANCE_CREATE',
|
||||||
|
STAGE_INSTANCE_UPDATE = 'STAGE_INSTANCE_UPDATE',
|
||||||
|
STAGE_INSTANCE_DELETE = 'STAGE_INSTANCE_DELETE',
|
||||||
|
GUILD_CREATE = 'GUILD_CREATE',
|
||||||
|
GUILD_UPDATE = 'GUILD_UPDATE',
|
||||||
|
GUILD_DELETE = 'GUILD_DELETE',
|
||||||
|
GUILD_ROLE_CREATE = 'GUILD_ROLE_CREATE',
|
||||||
|
GUILD_ROLE_UPDATE = 'GUILD_ROLE_UPDATE',
|
||||||
|
GUILD_ROLE_DELETE = 'GUILD_ROLE_DELETE',
|
||||||
|
GUILD_MEMBER_ADD = 'GUILD_MEMBER_ADD',
|
||||||
|
GUILD_MEMBER_UPDATE = 'GUILD_MEMBER_UPDATE',
|
||||||
|
GUILD_MEMBER_REMOVE = 'GUILD_MEMBER_REMOVE',
|
||||||
|
GUILD_MEMBERS_CHUNK = 'GUILD_MEMBERS_CHUNK *',
|
||||||
|
GUILD_BAN_ADD = 'GUILD_BAN_ADD',
|
||||||
|
GUILD_BAN_REMOVE = 'GUILD_BAN_REMOVE',
|
||||||
|
GUILD_EMOJIS_UPDATE = 'GUILD_EMOJIS_UPDATE',
|
||||||
|
GUILD_STICKERS_UPDATE = 'GUILD_STICKERS_UPDATE',
|
||||||
|
GUILD_INTEGRATIONS_UPDATE = 'GUILD_INTEGRATIONS_UPDATE',
|
||||||
|
INTEGRATION_CREATE = 'INTEGRATION_CREATE',
|
||||||
|
INTEGRATION_UPDATE = 'INTEGRATION_UPDATE',
|
||||||
|
INTEGRATION_DELETE = 'INTEGRATION_DELETE',
|
||||||
|
WEBHOOKS_UPDATE = 'WEBHOOKS_UPDATE',
|
||||||
|
INVITE_CREATE = 'INVITE_CREATE',
|
||||||
|
INVITE_DELETE = 'INVITE_DELETE',
|
||||||
|
VOICE_STATE_UPDATE = 'VOICE_STATE_UPDATE',
|
||||||
|
MESSAGE_CREATE = 'MESSAGE_CREATE',
|
||||||
|
MESSAGE_UPDATE = 'MESSAGE_UPDATE',
|
||||||
|
MESSAGE_DELETE = 'MESSAGE_DELETE',
|
||||||
|
MESSAGE_DELETE_BULK = 'MESSAGE_DELETE_BULK',
|
||||||
|
MESSAGE_REACTION_ADD = 'MESSAGE_REACTION_ADD',
|
||||||
|
MESSAGE_REACTION_REMOVE = 'MESSAGE_REACTION_REMOVE',
|
||||||
|
MESSAGE_REACTION_REMOVE_ALL = 'MESSAGE_REACTION_REMOVE_ALL',
|
||||||
|
MESSAGE_REACTION_REMOVE_EMOJI = 'MESSAGE_REACTION_REMOVE_EMOJI',
|
||||||
|
TYPING_START = 'TYPING_START',
|
||||||
|
PRESENCE_UPDATE = 'PRESENCE_UPDATE',
|
||||||
|
APPLICATION_COMMAND_CREATE = 'APPLICATION_COMMAND_CREATE',
|
||||||
|
APPLICATION_COMMAND_UPDATE = 'APPLICATION_COMMAND_UPDATE',
|
||||||
|
APPLICATION_COMMAND_DELETE = 'APPLICATION_COMMAND_DELETE',
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Embed Structure ===
|
||||||
|
|
||||||
|
// | Field | Type | Description |
|
||||||
|
// |--------------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|
|
||||||
|
// | title? | string | title of embed |
|
||||||
|
// | type? | string | [type of embed](/docs/resources/message#embed-object-embed-types) (always "rich" for webhook embeds) |
|
||||||
|
// | description? | string | description of embed |
|
||||||
|
// | url? | string | url of embed |
|
||||||
|
// | timestamp? | ISO8601 timestamp | timestamp of embed content |
|
||||||
|
// | color? | integer | color code of the embed |
|
||||||
|
// | footer? | [embed footer](/docs/resources/message#embed-object-embed-footer-structure) object | footer information |
|
||||||
|
// | image? | [embed image](/docs/resources/message#embed-object-embed-image-structure) object | image information |
|
||||||
|
// | thumbnail? | [embed thumbnail](/docs/resources/message#embed-object-embed-thumbnail-structure) object | thumbnail information |
|
||||||
|
// | video? | [embed video](/docs/resources/message#embed-object-embed-video-structure) object | video information |
|
||||||
|
// | provider? | [embed provider](/docs/resources/message#embed-object-embed-provider-structure) object | provider information |
|
||||||
|
// | author? | [embed author](/docs/resources/message#embed-object-embed-author-structure) object | author information |
|
||||||
|
// | fields? | array of [embed field](/docs/resources/message#embed-object-embed-field-structure) objects | fields information, max of 25 |
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscordEmbed {
|
||||||
|
title?: string;
|
||||||
|
type?: 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[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { UnicordGatewayObject } from './base';
|
||||||
|
import { Trait } from './common';
|
||||||
|
// import { Emoji } from "./emoji";
|
||||||
|
// import { Member } from "./member";
|
||||||
|
// import { Role } from "./role";
|
||||||
|
|
||||||
|
export class Guild extends UnicordGatewayObject<Guild, DiscordGuild> {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
icon_hash?: string;
|
||||||
|
splash: string;
|
||||||
|
discovery_splash: string;
|
||||||
|
owner?: boolean;
|
||||||
|
owner_id: string;
|
||||||
|
permissions?: string;
|
||||||
|
region?: string;
|
||||||
|
afk_channel_id: string;
|
||||||
|
afk_timeout: number;
|
||||||
|
widget_enabled?: boolean;
|
||||||
|
widget_channel_id?: string;
|
||||||
|
verification_level: number;
|
||||||
|
default_message_notifications: number;
|
||||||
|
explicit_content_filter: number;
|
||||||
|
roles: Record<string, any>[];
|
||||||
|
emojis: Record<string, any>[];
|
||||||
|
features: Record<string, any>[];
|
||||||
|
mfa_level: number;
|
||||||
|
application_id: string;
|
||||||
|
system_channel_id: string;
|
||||||
|
system_channel_flags: number;
|
||||||
|
rules_channel_id: string;
|
||||||
|
max_presences?: number;
|
||||||
|
max_members?: number;
|
||||||
|
vanity_url_code: string;
|
||||||
|
description: string;
|
||||||
|
banner: string;
|
||||||
|
premium_tier: number;
|
||||||
|
premium_subscription_count?: number;
|
||||||
|
preferred_locale: string;
|
||||||
|
public_updates_channel_id: string;
|
||||||
|
max_video_channel_users?: number;
|
||||||
|
max_stage_video_channel_users?: number;
|
||||||
|
approximate_member_count?: number;
|
||||||
|
approximate_presence_count?: number;
|
||||||
|
welcome_screen?: Record<string, any>;
|
||||||
|
nsfw_level: number;
|
||||||
|
stickers?: string;
|
||||||
|
premium_progress_bar_enabled: boolean;
|
||||||
|
safety_alerts_channel_id: string;
|
||||||
|
incidents_data: Record<string, any>;
|
||||||
|
|
||||||
|
constructor(data: DiscordGuild) {
|
||||||
|
super();
|
||||||
|
this.id = data.id;
|
||||||
|
this.name = data.name;
|
||||||
|
this.icon = data.icon;
|
||||||
|
this.icon_hash = data.icon_hash;
|
||||||
|
this.splash = data.splash;
|
||||||
|
this.discovery_splash = data.discovery_splash;
|
||||||
|
this.owner = data.owner;
|
||||||
|
this.owner_id = data.owner_id;
|
||||||
|
this.permissions = data.permissions;
|
||||||
|
this.region = data.region;
|
||||||
|
this.afk_channel_id = data.afk_channel_id;
|
||||||
|
this.afk_timeout = data.afk_timeout;
|
||||||
|
this.widget_enabled = data.widget_enabled;
|
||||||
|
this.widget_channel_id = data.widget_channel_id;
|
||||||
|
this.verification_level = data.verification_level;
|
||||||
|
this.default_message_notifications = data.default_message_notifications;
|
||||||
|
this.explicit_content_filter = data.explicit_content_filter;
|
||||||
|
this.roles = data.roles;
|
||||||
|
this.emojis = data.emojis;
|
||||||
|
this.features = data.features;
|
||||||
|
this.mfa_level = data.mfa_level;
|
||||||
|
this.application_id = data.application_id;
|
||||||
|
this.system_channel_id = data.system_channel_id;
|
||||||
|
this.system_channel_flags = data.system_channel_flags;
|
||||||
|
this.rules_channel_id = data.rules_channel_id;
|
||||||
|
this.max_presences = data.max_presences;
|
||||||
|
this.max_members = data.max_members;
|
||||||
|
this.vanity_url_code = data.vanity_url_code;
|
||||||
|
this.description = data.description;
|
||||||
|
this.banner = data.banner;
|
||||||
|
this.premium_tier = data.premium_tier;
|
||||||
|
this.premium_subscription_count = data.premium_subscription_count;
|
||||||
|
this.preferred_locale = data.preferred_locale;
|
||||||
|
this.public_updates_channel_id = data.public_updates_channel_id;
|
||||||
|
this.max_video_channel_users = data.max_video_channel_users;
|
||||||
|
this.max_stage_video_channel_users = data.max_stage_video_channel_users;
|
||||||
|
this.approximate_member_count = data.approximate_member_count;
|
||||||
|
this.approximate_presence_count = data.approximate_presence_count;
|
||||||
|
this.welcome_screen = data.welcome_screen;
|
||||||
|
this.nsfw_level = data.nsfw_level;
|
||||||
|
this.stickers = data.stickers;
|
||||||
|
this.premium_progress_bar_enabled = data.premium_progress_bar_enabled;
|
||||||
|
this.safety_alerts_channel_id = data.safety_alerts_channel_id;
|
||||||
|
this.incidents_data = data.incidents_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static [Trait.fromDiscord]<T = Guild>(data: DiscordGuild): T {
|
||||||
|
return new Guild(data) as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Guild Structure ===
|
||||||
|
|
||||||
|
// | Field | Type | Description |
|
||||||
|
// |--------------------------------|-------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
// | id | snowflake | guild id |
|
||||||
|
// | name | string | guild name (2-100 characters, excluding trailing and leading whitespace) |
|
||||||
|
// | icon | ?string | [icon hash](/docs/reference#image-formatting) |
|
||||||
|
// | icon_hash? | ?string | [icon hash](/docs/reference#image-formatting), returned when in the template object |
|
||||||
|
// | splash | ?string | [splash hash](/docs/reference#image-formatting) |
|
||||||
|
// | discovery_splash | ?string | [discovery splash hash](/docs/reference#image-formatting); only present for guilds with the "DISCOVERABLE" feature |
|
||||||
|
// | owner? \* | boolean | true if [the user](/docs/resources/user#get-current-user-guilds) is the owner of the guild |
|
||||||
|
// | owner_id | snowflake | id of owner |
|
||||||
|
// | permissions? \* | string | total permissions for [the user](/docs/resources/user#get-current-user-guilds) in the guild (excludes overwrites and [implicit permissions](/docs/topics/permissions#implicit-permissions)) |
|
||||||
|
// | region? \*\* | ?string | [voice region](/docs/resources/voice#voice-region-object) id for the guild (deprecated) |
|
||||||
|
// | afk_channel_id | ?snowflake | id of afk channel |
|
||||||
|
// | afk_timeout | integer | afk timeout in seconds |
|
||||||
|
// | widget_enabled? | boolean | true if the server widget is enabled |
|
||||||
|
// | widget_channel_id? | ?snowflake | the channel id that the widget will generate an invite to, or `null` if set to no invite |
|
||||||
|
// | verification_level | integer | [verification level](/docs/resources/guild#guild-object-verification-level) required for the guild |
|
||||||
|
// | default_message_notifications | integer | default [message notifications level](/docs/resources/guild#guild-object-default-message-notification-level) |
|
||||||
|
// | explicit_content_filter | integer | [explicit content filter level](/docs/resources/guild#guild-object-explicit-content-filter-level) |
|
||||||
|
// | roles | array of [role](/docs/topics/permissions#role-object) objects | roles in the guild |
|
||||||
|
// | emojis | array of [emoji](/docs/resources/emoji#emoji-object) objects | custom guild emojis |
|
||||||
|
// | features | array of [guild feature](/docs/resources/guild#guild-object-guild-features) strings | enabled guild features |
|
||||||
|
// | mfa_level | integer | required [MFA level](/docs/resources/guild#guild-object-mfa-level) for the guild |
|
||||||
|
// | application_id | ?snowflake | application id of the guild creator if it is bot-created |
|
||||||
|
// | system_channel_id | ?snowflake | the id of the channel where guild notices such as welcome messages and boost events are posted |
|
||||||
|
// | system_channel_flags | integer | [system channel flags](/docs/resources/guild#guild-object-system-channel-flags) |
|
||||||
|
// | rules_channel_id | ?snowflake | the id of the channel where Community guilds can display rules and/or guidelines |
|
||||||
|
// | max_presences? | ?integer | the maximum number of presences for the guild (`null` is always returned, apart from the largest of guilds) |
|
||||||
|
// | max_members? | integer | the maximum number of members for the guild |
|
||||||
|
// | vanity_url_code | ?string | the vanity url code for the guild |
|
||||||
|
// | description | ?string | the description of a guild |
|
||||||
|
// | banner | ?string | [banner hash](/docs/reference#image-formatting) |
|
||||||
|
// | premium_tier | integer | [premium tier](/docs/resources/guild#guild-object-premium-tier) (Server Boost level) |
|
||||||
|
// | premium_subscription_count? | integer | the number of boosts this guild currently has |
|
||||||
|
// | preferred_locale | string | the preferred [locale](/docs/reference#locales) of a Community guild; used in server discovery and notices from Discord, and sent in interactions; defaults to "en-US" |
|
||||||
|
// | public_updates_channel_id | ?snowflake | the id of the channel where admins and moderators of Community guilds receive notices from Discord |
|
||||||
|
// | max_video_channel_users? | integer | the maximum amount of users in a video channel |
|
||||||
|
// | max_stage_video_channel_users? | integer | the maximum amount of users in a stage video channel |
|
||||||
|
// | approximate_member_count? | integer | approximate number of members in this guild, returned from the `GET /guilds/<id>` and `/users/@me/guilds` endpoints when `with_counts` is `true` |
|
||||||
|
// | approximate_presence_count? | integer | approximate number of non-offline members in this guild, returned from the `GET /guilds/<id>` and `/users/@me/guilds` endpoints when `with_counts` is `true` |
|
||||||
|
// | welcome_screen? | [welcome screen](/docs/resources/guild#welcome-screen-object) object | the welcome screen of a Community guild, shown to new members, returned in an [Invite](/docs/resources/invite#invite-object)'s guild object |
|
||||||
|
// | nsfw_level | integer | [guild NSFW level](/docs/resources/guild#guild-object-guild-nsfw-level) |
|
||||||
|
// | stickers? | array of [sticker](/docs/resources/sticker#sticker-object) objects | custom guild stickers |
|
||||||
|
// | premium_progress_bar_enabled | boolean | whether the guild has the boost progress bar enabled |
|
||||||
|
// | safety_alerts_channel_id | ?snowflake | the id of the channel where admins and moderators of Community guilds receive safety alerts from Discord |
|
||||||
|
// | incidents_data | ?[incidents data](/docs/resources/guild#incidents-data-object) object | the incidents data for this guild |
|
||||||
|
|
||||||
|
export interface DiscordGuild {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
icon_hash?: string;
|
||||||
|
splash: string;
|
||||||
|
discovery_splash: string;
|
||||||
|
owner?: boolean;
|
||||||
|
owner_id: string;
|
||||||
|
permissions?: string;
|
||||||
|
region?: string;
|
||||||
|
afk_channel_id: string;
|
||||||
|
afk_timeout: number;
|
||||||
|
widget_enabled?: boolean;
|
||||||
|
widget_channel_id?: string;
|
||||||
|
verification_level: number;
|
||||||
|
default_message_notifications: number;
|
||||||
|
explicit_content_filter: number;
|
||||||
|
roles: Record<string, any>[];
|
||||||
|
emojis: Record<string, any>[];
|
||||||
|
features: Record<string, any>[];
|
||||||
|
mfa_level: number;
|
||||||
|
application_id: string;
|
||||||
|
system_channel_id: string;
|
||||||
|
system_channel_flags: number;
|
||||||
|
rules_channel_id: string;
|
||||||
|
max_presences?: number;
|
||||||
|
max_members?: number;
|
||||||
|
vanity_url_code: string;
|
||||||
|
description: string;
|
||||||
|
banner: string;
|
||||||
|
premium_tier: number;
|
||||||
|
premium_subscription_count?: number;
|
||||||
|
preferred_locale: string;
|
||||||
|
public_updates_channel_id: string;
|
||||||
|
max_video_channel_users?: number;
|
||||||
|
max_stage_video_channel_users?: number;
|
||||||
|
approximate_member_count?: number;
|
||||||
|
approximate_presence_count?: number;
|
||||||
|
welcome_screen?: Record<string, any>;
|
||||||
|
nsfw_level: number;
|
||||||
|
stickers?: string;
|
||||||
|
premium_progress_bar_enabled: boolean;
|
||||||
|
safety_alerts_channel_id: string;
|
||||||
|
incidents_data: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './common';
|
||||||
|
export * from './user';
|
||||||
|
export * from './channel';
|
||||||
|
export * from './guild';
|
||||||
|
export * from './message';
|
||||||
|
export * from './embed';
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import { exists, isNil } from '../utils';
|
||||||
|
import { UnicordGatewayObject } from './base';
|
||||||
|
import type { Channel } from './channel';
|
||||||
|
import { fromDiscord, Trait, type Expectation } from './common';
|
||||||
|
import type { DiscordEmbed, Embed } from './embed';
|
||||||
|
import type { Guild } from './guild';
|
||||||
|
import { User, type DiscordUser } 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 UnicordGatewayObject<Message, DiscordMessage> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,110 @@
|
|||||||
|
import { UnicordGatewayObject } from './base';
|
||||||
|
import { Trait } from './common';
|
||||||
|
|
||||||
|
export class User extends UnicordGatewayObject<User, 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;
|
||||||
|
// TODO: Add classes and enums for the following properties
|
||||||
|
flags?: number;
|
||||||
|
premium_type?: number;
|
||||||
|
public_flags?: number;
|
||||||
|
avatar_decoration_data?: Record<string, any>;
|
||||||
|
collectibles?: Record<string, any>;
|
||||||
|
primary_guild?: Record<string, any>;
|
||||||
|
|
||||||
|
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: DiscordUser) {
|
||||||
|
super();
|
||||||
|
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;
|
||||||
|
this.collectibles = data.collectibles;
|
||||||
|
this.primary_guild = data.primary_guild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(data) as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Discord User Structure ===
|
||||||
|
|
||||||
|
// | Field | Type | Description | Required OAuth2 Scope |
|
||||||
|
// |-------------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|-----------------------|
|
||||||
|
// | 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](/docs/reference#image-formatting) | 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](/docs/reference#image-formatting) | 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](/docs/reference#locales) | identify |
|
||||||
|
// | verified? | boolean | whether the email on this account has been verified | email |
|
||||||
|
// | email? | ?string | the user's email | email |
|
||||||
|
// | flags? | integer | the [flags](/docs/resources/user#user-object-user-flags) on a user's account | identify |
|
||||||
|
// | premium_type? | integer | the [type of Nitro subscription](/docs/resources/user#user-object-premium-types) on a user's account | identify |
|
||||||
|
// | public_flags? | integer | the public [flags](/docs/resources/user#user-object-user-flags) on a user's account | identify |
|
||||||
|
// | avatar_decoration_data? | ?[avatar decoration data](/docs/resources/user#avatar-decoration-data-object) object | data for the user's avatar decoration | identify |
|
||||||
|
// | collectibles? | ?[collectibles](/docs/resources/user#collectibles) object | data for the user's collectibles | identify |
|
||||||
|
// | primary_guild? | ?[user primary guild](/docs/resources/user#user-object-user-primary-guild) object | the user's primary guild | identify |
|
||||||
|
|
||||||
|
export interface 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>;
|
||||||
|
collectibles?: Record<string, any>;
|
||||||
|
primary_guild?: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Sets top level properties when undefined on the original object — cretates a new object, does not modify the original
|
||||||
|
* @param original original object
|
||||||
|
* @param defaults defailts to set on the object
|
||||||
|
* @returns a new object with top level defaults set
|
||||||
|
*/
|
||||||
|
export function setDefaults<T>(original: Record<string, any>, defaults: Record<string, any>): T {
|
||||||
|
const final: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const prop of Object.keys(defaults)) {
|
||||||
|
final[prop] = defaults[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop of Object.keys(original)) {
|
||||||
|
final[prop] = original[prop];
|
||||||
|
}
|
||||||
|
|
||||||
|
return final as T;
|
||||||
|
}
|
||||||
|
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().toLocaleTimeString()}]\x1b[0m`;
|
||||||
|
const prefix = `\x1b[35m[unicord]`;
|
||||||
|
console.log(`${timestamp} ${prefix} \x1b[0m`, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
+25
-89
@@ -1,89 +1,25 @@
|
|||||||
import { ComponentStyle, Context, Gateway, Guilds, Message } from './lib';
|
// Base classes
|
||||||
import { Command, CommandFuntion, Intent, Presence } from './types';
|
export { Client, Client as unicord } from './services/client';
|
||||||
export { ComponentStyle, Context, Gateway, Guilds, Message, Command, CommandFuntion, Intent, Presence };
|
export { Intent, Trait } from './types/common';
|
||||||
|
// Consumable types
|
||||||
type Config = {
|
export type { ClientConfig } from './types/common';
|
||||||
token: string;
|
export type { Context } from './types/context';
|
||||||
application_id: string;
|
export { Expectation } from './types/common';
|
||||||
prefix?: string;
|
export { ChannelType, ChannelFlags, VideoQualityMode, SortOrderType, ForumLayoutType } from './types/channel';
|
||||||
intents?: Intent[];
|
// Consumable util classes
|
||||||
};
|
export { Guild } from './types/guild';
|
||||||
|
export { Role } from './types/role';
|
||||||
export class Client {
|
export { Member } from './types/member';
|
||||||
readonly prefix: string;
|
export { User } from './types/user';
|
||||||
readonly token: string;
|
export { Message, type MessagePayload, ComponentStyle, ComponentType } from './types/message';
|
||||||
readonly application_id: string;
|
export { Embed } from './types/embed';
|
||||||
private readonly gateway: Gateway;
|
export { Emoji } from './types/emoji';
|
||||||
private readonly client_commands: Map<string, Command | CommandFuntion> = new Map();
|
// Other
|
||||||
readonly guilds: Guilds;
|
export {
|
||||||
|
createCommandHandler,
|
||||||
constructor({ token, application_id, prefix, intents }: Config) {
|
type InteractionData,
|
||||||
this.token = token;
|
type InteractionCommpoentData,
|
||||||
this.application_id = application_id;
|
type InteractionCommandData,
|
||||||
this.prefix = prefix ?? '!';
|
type InteractionHandlerFunction,
|
||||||
this.gateway = new Gateway({ token, intents, client: this });
|
} from './types/handler';
|
||||||
this.guilds = new Guilds({ token, gateway: this.gateway });
|
export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand';
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,77 @@
|
|||||||
|
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.config.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,15 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import type { Context } from '../types/context';
|
||||||
|
|
||||||
|
const EventBus = new EventEmitter() as Emitter;
|
||||||
|
|
||||||
|
export class Emitter extends EventEmitter {
|
||||||
|
emit(event_name: string, context: Context | null, ...args: any[]): boolean {
|
||||||
|
return super.emit(event_name, context, ...args);
|
||||||
|
}
|
||||||
|
on(event_name: string, listener: (context: Context | null, ...args: any[]) => void): this {
|
||||||
|
return super.on(event_name, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { EventBus };
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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: LRUCache<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[],
|
||||||
|
maxCapacity: number = 10_000,
|
||||||
|
) {
|
||||||
|
this.cache = new LRUCache<string, K>(maxCapacity);
|
||||||
|
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 when it finishes
|
||||||
|
req.finally(() => 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListNode<K, V> {
|
||||||
|
key: K;
|
||||||
|
value: V;
|
||||||
|
prev: ListNode<K, V> | null = null;
|
||||||
|
next: ListNode<K, V> | null = null;
|
||||||
|
|
||||||
|
constructor(key: K, value: V) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LRUCache<K, V> {
|
||||||
|
private capacity: number;
|
||||||
|
private map: Map<K, ListNode<K, V>>;
|
||||||
|
private head: ListNode<K, V>;
|
||||||
|
private tail: ListNode<K, V>;
|
||||||
|
|
||||||
|
constructor(capacity: number) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.map = new Map();
|
||||||
|
|
||||||
|
// Dummy head and tail to simplify boundary conditions
|
||||||
|
this.head = new ListNode<K, V>(null as any, null as any);
|
||||||
|
this.tail = new ListNode<K, V>(null as any, null as any);
|
||||||
|
this.head.next = this.tail;
|
||||||
|
this.tail.prev = this.head;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
if (!this.map.has(key)) return undefined;
|
||||||
|
|
||||||
|
const node = this.map.get(key)!;
|
||||||
|
this.moveToFront(node);
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.map.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): void {
|
||||||
|
if (this.map.has(key)) {
|
||||||
|
// Update existing node value and move it to front
|
||||||
|
const node = this.map.get(key)!;
|
||||||
|
node.value = value;
|
||||||
|
this.moveToFront(node);
|
||||||
|
} else {
|
||||||
|
// Create a new node
|
||||||
|
const newNode = new ListNode(key, value);
|
||||||
|
this.map.set(key, newNode);
|
||||||
|
this.addToFront(newNode);
|
||||||
|
|
||||||
|
// If over capacity, remove the least recently used (LRU) node
|
||||||
|
if (this.map.size > this.capacity) {
|
||||||
|
this.removeLRU();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveToFront(node: ListNode<K, V>): void {
|
||||||
|
this.removeNode(node);
|
||||||
|
this.addToFront(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToFront(node: ListNode<K, V>): void {
|
||||||
|
node.next = this.head.next;
|
||||||
|
node.prev = this.head;
|
||||||
|
this.head.next!.prev = node;
|
||||||
|
this.head.next = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeNode(node: ListNode<K, V>): void {
|
||||||
|
node.prev!.next = node.next;
|
||||||
|
node.next!.prev = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeLRU(): void {
|
||||||
|
const lruNode = this.tail.prev!;
|
||||||
|
this.removeNode(lruNode);
|
||||||
|
this.map.delete(lruNode.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||||
|
let current = this.head.next;
|
||||||
|
while (current && current !== this.tail) {
|
||||||
|
yield [current.key, current.value];
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { API } from "../api";
|
||||||
|
import { Processor } from "../processor";
|
||||||
|
import { Channel } from "../../types/channel";
|
||||||
|
import { ObjectCache } from "./cache";
|
||||||
|
|
||||||
|
export class Channels extends ObjectCache<Channel, Channel> {
|
||||||
|
constructor(api: API, processor: Processor) {
|
||||||
|
super(api, processor, 'channels', Channel, ['CHANNEL_CREATE', 'CHANNEL_UPDATE']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,41 @@
|
|||||||
|
import { fromDiscord } from "../../types/common";
|
||||||
|
import { Message, type MessagePayload } from "../../types/message";
|
||||||
|
import type { API } from "../api";
|
||||||
|
import type { Processor } from "../processor";
|
||||||
|
import { ObjectCache } from "./cache";
|
||||||
|
|
||||||
|
|
||||||
|
export class Messages extends ObjectCache<Message, MessagePayload> {
|
||||||
|
constructor (api: API, processor: Processor) {
|
||||||
|
super(api, processor, 'messages', 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,178 @@
|
|||||||
|
import { type DiscordAPIEvent, Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
|
||||||
|
import { Message, type DiscordMessage } from '../types/message';
|
||||||
|
import { exists, isNil } from '../utils/index';
|
||||||
|
import { API } from './api';
|
||||||
|
import { EventBus, type Emitter } from './bus';
|
||||||
|
import { Channels } from './caches/channels';
|
||||||
|
import { Guilds } from './caches/guilds';
|
||||||
|
import { Messages } from './caches/messages';
|
||||||
|
import { Users } from './caches/users';
|
||||||
|
import { Gateway } from './connectors/gateway';
|
||||||
|
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||||
|
import { Processor } from './processor';
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
readonly config: Readonly<Required<ClientConfig>>;
|
||||||
|
// Internal services
|
||||||
|
protected bus: Emitter;
|
||||||
|
protected api: API;
|
||||||
|
protected processor: Processor;
|
||||||
|
protected gateway: Gateway;
|
||||||
|
// Caches
|
||||||
|
readonly caches: {
|
||||||
|
readonly guilds: Guilds;
|
||||||
|
readonly users: Users;
|
||||||
|
readonly messages: Messages;
|
||||||
|
readonly channels: Channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(config: ClientConfig) {
|
||||||
|
// Validate the configuration
|
||||||
|
Client.validateConfig(config);
|
||||||
|
// Save the configuration
|
||||||
|
this.config = Object.freeze({
|
||||||
|
token: config.token,
|
||||||
|
application_id: config.application_id,
|
||||||
|
intents: config.intents ?? [Intent.DEFAULT],
|
||||||
|
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);
|
||||||
|
// Initialize caches
|
||||||
|
this.caches = {
|
||||||
|
guilds: new Guilds(this.api, this.processor),
|
||||||
|
users: new Users(this.api, this.processor),
|
||||||
|
messages: new Messages(this.api, this.processor),
|
||||||
|
channels: new Channels(this.api, this.processor),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
get chatCommands() {
|
||||||
|
return this.processor[HandlerType.ChatCommands];
|
||||||
|
}
|
||||||
|
|
||||||
|
get applicationCommands() {
|
||||||
|
return this.processor[HandlerType.ApplicationCommands];
|
||||||
|
}
|
||||||
|
|
||||||
|
get interactions() {
|
||||||
|
return this.processor[HandlerType.Interactions];
|
||||||
|
}
|
||||||
|
|
||||||
|
get events() {
|
||||||
|
return this.processor[HandlerType.Events];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a raw HTTP request to the Discord API.
|
||||||
|
* @param method HTTP method to use for the request (GET, POST, PUT, DELETE).
|
||||||
|
* @param endpoint The API endpoint to request; can start with a slash or be a full URL.
|
||||||
|
* @param body Optional body to send with the request, typically used for POST or PUT requests.
|
||||||
|
* @returns A promise that resolves to the response data of type T.
|
||||||
|
*/
|
||||||
|
rawRequest = async <T = Record<string, any>>(
|
||||||
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||||
|
endpoint: string,
|
||||||
|
body?: Record<string, any>,
|
||||||
|
): Promise<T> => {
|
||||||
|
return this.api.request<T>(method, endpoint, body);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.on(event, handler);
|
||||||
|
|
||||||
|
once = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.once(event, handler);
|
||||||
|
|
||||||
|
off = (event: DiscordAPIEvent, 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,112 @@
|
|||||||
|
import { Expectation } from "../types/common";
|
||||||
|
import { Context, type HydratedContext } from "../types/context";
|
||||||
|
import { Message, type HydratedMessage } from "../types/message";
|
||||||
|
import { Channel } from "../types/channel";
|
||||||
|
import { exists } from "../utils";
|
||||||
|
import type { API } from "./api";
|
||||||
|
import type { Client } from "./client";
|
||||||
|
|
||||||
|
export type GatewayObject = Message | Channel;
|
||||||
|
export type Hydrateable = Context | GatewayObject
|
||||||
|
export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
|
||||||
|
T extends Message ? HydratedMessage<K> & Omit<T, keyof HydratedMessage<K>> :
|
||||||
|
T extends Context ? HydratedContext<K> & Omit<T, keyof 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, K extends Array<Expectation>>(
|
||||||
|
data: T,
|
||||||
|
expectations: K,
|
||||||
|
client: Client,
|
||||||
|
api: API
|
||||||
|
): Promise<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.caches.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.caches.guilds.get(data.guild_id);
|
||||||
|
} else if (expectation === Expectation.Channel && exists(data.channel_id)) {
|
||||||
|
let context = data as unknown as HydratedContext<[Expectation.Channel, ...Expectation[]]>;
|
||||||
|
context.channel = await client.caches.channels.get(data.channel_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.caches.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.caches.guilds.get(data.guild_id);
|
||||||
|
} else if (expectation === Expectation.Channel && exists(data.channel_id)) {
|
||||||
|
let message = data as unknown as HydratedMessage<[Expectation.Channel, ...Expectation[]]>;
|
||||||
|
message.channel = await client.caches.channels.get(data.channel_id);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new object that preserves all existing properties
|
||||||
|
const result = { ...data } as any;
|
||||||
|
|
||||||
|
// Ensure the result is properly typed with both old and new hydrated properties
|
||||||
|
return result as T & 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, K extends Array<Expectation>>(
|
||||||
|
data: T,
|
||||||
|
expectations: K,
|
||||||
|
client: Client,
|
||||||
|
api: API
|
||||||
|
): Promise<(data: unknown) => data is 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 Hydrated<T, K> {
|
||||||
|
return hydrated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { ApplicationCommandOptionType, ApplicationCommandType, type ApplicationCommandOption } from "../types/applicationCommand";
|
||||||
|
import { HandlerType, Trait, type EventPayload } from "../types/common";
|
||||||
|
import type { Context } from "../types/context";
|
||||||
|
import { CommandHandler, OptionConstructorMap, type ArgsFromOptions, type Handler, type InteractionData } from "../types/handler";
|
||||||
|
import type { API } from "./api";
|
||||||
|
import type { Emitter } from "./bus";
|
||||||
|
import type { Client } from "./client";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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.caches.users.get(userId);
|
||||||
|
} else if (typeof arg === 'object') {
|
||||||
|
// Check if the object has an ID
|
||||||
|
if (arg.id) {
|
||||||
|
validated[def.name] = await this.client.caches.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 convert from arrow to regular 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.config.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,278 @@
|
|||||||
|
import { GatewayObject } from "./base";
|
||||||
|
import { Trait } from "./common";
|
||||||
|
import { User, type DiscordUser } from "./user";
|
||||||
|
|
||||||
|
export enum ChannelType {
|
||||||
|
GUILD_TEXT = 0,
|
||||||
|
DM = 1,
|
||||||
|
GUILD_VOICE = 2,
|
||||||
|
GROUP_DM = 3,
|
||||||
|
GUILD_CATEGORY = 4,
|
||||||
|
GUILD_ANNOUNCEMENT = 5,
|
||||||
|
ANNOUNCEMENT_THREAD = 10,
|
||||||
|
PUBLIC_THREAD = 11,
|
||||||
|
PRIVATE_THREAD = 12,
|
||||||
|
GUILD_STAGE_VOICE = 13,
|
||||||
|
GUILD_DIRECTORY = 14,
|
||||||
|
GUILD_FORUM = 15,
|
||||||
|
GUILD_MEDIA = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChannelFlags {
|
||||||
|
PINNED = 1 << 1,
|
||||||
|
REQUIRE_TAG = 1 << 4,
|
||||||
|
HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VideoQualityMode {
|
||||||
|
AUTO = 1,
|
||||||
|
FULL = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SortOrderType {
|
||||||
|
LATEST_ACTIVITY = 0,
|
||||||
|
CREATION_DATE = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ForumLayoutType {
|
||||||
|
NOT_SET = 0,
|
||||||
|
LIST_VIEW = 1,
|
||||||
|
GALLERY_VIEW = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermissionOverwrite = {
|
||||||
|
id: string;
|
||||||
|
type: 0 | 1; // 0 for role, 1 for member
|
||||||
|
allow: string;
|
||||||
|
deny: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThreadMetadata = {
|
||||||
|
archived: boolean;
|
||||||
|
auto_archive_duration: number;
|
||||||
|
archive_timestamp: string;
|
||||||
|
locked: boolean;
|
||||||
|
invitable?: boolean;
|
||||||
|
create_timestamp?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThreadMember = {
|
||||||
|
id?: string;
|
||||||
|
user_id?: string;
|
||||||
|
join_timestamp: string;
|
||||||
|
flags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ForumTag = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
moderated: boolean;
|
||||||
|
emoji_id?: string | null;
|
||||||
|
emoji_name?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultReaction = {
|
||||||
|
emoji_id?: string | null; // the id of a guild's custom emoji
|
||||||
|
emoji_name?: string | null; // the unicode character of the emoji
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Channel extends GatewayObject {
|
||||||
|
id: string;
|
||||||
|
type: ChannelType;
|
||||||
|
guild_id?: string;
|
||||||
|
position?: number;
|
||||||
|
permission_overwrites?: PermissionOverwrite[];
|
||||||
|
name?: string;
|
||||||
|
topic?: string | null;
|
||||||
|
nsfw?: boolean;
|
||||||
|
last_message_id?: string | null;
|
||||||
|
bitrate?: number;
|
||||||
|
user_limit?: number;
|
||||||
|
rate_limit_per_user?: number;
|
||||||
|
recipients?: User[];
|
||||||
|
icon?: string | null;
|
||||||
|
owner_id?: string;
|
||||||
|
application_id?: string;
|
||||||
|
managed?: boolean;
|
||||||
|
parent_id?: string | null;
|
||||||
|
last_pin_timestamp?: Date | null;
|
||||||
|
rtc_region?: string | null;
|
||||||
|
video_quality_mode?: VideoQualityMode;
|
||||||
|
message_count?: number;
|
||||||
|
member_count?: number;
|
||||||
|
thread_metadata?: ThreadMetadata;
|
||||||
|
member?: ThreadMember;
|
||||||
|
default_auto_archive_duration?: number;
|
||||||
|
permissions?: string;
|
||||||
|
flags?: number;
|
||||||
|
total_message_sent?: number;
|
||||||
|
available_tags?: ForumTag[];
|
||||||
|
applied_tags?: string[];
|
||||||
|
default_reaction_emoji?: DefaultReaction;
|
||||||
|
default_thread_rate_limit_per_user?: number;
|
||||||
|
default_sort_order?: SortOrderType | null;
|
||||||
|
default_forum_layout?: ForumLayoutType;
|
||||||
|
|
||||||
|
constructor(data: Partial<Channel>) {
|
||||||
|
super();
|
||||||
|
this.id = data.id!;
|
||||||
|
this.type = data.type!;
|
||||||
|
this.guild_id = data.guild_id;
|
||||||
|
this.position = data.position;
|
||||||
|
this.permission_overwrites = data.permission_overwrites;
|
||||||
|
this.name = data.name;
|
||||||
|
this.topic = data.topic;
|
||||||
|
this.nsfw = data.nsfw;
|
||||||
|
this.last_message_id = data.last_message_id;
|
||||||
|
this.bitrate = data.bitrate;
|
||||||
|
this.user_limit = data.user_limit;
|
||||||
|
this.rate_limit_per_user = data.rate_limit_per_user;
|
||||||
|
this.recipients = data.recipients;
|
||||||
|
this.icon = data.icon;
|
||||||
|
this.owner_id = data.owner_id;
|
||||||
|
this.application_id = data.application_id;
|
||||||
|
this.managed = data.managed;
|
||||||
|
this.parent_id = data.parent_id;
|
||||||
|
this.last_pin_timestamp = data.last_pin_timestamp instanceof Date
|
||||||
|
? data.last_pin_timestamp
|
||||||
|
: data.last_pin_timestamp
|
||||||
|
? new Date(data.last_pin_timestamp)
|
||||||
|
: null;
|
||||||
|
this.rtc_region = data.rtc_region;
|
||||||
|
this.video_quality_mode = data.video_quality_mode;
|
||||||
|
this.message_count = data.message_count;
|
||||||
|
this.member_count = data.member_count;
|
||||||
|
this.thread_metadata = data.thread_metadata;
|
||||||
|
this.member = data.member;
|
||||||
|
this.default_auto_archive_duration = data.default_auto_archive_duration;
|
||||||
|
this.permissions = data.permissions;
|
||||||
|
this.flags = data.flags;
|
||||||
|
this.total_message_sent = data.total_message_sent;
|
||||||
|
this.available_tags = data.available_tags;
|
||||||
|
this.applied_tags = data.applied_tags;
|
||||||
|
this.default_reaction_emoji = data.default_reaction_emoji;
|
||||||
|
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
|
||||||
|
this.default_sort_order = data.default_sort_order;
|
||||||
|
this.default_forum_layout = data.default_forum_layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static [Trait.fromDiscord]<T = Channel>(data: DiscordChannel): T {
|
||||||
|
return new Channel({
|
||||||
|
id: data.id,
|
||||||
|
type: data.type,
|
||||||
|
guild_id: data.guild_id,
|
||||||
|
position: data.position,
|
||||||
|
permission_overwrites: data.permission_overwrites,
|
||||||
|
name: data.name,
|
||||||
|
topic: data.topic,
|
||||||
|
nsfw: data.nsfw,
|
||||||
|
last_message_id: data.last_message_id,
|
||||||
|
bitrate: data.bitrate,
|
||||||
|
user_limit: data.user_limit,
|
||||||
|
rate_limit_per_user: data.rate_limit_per_user,
|
||||||
|
recipients: data.recipients?.map(u => User.fromDiscord(u)),
|
||||||
|
icon: data.icon,
|
||||||
|
owner_id: data.owner_id,
|
||||||
|
application_id: data.application_id,
|
||||||
|
managed: data.managed,
|
||||||
|
parent_id: data.parent_id,
|
||||||
|
last_pin_timestamp: data.last_pin_timestamp ? new Date(data.last_pin_timestamp) : null,
|
||||||
|
rtc_region: data.rtc_region,
|
||||||
|
video_quality_mode: data.video_quality_mode,
|
||||||
|
message_count: data.message_count,
|
||||||
|
member_count: data.member_count,
|
||||||
|
thread_metadata: data.thread_metadata,
|
||||||
|
member: data.member,
|
||||||
|
default_auto_archive_duration: data.default_auto_archive_duration,
|
||||||
|
permissions: data.permissions,
|
||||||
|
flags: data.flags,
|
||||||
|
total_message_sent: data.total_message_sent,
|
||||||
|
available_tags: data.available_tags,
|
||||||
|
applied_tags: data.applied_tags,
|
||||||
|
default_reaction_emoji: data.default_reaction_emoji,
|
||||||
|
default_thread_rate_limit_per_user: data.default_thread_rate_limit_per_user,
|
||||||
|
default_sort_order: data.default_sort_order,
|
||||||
|
default_forum_layout: data.default_forum_layout
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DiscordChannel {
|
||||||
|
id: string;
|
||||||
|
type: number;
|
||||||
|
guild_id?: string;
|
||||||
|
position?: number;
|
||||||
|
permission_overwrites?: PermissionOverwrite[];
|
||||||
|
name?: string;
|
||||||
|
topic?: string | null;
|
||||||
|
nsfw?: boolean;
|
||||||
|
last_message_id?: string | null;
|
||||||
|
bitrate?: number;
|
||||||
|
user_limit?: number;
|
||||||
|
rate_limit_per_user?: number;
|
||||||
|
recipients?: DiscordUser[];
|
||||||
|
icon?: string | null;
|
||||||
|
owner_id?: string;
|
||||||
|
application_id?: string;
|
||||||
|
managed?: boolean;
|
||||||
|
parent_id?: string | null;
|
||||||
|
last_pin_timestamp?: string | null;
|
||||||
|
rtc_region?: string | null;
|
||||||
|
video_quality_mode?: number;
|
||||||
|
message_count?: number;
|
||||||
|
member_count?: number;
|
||||||
|
thread_metadata?: ThreadMetadata;
|
||||||
|
member?: ThreadMember;
|
||||||
|
default_auto_archive_duration?: number;
|
||||||
|
permissions?: string;
|
||||||
|
flags?: number;
|
||||||
|
total_message_sent?: number;
|
||||||
|
available_tags?: ForumTag[];
|
||||||
|
applied_tags?: string[];
|
||||||
|
default_reaction_emoji?: DefaultReaction;
|
||||||
|
default_thread_rate_limit_per_user?: number;
|
||||||
|
default_sort_order?: number | null;
|
||||||
|
default_forum_layout?: number;
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
this.id = data.id;
|
||||||
|
this.type = data.type;
|
||||||
|
this.guild_id = data.guild_id;
|
||||||
|
this.position = data.position;
|
||||||
|
this.permission_overwrites = data.permission_overwrites;
|
||||||
|
this.name = data.name;
|
||||||
|
this.topic = data.topic;
|
||||||
|
this.nsfw = data.nsfw;
|
||||||
|
this.last_message_id = data.last_message_id;
|
||||||
|
this.bitrate = data.bitrate;
|
||||||
|
this.user_limit = data.user_limit;
|
||||||
|
this.rate_limit_per_user = data.rate_limit_per_user;
|
||||||
|
this.recipients = data.recipients;
|
||||||
|
this.icon = data.icon;
|
||||||
|
this.owner_id = data.owner_id;
|
||||||
|
this.application_id = data.application_id;
|
||||||
|
this.managed = data.managed;
|
||||||
|
this.parent_id = data.parent_id;
|
||||||
|
this.last_pin_timestamp = data.last_pin_timestamp;
|
||||||
|
this.rtc_region = data.rtc_region;
|
||||||
|
this.video_quality_mode = data.video_quality_mode;
|
||||||
|
this.message_count = data.message_count;
|
||||||
|
this.member_count = data.member_count;
|
||||||
|
this.thread_metadata = data.thread_metadata;
|
||||||
|
this.member = data.member;
|
||||||
|
this.default_auto_archive_duration = data.default_auto_archive_duration;
|
||||||
|
this.permissions = data.permissions;
|
||||||
|
this.flags = data.flags;
|
||||||
|
this.total_message_sent = data.total_message_sent;
|
||||||
|
this.available_tags = data.available_tags;
|
||||||
|
this.applied_tags = data.applied_tags;
|
||||||
|
this.default_reaction_emoji = data.default_reaction_emoji;
|
||||||
|
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
|
||||||
|
this.default_sort_order = data.default_sort_order;
|
||||||
|
this.default_forum_layout = data.default_forum_layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromAPIResponse(data: any): DiscordChannel {
|
||||||
|
return new DiscordChannel(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;
|
|
||||||
+228
-162
@@ -1,79 +1,66 @@
|
|||||||
|
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 = {
|
export type Payload = {
|
||||||
op: number; // opcode
|
op: number;
|
||||||
d: Record<string, any>; // data
|
d: Record<string, any> | Primitive | null;
|
||||||
s: number | null; // sequence number
|
s: number | null;
|
||||||
t: string | null; // event name
|
t: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Session = {
|
export enum InteractionResponseType {
|
||||||
s: number; // sequence number
|
Pong = 1,
|
||||||
session_id: string;
|
Message = 4,
|
||||||
session_type: string;
|
DeferredMessage = 5,
|
||||||
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 Intent {
|
export enum Intent {
|
||||||
@@ -94,110 +81,189 @@ export enum Intent {
|
|||||||
DIRECT_MESSAGE_TYPING = 1 << 14,
|
DIRECT_MESSAGE_TYPING = 1 << 14,
|
||||||
MESSAGE_CONTENT = 1 << 15,
|
MESSAGE_CONTENT = 1 << 15,
|
||||||
DEFAULT = 513,
|
DEFAULT = 513,
|
||||||
ALL = 32767
|
ALL = 32767,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntentGroup : {[key in Intent]: [string[], number]} = {
|
export const IntentGroup: { [key in Intent]?: [string[], number] } = {
|
||||||
[Intent.GUILDS]: [[
|
[Intent.GUILDS]: [
|
||||||
"GUILD_CREATE",
|
[
|
||||||
"GUILD_UPDATE",
|
'GUILD_CREATE',
|
||||||
"GUILD_DELETE",
|
'GUILD_UPDATE',
|
||||||
"GUILD_ROLE_CREATE",
|
'GUILD_DELETE',
|
||||||
"GUILD_ROLE_UPDATE",
|
'GUILD_ROLE_CREATE',
|
||||||
"GUILD_ROLE_DELETE",
|
'GUILD_ROLE_UPDATE',
|
||||||
"CHANNEL_CREATE",
|
'GUILD_ROLE_DELETE',
|
||||||
"CHANNEL_UPDATE",
|
'CHANNEL_CREATE',
|
||||||
"CHANNEL_DELETE",
|
'CHANNEL_UPDATE',
|
||||||
"CHANNEL_PINS_UPDATE",
|
'CHANNEL_DELETE',
|
||||||
"THREAD_CREATE",
|
'CHANNEL_PINS_UPDATE',
|
||||||
"THREAD_UPDATE",
|
'THREAD_CREATE',
|
||||||
"THREAD_DELETE",
|
'THREAD_UPDATE',
|
||||||
"THREAD_LIST_SYNC",
|
'THREAD_DELETE',
|
||||||
"THREAD_MEMBER_UPDATE",
|
'THREAD_LIST_SYNC',
|
||||||
"THREAD_MEMBERS_UPDATE *",
|
'THREAD_MEMBER_UPDATE',
|
||||||
"STAGE_INSTANCE_CREATE",
|
'THREAD_MEMBERS_UPDATE *',
|
||||||
"STAGE_INSTANCE_UPDATE",
|
'STAGE_INSTANCE_CREATE',
|
||||||
"STAGE_INSTANCE_DELETE"
|
'STAGE_INSTANCE_UPDATE',
|
||||||
], 0],
|
'STAGE_INSTANCE_DELETE',
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
|
||||||
[Intent.GUILD_MEMBERS]: [[
|
[Intent.GUILD_MEMBERS]: [
|
||||||
"GUILD_MEMBER_ADD",
|
['GUILD_MEMBER_ADD', 'GUILD_MEMBER_UPDATE', 'GUILD_MEMBER_REMOVE', 'THREAD_MEMBERS_UPDATE *'],
|
||||||
"GUILD_MEMBER_UPDATE",
|
1,
|
||||||
"GUILD_MEMBER_REMOVE",
|
],
|
||||||
"THREAD_MEMBERS_UPDATE *",
|
|
||||||
], 1],
|
|
||||||
|
|
||||||
[Intent.GUILD_BANS]: [[
|
[Intent.GUILD_BANS]: [['GUILD_BAN_ADD', 'GUILD_BAN_REMOVE'], 2],
|
||||||
"GUILD_BAN_ADD",
|
|
||||||
"GUILD_BAN_REMOVE",
|
|
||||||
], 2],
|
|
||||||
|
|
||||||
[Intent.GUILD_EMOJIS_AND_STICKERS]: [[
|
[Intent.GUILD_EMOJIS_AND_STICKERS]: [['GUILD_EMOJIS_UPDATE', 'GUILD_STICKERS_UPDATE'], 3],
|
||||||
"GUILD_EMOJIS_UPDATE",
|
|
||||||
"GUILD_STICKERS_UPDATE",
|
|
||||||
], 3],
|
|
||||||
|
|
||||||
[Intent.GUILD_INTEGRATIONS]: [[
|
[Intent.GUILD_INTEGRATIONS]: [
|
||||||
"GUILD_INTEGRATIONS_UPDATE",
|
['GUILD_INTEGRATIONS_UPDATE', 'INTEGRATION_CREATE', 'INTEGRATION_UPDATE', 'INTEGRATION_DELETE'],
|
||||||
"INTEGRATION_CREATE",
|
4,
|
||||||
"INTEGRATION_UPDATE",
|
],
|
||||||
"INTEGRATION_DELETE",
|
|
||||||
], 4],
|
|
||||||
|
|
||||||
[Intent.GUILD_WEBHOOKS]: [[
|
[Intent.GUILD_WEBHOOKS]: [['WEBHOOKS_UPDATE'], 5],
|
||||||
"WEBHOOKS_UPDATE"
|
|
||||||
], 5],
|
|
||||||
|
|
||||||
[Intent.GUILD_INVITES]: [[
|
[Intent.GUILD_INVITES]: [['INVITE_CREATE', 'INVITE_DELETE'], 6],
|
||||||
"INVITE_CREATE",
|
|
||||||
"INVITE_DELETE",
|
|
||||||
], 6],
|
|
||||||
|
|
||||||
[Intent.GUILD_VOICE_STATES]: [[
|
[Intent.GUILD_VOICE_STATES]: [['VOICE_STATE_UPDATE'], 7],
|
||||||
"VOICE_STATE_UPDATE"
|
|
||||||
], 7],
|
|
||||||
|
|
||||||
[Intent.GUILD_PRESENCES]: [[
|
[Intent.GUILD_PRESENCES]: [['PRESENCE_UPDATE'], 8],
|
||||||
"PRESENCE_UPDATE"
|
|
||||||
], 8],
|
|
||||||
|
|
||||||
[Intent.GUILD_MESSAGES]: [[
|
[Intent.GUILD_MESSAGES]: [['MESSAGE_CREATE', 'MESSAGE_UPDATE', 'MESSAGE_DELETE', 'MESSAGE_DELETE_BULK'], 9],
|
||||||
"MESSAGE_CREATE",
|
|
||||||
"MESSAGE_UPDATE",
|
|
||||||
"MESSAGE_DELETE",
|
|
||||||
"MESSAGE_DELETE_BULK",
|
|
||||||
], 9],
|
|
||||||
|
|
||||||
[Intent.GUILD_MESSAGE_REACTIONS]: [[
|
[Intent.GUILD_MESSAGE_REACTIONS]: [
|
||||||
"MESSAGE_REACTION_ADD",
|
[
|
||||||
"MESSAGE_REACTION_REMOVE",
|
'MESSAGE_REACTION_ADD',
|
||||||
"MESSAGE_REACTION_REMOVE_ALL",
|
'MESSAGE_REACTION_REMOVE',
|
||||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
'MESSAGE_REACTION_REMOVE_ALL',
|
||||||
], 10],
|
'MESSAGE_REACTION_REMOVE_EMOJI',
|
||||||
|
],
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
|
||||||
[Intent.GUILD_MESSAGE_TYPING]: [[
|
[Intent.GUILD_MESSAGE_TYPING]: [['TYPING_START'], 11],
|
||||||
"TYPING_START"
|
|
||||||
], 11],
|
|
||||||
|
|
||||||
[Intent.DIRECT_MESSAGES]: [[
|
[Intent.DIRECT_MESSAGES]: [['MESSAGE_CREATE', 'MESSAGE_UPDATE', 'MESSAGE_DELETE', 'CHANNEL_PINS_UPDATE'], 12],
|
||||||
"MESSAGE_CREATE",
|
|
||||||
"MESSAGE_UPDATE",
|
|
||||||
"MESSAGE_DELETE",
|
|
||||||
"CHANNEL_PINS_UPDATE",
|
|
||||||
], 12],
|
|
||||||
|
|
||||||
[Intent.DIRECT_MESSAGE_REACTIONS]: [[
|
[Intent.DIRECT_MESSAGE_REACTIONS]: [
|
||||||
"MESSAGE_REACTION_ADD",
|
[
|
||||||
"MESSAGE_REACTION_REMOVE",
|
'MESSAGE_REACTION_ADD',
|
||||||
"MESSAGE_REACTION_REMOVE_ALL",
|
'MESSAGE_REACTION_REMOVE',
|
||||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
'MESSAGE_REACTION_REMOVE_ALL',
|
||||||
], 13],
|
'MESSAGE_REACTION_REMOVE_EMOJI',
|
||||||
|
],
|
||||||
|
13,
|
||||||
|
],
|
||||||
|
|
||||||
[Intent.DIRECT_MESSAGE_TYPING]: [[
|
[Intent.DIRECT_MESSAGE_TYPING]: [['TYPING_START'], 14],
|
||||||
"TYPING_START"
|
|
||||||
], 14],
|
|
||||||
|
|
||||||
[Intent.MESSAGE_CONTENT]: [[
|
[Intent.MESSAGE_CONTENT]: [['MESSAGE_CONTENT'], 15],
|
||||||
"MESSAGE_CONTENT"
|
};
|
||||||
], 15]
|
|
||||||
|
export type DiscordAPIEvent =
|
||||||
|
| 'READY'
|
||||||
|
| 'RESUMED'
|
||||||
|
| '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'
|
||||||
|
| 'GUILD_MEMBER_ADD'
|
||||||
|
| 'GUILD_MEMBER_UPDATE'
|
||||||
|
| 'GUILD_MEMBER_REMOVE'
|
||||||
|
| 'GUILD_MEMBERS_CHUNK *'
|
||||||
|
| 'GUILD_BAN_ADD'
|
||||||
|
| 'GUILD_BAN_REMOVE'
|
||||||
|
| 'GUILD_EMOJIS_UPDATE'
|
||||||
|
| 'GUILD_STICKERS_UPDATE'
|
||||||
|
| 'GUILD_INTEGRATIONS_UPDATE'
|
||||||
|
| 'INTEGRATION_CREATE'
|
||||||
|
| 'INTEGRATION_UPDATE'
|
||||||
|
| 'INTEGRATION_DELETE'
|
||||||
|
| 'WEBHOOKS_UPDATE'
|
||||||
|
| 'INVITE_CREATE'
|
||||||
|
| 'INVITE_DELETE'
|
||||||
|
| 'VOICE_STATE_UPDATE'
|
||||||
|
| 'MESSAGE_CREATE'
|
||||||
|
| 'MESSAGE_UPDATE'
|
||||||
|
| 'MESSAGE_DELETE'
|
||||||
|
| 'MESSAGE_DELETE_BULK'
|
||||||
|
| 'MESSAGE_REACTION_ADD'
|
||||||
|
| 'MESSAGE_REACTION_REMOVE'
|
||||||
|
| 'MESSAGE_REACTION_REMOVE_ALL'
|
||||||
|
| 'MESSAGE_REACTION_REMOVE_EMOJI'
|
||||||
|
| 'TYPING_START'
|
||||||
|
| 'PRESENCE_UPDATE'
|
||||||
|
| 'APPLICATION_COMMAND_CREATE'
|
||||||
|
| 'APPLICATION_COMMAND_UPDATE'
|
||||||
|
| 'APPLICATION_COMMAND_DELETE';
|
||||||
|
|
||||||
|
export enum DiscordAPIEvents {
|
||||||
|
READY = 'READY',
|
||||||
|
RESUMED = 'RESUMED',
|
||||||
|
CHANNEL_CREATE = 'CHANNEL_CREATE',
|
||||||
|
CHANNEL_UPDATE = 'CHANNEL_UPDATE',
|
||||||
|
CHANNEL_DELETE = 'CHANNEL_DELETE',
|
||||||
|
CHANNEL_PINS_UPDATE = 'CHANNEL_PINS_UPDATE',
|
||||||
|
THREAD_CREATE = 'THREAD_CREATE',
|
||||||
|
THREAD_UPDATE = 'THREAD_UPDATE',
|
||||||
|
THREAD_DELETE = 'THREAD_DELETE',
|
||||||
|
THREAD_LIST_SYNC = 'THREAD_LIST_SYNC',
|
||||||
|
THREAD_MEMBER_UPDATE = 'THREAD_MEMBER_UPDATE',
|
||||||
|
THREAD_MEMBERS_UPDATE = 'THREAD_MEMBERS_UPDATE *',
|
||||||
|
STAGE_INSTANCE_CREATE = 'STAGE_INSTANCE_CREATE',
|
||||||
|
STAGE_INSTANCE_UPDATE = 'STAGE_INSTANCE_UPDATE',
|
||||||
|
STAGE_INSTANCE_DELETE = 'STAGE_INSTANCE_DELETE',
|
||||||
|
GUILD_CREATE = 'GUILD_CREATE',
|
||||||
|
GUILD_UPDATE = 'GUILD_UPDATE',
|
||||||
|
GUILD_DELETE = 'GUILD_DELETE',
|
||||||
|
GUILD_ROLE_CREATE = 'GUILD_ROLE_CREATE',
|
||||||
|
GUILD_ROLE_UPDATE = 'GUILD_ROLE_UPDATE',
|
||||||
|
GUILD_ROLE_DELETE = 'GUILD_ROLE_DELETE',
|
||||||
|
GUILD_MEMBER_ADD = 'GUILD_MEMBER_ADD',
|
||||||
|
GUILD_MEMBER_UPDATE = 'GUILD_MEMBER_UPDATE',
|
||||||
|
GUILD_MEMBER_REMOVE = 'GUILD_MEMBER_REMOVE',
|
||||||
|
GUILD_MEMBERS_CHUNK = 'GUILD_MEMBERS_CHUNK *',
|
||||||
|
GUILD_BAN_ADD = 'GUILD_BAN_ADD',
|
||||||
|
GUILD_BAN_REMOVE = 'GUILD_BAN_REMOVE',
|
||||||
|
GUILD_EMOJIS_UPDATE = 'GUILD_EMOJIS_UPDATE',
|
||||||
|
GUILD_STICKERS_UPDATE = 'GUILD_STICKERS_UPDATE',
|
||||||
|
GUILD_INTEGRATIONS_UPDATE = 'GUILD_INTEGRATIONS_UPDATE',
|
||||||
|
INTEGRATION_CREATE = 'INTEGRATION_CREATE',
|
||||||
|
INTEGRATION_UPDATE = 'INTEGRATION_UPDATE',
|
||||||
|
INTEGRATION_DELETE = 'INTEGRATION_DELETE',
|
||||||
|
WEBHOOKS_UPDATE = 'WEBHOOKS_UPDATE',
|
||||||
|
INVITE_CREATE = 'INVITE_CREATE',
|
||||||
|
INVITE_DELETE = 'INVITE_DELETE',
|
||||||
|
VOICE_STATE_UPDATE = 'VOICE_STATE_UPDATE',
|
||||||
|
MESSAGE_CREATE = 'MESSAGE_CREATE',
|
||||||
|
MESSAGE_UPDATE = 'MESSAGE_UPDATE',
|
||||||
|
MESSAGE_DELETE = 'MESSAGE_DELETE',
|
||||||
|
MESSAGE_DELETE_BULK = 'MESSAGE_DELETE_BULK',
|
||||||
|
MESSAGE_REACTION_ADD = 'MESSAGE_REACTION_ADD',
|
||||||
|
MESSAGE_REACTION_REMOVE = 'MESSAGE_REACTION_REMOVE',
|
||||||
|
MESSAGE_REACTION_REMOVE_ALL = 'MESSAGE_REACTION_REMOVE_ALL',
|
||||||
|
MESSAGE_REACTION_REMOVE_EMOJI = 'MESSAGE_REACTION_REMOVE_EMOJI',
|
||||||
|
TYPING_START = 'TYPING_START',
|
||||||
|
PRESENCE_UPDATE = 'PRESENCE_UPDATE',
|
||||||
|
APPLICATION_COMMAND_CREATE = 'APPLICATION_COMMAND_CREATE',
|
||||||
|
APPLICATION_COMMAND_UPDATE = 'APPLICATION_COMMAND_UPDATE',
|
||||||
|
APPLICATION_COMMAND_DELETE = 'APPLICATION_COMMAND_DELETE',
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
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 { Channel } from "./channel";
|
||||||
|
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;
|
||||||
|
channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
||||||
|
};
|
||||||
|
|
||||||
|
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, 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 {
|
// TODO: Flesh out InteractionPayload type
|
||||||
abstract events: string | string[];
|
export type InteractionPayload = {
|
||||||
abstract execute: HandlerFunction;
|
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,283 @@
|
|||||||
|
import { exists, isNil } from '../utils';
|
||||||
|
import { GatewayObject } from './base';
|
||||||
|
import type { Channel } from './channel';
|
||||||
|
import { fromDiscord, Trait, type Expectation } from './common';
|
||||||
|
import type { Embed } from './embed';
|
||||||
|
import type { Guild } from './guild';
|
||||||
|
import { User, type DiscordUser } 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 DefaultReaction = {
|
||||||
|
emoji_id?: string | null; // the id of a guild's custom emoji
|
||||||
|
emoji_name?: string | null; // the unicode character of the emoji
|
||||||
|
};
|
||||||
|
|
||||||
|
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,241 @@
|
|||||||
|
import { ApplicationCommandType, ChannelType, 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,
|
||||||
|
Intent.DIRECT_MESSAGES,
|
||||||
|
Intent.DIRECT_MESSAGE_REACTIONS
|
||||||
|
],
|
||||||
|
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 { channel } = await context.hydrate(message, [Expectation.Channel]);
|
||||||
|
|
||||||
|
const hydrate = await context.hydrator(context, [Expectation.Guild]);
|
||||||
|
const hasGuild = hydrate(context);
|
||||||
|
|
||||||
|
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 ${context.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 in ${channel.type === ChannelType.DM ? 'our DM\'s 😘' : 'a Group 👀'}`, 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();
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Here we are defining what we want the interface to look like so that there is a goal to work towards
|
||||||
|
// Things here are not necessarily implemented in the actual code but show what we want it to look like to use the library
|
||||||
|
|
||||||
|
import { Expectation, Unicord, UnicordArgumentType } from '../rewrite/index.ts';
|
||||||
|
|
||||||
|
const DISCORD_APPLICATION_ID = process.env.APPLICATION_ID!;
|
||||||
|
const DISCORD_CLIENT_TOKEN = process.env.BOT_TOKEN!;
|
||||||
|
|
||||||
|
// Create a new client instance
|
||||||
|
// Most usage will be via chaining methods off the client instance
|
||||||
|
const client = new Unicord({
|
||||||
|
application_id: DISCORD_APPLICATION_ID,
|
||||||
|
token: DISCORD_CLIENT_TOKEN,
|
||||||
|
})
|
||||||
|
.registerChatCommand(
|
||||||
|
'ping',
|
||||||
|
async (context, args) => {
|
||||||
|
let response = 'Pong!';
|
||||||
|
response += ` Hello, ${args.name}!`;
|
||||||
|
if (!!args.age && args.age > 0) {
|
||||||
|
response += ` Happy to hear you are ${args.age} years old.`;
|
||||||
|
}
|
||||||
|
await context.reply(response);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Say hello',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
description: 'Your name',
|
||||||
|
// Maybe we can use t.String() here but only certain types are valid for command options
|
||||||
|
// But in theory we could parse the raw value and attempt convert it to requested type here
|
||||||
|
type: UnicordArgumentType.String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'age',
|
||||||
|
description: 'Your age',
|
||||||
|
type: UnicordArgumentType.Number,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.registerChatCommand(
|
||||||
|
'channel',
|
||||||
|
async (context) => {
|
||||||
|
const hydrated = await context.hydrate(context, [Expectation.Channel]);
|
||||||
|
await context.reply(hydrated.channel.name ?? 'Unnamed Channel');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Get info about a channel',
|
||||||
|
args: [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.registerApplicationCommand(
|
||||||
|
'echo',
|
||||||
|
async (context, args) => {
|
||||||
|
await context.reply('Echo! ' + args.text);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Echo command',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'text',
|
||||||
|
description: 'Text to echo',
|
||||||
|
type: UnicordArgumentType.String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.onEvent('READY', async (payload) => {
|
||||||
|
console.log(`Logged in as ${payload.user.username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow use of "child" instances inside parent instance
|
||||||
|
const etc = new Unicord().registerChatCommand(
|
||||||
|
'zing',
|
||||||
|
async (context, args) => {
|
||||||
|
await context.reply(`Zing! You got served!` + args.fav_amimal ? ` One ${args.fav_amimal} please!` : '');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'fav_amimal',
|
||||||
|
description: 'Your favorite animal',
|
||||||
|
type: UnicordArgumentType.String,
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ name: 'Dog', value: 'dog' },
|
||||||
|
{ name: 'Cat', value: 'cat' },
|
||||||
|
{ name: 'Hamster', value: 'hamster' },
|
||||||
|
{ name: 'Dolphin', value: 'dolphin' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
client.use(etc);
|
||||||
|
|
||||||
|
client.login();
|
||||||
+28
-7
@@ -1,11 +1,32 @@
|
|||||||
{
|
{
|
||||||
|
"files": ["src/index.ts"],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
// Enable latest features
|
||||||
"target": "es2015",
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "./dist"
|
"outDir": "out",
|
||||||
},
|
|
||||||
"include": [
|
// Bundler mode
|
||||||
"src/**/*"
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "unicord",
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "quotes-bot",
|
||||||
|
"path": "../quotes-bot"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
output: 'export'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+2057
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "unicord-website",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||||
|
"next": "15.1.7",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-syntax-highlighter": "^15.4.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,31 @@
|
|||||||
|
:root {
|
||||||
|
--background: #252525;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
max-width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--background);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1012 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Space_Grotesk, Space_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const spaceGrotesk = Space_Grotesk({
|
||||||
|
variable: "--font-space-grotesk",
|
||||||
|
subsets: ["latin"]
|
||||||
|
});
|
||||||
|
|
||||||
|
const spaceMono = Space_Mono({
|
||||||
|
variable: "--font-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400"]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Unicord',
|
||||||
|
description: 'Unicord is a simple, type-safe, and modular framework for building Discord bots in TypeScript.',
|
||||||
|
openGraph: {
|
||||||
|
title: 'Unicord',
|
||||||
|
description: 'Unicord is a simple, type-safe, and modular framework for building Discord bots in TypeScript.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://unicord.hat.fish/unicord.png',
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
alt: 'Unicord logo'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={`${spaceGrotesk.variable} ${spaceMono.variable}`}>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
.page {
|
||||||
|
--gray-rgb: 255, 255, 255;
|
||||||
|
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||||
|
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||||
|
|
||||||
|
--button-primary-hover: #ccc;
|
||||||
|
--button-secondary-hover: #1a1a1a;
|
||||||
|
|
||||||
|
font-family: var(--font-space-grotesk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* page with header needs margin atop */
|
||||||
|
.pageWithHeader {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.7rem;
|
||||||
|
/* glassy background */
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border-bottom: 1px solid var(--gray-alpha-200);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLink {
|
||||||
|
color: var(--color-text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLink:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoMark {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0px;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoMarkSpan {
|
||||||
|
color: #ff0080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* padding: 4rem 0em; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dotPattern {
|
||||||
|
0% {
|
||||||
|
background-position: top right;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: bottom left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: 0 5em 0;
|
||||||
|
height: calc(100vh - 4rem);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 2rem;
|
||||||
|
border-bottom: 1px solid var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-image: radial-gradient(circle, #ff0080 1px, transparent 1px);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
animation: dotPattern 30s linear infinite;
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 50%;
|
||||||
|
background: linear-gradient(to bottom, transparent, var(--gray-alpha-100));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeExample {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 60%;
|
||||||
|
height: 75%;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock {
|
||||||
|
background: var(--gray-alpha-100);
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code::before {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code::after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code:hover {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code:active {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code:focus {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code:focus-visible {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlock code:focus-within {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillars {
|
||||||
|
padding: 6rem 12em;
|
||||||
|
background: var(--gray-alpha-100);
|
||||||
|
min-height: 50vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillarContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 4rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--gray-alpha-100);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--gray-alpha-200);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillarIcon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar h2 {
|
||||||
|
color: #ff0080;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar p {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationSection {
|
||||||
|
padding: 6rem 12em;
|
||||||
|
min-height: 50vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: space;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 4rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationCode {
|
||||||
|
flex: 2;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 780px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationText {
|
||||||
|
flex: 1;
|
||||||
|
/* padding: 2rem; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationText h2 {
|
||||||
|
color: #ff0080;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationText p {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeSection {
|
||||||
|
padding: 6rem 12em;
|
||||||
|
min-height: 50vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: space;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
background: var(--gray-alpha-100);
|
||||||
|
border-top: 1px solid var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 4rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeText {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCode {
|
||||||
|
flex: 2;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 780px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeText h2 {
|
||||||
|
color: #ff0080;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeText p {
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeText p code, .hydrationText p code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85em;
|
||||||
|
background-color: var(--gray-alpha-100);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
padding: 0.1em 0.2em;
|
||||||
|
color: var(--foreground);
|
||||||
|
border: 1px solid var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.installSection {
|
||||||
|
padding: 6rem 12em;
|
||||||
|
min-height: 30vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.installContainer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installContainer h2 {
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 0 2rem 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packageManagerTabs {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--gray-alpha-200);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton:hover {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeTab {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
border-color: #ff0080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installCommandWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--gray-alpha-100);
|
||||||
|
border: 1px solid var(--gray-alpha-200);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installCommand {
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--gray-alpha-200);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyButton:hover {
|
||||||
|
background: var(--gray-alpha-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.hero {
|
||||||
|
padding: 0 2rem;
|
||||||
|
height: calc(100vh - 4rem);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .codeExample, .hero::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillars {
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillarContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pillar {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationSection {
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationContainer {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationCode {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hydrationText {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeSection {
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeCode {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeText {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installSection {
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.packageManagerTabs {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
gap: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navLink {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styles from "./page.module.css";
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const CodeExample = dynamic(
|
||||||
|
() => import('../components/CodeExample')
|
||||||
|
);
|
||||||
|
|
||||||
|
import { simpleExample, hydrationExample, typeCoercionExample } from '../components/CodeExample';
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [activeTab, setActiveTab] = useState('npm');
|
||||||
|
const [windowWidth, setWindowWidth] = useState(1200);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWindowWidth(window.innerWidth);
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowWidth(window.innerWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[styles.page, styles.pageWithHeader].join(" ")}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<div className={styles.logoMark}>
|
||||||
|
<img src="/unicord.png" alt="Unicord logo"></img>
|
||||||
|
<div><span className={styles.logoMarkSpan}>uni</span>cord</div>
|
||||||
|
</div>
|
||||||
|
<nav className={styles.nav}>
|
||||||
|
{/* <a href="/docs" className={styles.navLink}>Docs</a> */}
|
||||||
|
<a href="https://github.com/jaronpate/unicord" className={styles.navLink}>Source</a>
|
||||||
|
<a href="https://discord.gg/6kFfqa5bde" target='_blank' className={styles.navLink}>Discord</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.hero}>
|
||||||
|
<div>
|
||||||
|
<h1 className={styles.title}>
|
||||||
|
Welcome to <span className={styles.logoMarkSpan}>uni</span>cord
|
||||||
|
</h1>
|
||||||
|
<p className={styles.description}>
|
||||||
|
Build good bots fast.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.codeExample}>
|
||||||
|
<CodeExample code={simpleExample} noScroll={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.pillars}>
|
||||||
|
<div className={styles.pillarContainer}>
|
||||||
|
<div className={styles.pillar}>
|
||||||
|
<div className={styles.pillarIcon}>⚡</div>
|
||||||
|
<h2>Simple</h2>
|
||||||
|
<p>Build Discord bots with minimal boilerplate and intuitive APIs</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pillar}>
|
||||||
|
<div className={styles.pillarIcon}>🔧</div>
|
||||||
|
<h2>Maintainable</h2>
|
||||||
|
<p>Type-safe, modular architecture that scales with your project</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.pillar}>
|
||||||
|
<div className={styles.pillarIcon}>📈</div>
|
||||||
|
<h2>Scalable</h2>
|
||||||
|
<p>Efficient caching and event handling for bots of any size</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.hydrationSection}>
|
||||||
|
<div className={styles.hydrationContainer}>
|
||||||
|
<div className={styles.hydrationCode}>
|
||||||
|
<CodeExample code={hydrationExample} height={260} noScroll={windowWidth < 1200 ? false : true} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.hydrationText}>
|
||||||
|
<h2>Smart Hydration</h2>
|
||||||
|
<p>
|
||||||
|
Unicord's hydration system automatically manages Discord object lifecycles.
|
||||||
|
The <code>context.hydrate</code> method intelligently fetches and caches Discord objects
|
||||||
|
like messages, users, and guilds, ensuring your bot stays efficient while
|
||||||
|
keeping your code clean and predictable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.typeSection}>
|
||||||
|
<div className={styles.typeContainer}>
|
||||||
|
<div className={styles.typeText}>
|
||||||
|
<h2>Type Coercion</h2>
|
||||||
|
<p>
|
||||||
|
Unicord automatically handles type coercion for command arguments,
|
||||||
|
ensuring you always get the correct types in your handlers. Define your
|
||||||
|
command structure once, and get full type safety and autocompletion
|
||||||
|
throughout your codebase.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In this example, the <code>user</code> argument is automatically typed
|
||||||
|
as a <code>User</code> object, providing access to all user properties
|
||||||
|
with full TypeScript support.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.typeCode}>
|
||||||
|
<CodeExample code={typeCoercionExample} height={370} noScroll={windowWidth < 1200 ? false : true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.installSection}>
|
||||||
|
<div className={styles.installContainer}>
|
||||||
|
<h2>Install <span className={styles.logoMarkSpan}>uni</span>cord</h2>
|
||||||
|
<div className={styles.packageManagerTabs}>
|
||||||
|
<button
|
||||||
|
className={`${styles.tabButton} ${activeTab === 'npm' ? styles.activeTab : ''}`}
|
||||||
|
onClick={() => setActiveTab('npm')}
|
||||||
|
>
|
||||||
|
npm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.tabButton} ${activeTab === 'yarn' ? styles.activeTab : ''}`}
|
||||||
|
onClick={() => setActiveTab('yarn')}
|
||||||
|
>
|
||||||
|
yarn
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.tabButton} ${activeTab === 'pnpm' ? styles.activeTab : ''}`}
|
||||||
|
onClick={() => setActiveTab('pnpm')}
|
||||||
|
>
|
||||||
|
pnpm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.tabButton} ${activeTab === 'bun' ? styles.activeTab : ''}`}
|
||||||
|
onClick={() => setActiveTab('bun')}
|
||||||
|
>
|
||||||
|
bun
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.installCommandWrapper}>
|
||||||
|
<code className={styles.installCommand}>
|
||||||
|
{activeTab === 'npm' && 'npm install unicord'}
|
||||||
|
{activeTab === 'yarn' && 'yarn add unicord'}
|
||||||
|
{activeTab === 'pnpm' && 'pnpm add unicord'}
|
||||||
|
{activeTab === 'bun' && 'bun add unicord'}
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
className={styles.copyButton}
|
||||||
|
onClick={() => {
|
||||||
|
const commands = {
|
||||||
|
npm: 'npm install unicord',
|
||||||
|
yarn: 'yarn add unicord',
|
||||||
|
pnpm: 'pnpm add unicord',
|
||||||
|
bun: 'bun add unicord'
|
||||||
|
};
|
||||||
|
navigator.clipboard.writeText(commands[activeTab as keyof typeof commands]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.editorWrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #2d2d2d;
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Editor, { Monaco } from "@monaco-editor/react";
|
||||||
|
import styles from './CodeExample.module.css';
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type CodeExampleProps = {
|
||||||
|
code: string;
|
||||||
|
height?: string | number;
|
||||||
|
noScroll?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function CodeExample({ code, height = "100%", noScroll = false }: CodeExampleProps) {
|
||||||
|
// const [unicordLib, setUnicordLib] = useState("");
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// setUnicordLib(`
|
||||||
|
// declare module 'unicord' {
|
||||||
|
// export enum Intent {
|
||||||
|
// GUILDS = 1 << 0,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `);
|
||||||
|
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// const handleEditorDidMount = (monaco: Monaco) => {
|
||||||
|
// monaco.languages.typescript.typescriptDefaults.addExtraLib(unicordLib);
|
||||||
|
|
||||||
|
// monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||||
|
// noSemanticValidation: false,
|
||||||
|
// noSyntaxValidation: false,
|
||||||
|
// noSuggestionDiagnostics: false,
|
||||||
|
// });
|
||||||
|
// monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||||
|
// target: monaco.languages.typescript.ScriptTarget.ESNext,
|
||||||
|
// module: monaco.languages.typescript.ModuleKind.ESNext,
|
||||||
|
// allowJs: true,
|
||||||
|
// checkJs: true,
|
||||||
|
// allowNonTsExtensions: true,
|
||||||
|
// strict: true,
|
||||||
|
// noEmit: true,
|
||||||
|
// lib: [
|
||||||
|
// "ESNext",
|
||||||
|
// "DOM",
|
||||||
|
// "DOM.Iterable",
|
||||||
|
// "ESNext.AsyncIterable",
|
||||||
|
// "ESNext.BigInt"
|
||||||
|
// ],
|
||||||
|
// types: ["node", "unicord"],
|
||||||
|
// baseUrl: "./",
|
||||||
|
// paths: {
|
||||||
|
// "index": ["./src/index.ts"],
|
||||||
|
// "unicord": ["./node_modules/unicord/dist/index.d.ts"]
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// };
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// fetch("https://www.unpkg.com/unicord@0.0.4/out/types/common.d.ts")
|
||||||
|
// .then((response) => response.text())
|
||||||
|
// .then((text) => {
|
||||||
|
// setUnicordLib(text);
|
||||||
|
// console.log("Fetched extra lib:", text);
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error("Error fetching extra lib:", error);
|
||||||
|
// });
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.editorWrapper}>
|
||||||
|
<div className={styles.editorContainer}>
|
||||||
|
<Editor
|
||||||
|
height={height}
|
||||||
|
defaultLanguage="typescript"
|
||||||
|
defaultValue={code}
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
roundedSelection: false,
|
||||||
|
padding: { top: 16, bottom: 16 },
|
||||||
|
automaticLayout: true,
|
||||||
|
scrollbar: {
|
||||||
|
alwaysConsumeMouseWheel: !noScroll,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
// beforeMount={handleEditorDidMount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the example code snippets for reuse
|
||||||
|
export const simpleExample = `\
|
||||||
|
import { Client, type Context, Intent } from 'unicord';
|
||||||
|
|
||||||
|
// Initialize and configure
|
||||||
|
const client = new Client({
|
||||||
|
token: '<token>',
|
||||||
|
application_id: '<application_id>',
|
||||||
|
intents: [
|
||||||
|
Intent.GUILDS,
|
||||||
|
Intent.GUILD_MESSAGES,
|
||||||
|
Intent.GUILD_MESSAGE_REACTIONS,
|
||||||
|
Intent.MESSAGE_CONTENT
|
||||||
|
],
|
||||||
|
prefix: '??'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register a command
|
||||||
|
client.chatCommands.register('ping', (context: Context, args) => {
|
||||||
|
context.reply('Pong!', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
client.connect();`;
|
||||||
|
|
||||||
|
export const hydrationExample = `\
|
||||||
|
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 = Date.now() - message.timestamp.getTime();
|
||||||
|
// Edit the message to include the time
|
||||||
|
await context.editMessage(response, \`Pong! Latency: \${time}ms\`);
|
||||||
|
});`;
|
||||||
|
|
||||||
|
export const typeCoercionExample = `\
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});`;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
:root {
|
||||||
|
--color-primary: #ff0080;
|
||||||
|
--color-background: #000;
|
||||||
|
--color-text: #fff;
|
||||||
|
|
||||||
|
--gray-rgb: 255, 255, 255;
|
||||||
|
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
|
||||||
|
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
|
||||||
|
|
||||||
|
--spacing-xs: 0.5rem;
|
||||||
|
--spacing-sm: 1rem;
|
||||||
|
--spacing-md: 2rem;
|
||||||
|
--spacing-lg: 4rem;
|
||||||
|
--spacing-xl: 6rem;
|
||||||
|
|
||||||
|
--border-radius-sm: 6px;
|
||||||
|
--border-radius-md: 8px;
|
||||||
|
--border-radius-lg: 12px;
|
||||||
|
|
||||||
|
--font-size-sm: 0.9rem;
|
||||||
|
--font-size-md: 1.1rem;
|
||||||
|
--font-size-lg: 1.5rem;
|
||||||
|
--font-size-xl: 2rem;
|
||||||
|
|
||||||
|
--section-padding-desktop: 6rem 12em;
|
||||||
|
--section-padding-mobile: 4rem 2rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -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