mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
Compare commits
78 Commits
v0.0.1
...
f8318e6399
| Author | SHA1 | Date | |
|---|---|---|---|
| f8318e6399 | |||
| ce45eacb3b | |||
| 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 |
@@ -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 }}
|
||||||
|
|||||||
@@ -173,3 +173,4 @@ dist
|
|||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.aider*
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
src/services/processor.ts
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="./assets/unicord.png" alt="unicord" width="200"/>
|
<img src="https://raw.githubusercontent.com/jaronpate/unicord/master/assets/unicord.png" width="150" height="150">
|
||||||
<h2>unicord</h2>
|
<h2>unicord</h2>
|
||||||
<p><em>Build good bots fast</em></p>
|
<p><em>Build good bots fast</em></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,3 +181,46 @@ client.interactions.register('button_1', buttonInteractionHandler);
|
|||||||
client.interactions.register('button_2', buttonInteractionHandler);
|
client.interactions.register('button_2', buttonInteractionHandler);
|
||||||
client.interactions.register('button_3', 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.
|
Before Width: | Height: | Size: 1012 KiB After Width: | Height: | Size: 1.7 MiB |
+10
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unicord",
|
"name": "unicord",
|
||||||
"description": "A simple discord library primarily for TypeScript. REST like and easy to use.",
|
"description": "A simple discord library primarily for TypeScript. Web server like and easy to use.",
|
||||||
"version": "0.0.1",
|
"version": "0.0.4",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "out/index.js",
|
"main": "out/index.js",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"out"
|
"out"
|
||||||
],
|
],
|
||||||
|
"repository": "github:jaronpate/unicord",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun run testing/demo.test.ts",
|
"test": "bun run testing/demo.test.ts",
|
||||||
"test:inspect": "bun --inspect testing/demo.test.ts",
|
"test:inspect": "bun --inspect testing/demo.test.ts",
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
"build:packages": "bun run build:types && bun run build:core -- --packages external",
|
"build:packages": "bun run build:types && bun run build:core -- --packages external",
|
||||||
"build:nocheck": "bun run build:types:nocheck && bun run build:core",
|
"build:nocheck": "bun run build:types:nocheck && bun run build:core",
|
||||||
|
|
||||||
"build:core": "bun build src/index.ts --outdir out --drop=console --drop=debugger",
|
"build:core": "bun build src/index.ts --outdir out --drop=debugger",
|
||||||
"build:types": "tsc --emitDeclarationOnly --project tsconfig.json",
|
"build:types": "tsc --emitDeclarationOnly --project tsconfig.json",
|
||||||
"build:types:nocheck": "bun run build:types -- --noCheck",
|
"build:types:nocheck": "bun run build:types -- --noCheck",
|
||||||
|
|
||||||
@@ -34,5 +35,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"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.onSystemEvent(event, async (payload) => {
|
||||||
|
this.cache.set(payload.id, factory[Trait.fromDiscord](payload));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,230 @@
|
|||||||
|
import type {
|
||||||
|
ArgumentTypeFromOptions,
|
||||||
|
Unicord,
|
||||||
|
UnicordArgumentDefinition,
|
||||||
|
UnicordCommandHandler,
|
||||||
|
UnicordCommandOptions,
|
||||||
|
} from '.';
|
||||||
|
import { UnicordCommandContext } from './context';
|
||||||
|
import type { UnicordEventProcessor } from './eventProcessor';
|
||||||
|
import { Embed } from './types';
|
||||||
|
import { UnicordArgumentType, UnicordEventType } from './types/common';
|
||||||
|
|
||||||
|
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']>> {
|
||||||
|
const validated: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (args.length > definitions.length) {
|
||||||
|
throw new Error(`Too many arguments. Expected ${definitions.length}, received ${args.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, def] of definitions.entries()) {
|
||||||
|
const arg = args[index];
|
||||||
|
|
||||||
|
if (arg === undefined || arg === null || arg === '') {
|
||||||
|
if (def.required) {
|
||||||
|
throw new Error(`Missing required argument: ${def.name}`);
|
||||||
|
}
|
||||||
|
validated[def.name] = undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validated[def.name] = this.coerceArgument(def, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(validated as ArgumentTypeFromOptions<Options['args']>);
|
||||||
|
}
|
||||||
|
|
||||||
|
private coerceArgument(def: UnicordArgumentDefinition, value: any) {
|
||||||
|
let coerced = value;
|
||||||
|
|
||||||
|
if (def.type === UnicordArgumentType.Integer) {
|
||||||
|
coerced = typeof value === 'number' ? value : parseInt(value, 10);
|
||||||
|
if (!Number.isInteger(coerced)) {
|
||||||
|
throw new Error(`Argument ${def.name} must be an integer`);
|
||||||
|
}
|
||||||
|
} else if (def.type === UnicordArgumentType.Number) {
|
||||||
|
coerced = typeof value === 'number' ? value : Number(value);
|
||||||
|
if (Number.isNaN(coerced)) {
|
||||||
|
throw new Error(`Argument ${def.name} must be a number`);
|
||||||
|
}
|
||||||
|
} else if (def.type === UnicordArgumentType.Boolean) {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
coerced = value;
|
||||||
|
} else if (value === 'true') {
|
||||||
|
coerced = true;
|
||||||
|
} else if (value === 'false') {
|
||||||
|
coerced = false;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Argument ${def.name} must be a boolean`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.choices && !def.choices.some((choice) => choice.value === coerced)) {
|
||||||
|
throw new Error(`Invalid choice for argument ${def.name}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coerced;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
register<const Options extends UnicordCommandOptions>(
|
||||||
|
type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands,
|
||||||
|
event: string,
|
||||||
|
handler: UnicordCommandHandler<Options>,
|
||||||
|
options: Options = { args: [] } as unknown as Options,
|
||||||
|
) {
|
||||||
|
if (type === UnicordEventType.ApplicationCommands) {
|
||||||
|
const commandNameRegex = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
|
||||||
|
if (!commandNameRegex.test(event)) {
|
||||||
|
throw new Error(
|
||||||
|
`${event} is not a valid application command name (1-32 characters, only letters, numbers, hyphens, and underscores)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.description || options.description.length === 0) {
|
||||||
|
throw new Error('Application commands require a description');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.args && options.args.length > 25) {
|
||||||
|
throw new Error('Application commands cannot have more than 25 arguments');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const arg of options.args) {
|
||||||
|
if (arg.choices && arg.choices.length > 25) {
|
||||||
|
throw new Error(`Application command argument ${arg.name} cannot have more than 25 choices`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,103 @@
|
|||||||
|
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,82 @@
|
|||||||
|
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 put<T = any>(path: string, data: any): Promise<T> {
|
||||||
|
return this.request('PUT', 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,32 @@
|
|||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import type { Unicord } from '.';
|
||||||
|
import type { UnicordCommandContext } from './context';
|
||||||
|
import type { UnicordCommandOptions, UnicordEventHandler, UnicordHandler } from './types/common';
|
||||||
|
import { UnicordEventType } 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitCommand(
|
||||||
|
type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands,
|
||||||
|
event: string,
|
||||||
|
context: UnicordCommandContext,
|
||||||
|
args: any[],
|
||||||
|
): boolean {
|
||||||
|
return super.emit(`${type}:${event}`, context, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSystem(event: string, payload: Parameters<UnicordEventHandler>[0]): boolean {
|
||||||
|
return super.emit(`${UnicordEventType.SystemEvent}:${event}`, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import { WebSocket, type Data } from 'ws';
|
||||||
|
import { Unicord } from '.';
|
||||||
|
import { UnicordCommandContext } from './context';
|
||||||
|
import type { DiscordMessage, Payload, UnicordEventContext } from './types';
|
||||||
|
import { fromDiscord, Message, 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: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private sequence: number | null = null;
|
||||||
|
private awaiting_heartbeat_ack = false;
|
||||||
|
|
||||||
|
private session_id: string | null = null;
|
||||||
|
private resume_gateway_url: string | null = null;
|
||||||
|
private should_resume = false;
|
||||||
|
private reconnecting = false;
|
||||||
|
private reconnect_attempts = 0;
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
user: User | null = null;
|
||||||
|
|
||||||
|
constructor(private readonly self: Unicord) {}
|
||||||
|
|
||||||
|
private onOpen = () => {
|
||||||
|
log('Connected to gateway');
|
||||||
|
this.reconnecting = false;
|
||||||
|
this.reconnect_attempts = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onClose = (code: number, reason: Buffer) => {
|
||||||
|
log('Gateway closed:', code, reason.toString());
|
||||||
|
this.clearHeartbeat();
|
||||||
|
|
||||||
|
if (!this.reconnecting) {
|
||||||
|
this.reconnect(this.canResumeFromCloseCode(code));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onError = (error: Error) => {
|
||||||
|
log('Gateway error:', error);
|
||||||
|
|
||||||
|
if (!this.reconnecting) {
|
||||||
|
this.reconnect(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
if (exists(data.s)) {
|
||||||
|
this.sequence = data.s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.sendHeartbeat();
|
||||||
|
} else if (code === 7) {
|
||||||
|
// Reconnect requested
|
||||||
|
this.reconnect(true);
|
||||||
|
} else if (code === 9) {
|
||||||
|
// Invalid session. Discord sends a boolean indicating whether resume is allowed.
|
||||||
|
const canResume = data.d === true;
|
||||||
|
if (!canResume) {
|
||||||
|
this.session_id = null;
|
||||||
|
this.sequence = null;
|
||||||
|
this.should_resume = false;
|
||||||
|
}
|
||||||
|
this.reconnect(canResume);
|
||||||
|
} 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 with initial jitter
|
||||||
|
this.startHeartbeat();
|
||||||
|
// Resume or identify with the gateway
|
||||||
|
if (this.should_resume && exists(this.session_id)) {
|
||||||
|
this.resume();
|
||||||
|
} else {
|
||||||
|
this.identify();
|
||||||
|
}
|
||||||
|
} else if (code === 11) {
|
||||||
|
// Heartbeat ACK
|
||||||
|
this.awaiting_heartbeat_ack = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private startHeartbeat() {
|
||||||
|
if (!exists(this.heartbeat_interval)) {
|
||||||
|
throw new Error('Heartbeat interval not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearHeartbeat();
|
||||||
|
|
||||||
|
const firstHeartbeatDelay = this.heartbeat_interval * Math.random();
|
||||||
|
this.heartbeat_timeout = setTimeout(() => {
|
||||||
|
this.heartbeat();
|
||||||
|
}, firstHeartbeatDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private heartbeat() {
|
||||||
|
if (!exists(this.heartbeat_interval)) {
|
||||||
|
throw new Error('Heartbeat interval not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.awaiting_heartbeat_ack) {
|
||||||
|
log('Missed heartbeat ACK; reconnecting gateway');
|
||||||
|
this.reconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendHeartbeat();
|
||||||
|
|
||||||
|
this.heartbeat_timeout = setTimeout(() => {
|
||||||
|
this.heartbeat();
|
||||||
|
}, this.heartbeat_interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendHeartbeat() {
|
||||||
|
this.awaiting_heartbeat_ack = true;
|
||||||
|
this.send({ op: 1, d: this.sequence });
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearHeartbeat() {
|
||||||
|
if (exists(this.heartbeat_timeout)) {
|
||||||
|
clearTimeout(this.heartbeat_timeout);
|
||||||
|
this.heartbeat_timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.awaiting_heartbeat_ack = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected send = (payload: Partial<Payload>) => {
|
||||||
|
if (isNil(this.socket) || this.socket.readyState !== WebSocket.OPEN) {
|
||||||
|
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 identify = () => {
|
||||||
|
this.should_resume = false;
|
||||||
|
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 resume = () => {
|
||||||
|
this.send({
|
||||||
|
op: 6,
|
||||||
|
d: {
|
||||||
|
token: this.self.config.token,
|
||||||
|
session_id: this.session_id,
|
||||||
|
seq: this.sequence,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
this.session_id = event_data.session_id;
|
||||||
|
this.resume_gateway_url = event_data.resume_gateway_url;
|
||||||
|
this.should_resume = true;
|
||||||
|
} else if (event_name === 'RESUMED') {
|
||||||
|
this.should_resume = true;
|
||||||
|
} else if (event_name === 'INTERACTION_CREATE') {
|
||||||
|
// TODO: Application commands/interactions need a redesigned context/hydration model before implementation.
|
||||||
|
} 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.emitSystem(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;
|
||||||
|
}
|
||||||
|
if (typeof payload.d.content !== 'string') {
|
||||||
|
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.emitCommand(UnicordEventType.ChatCommands, command, context, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private reconnect(resume: boolean) {
|
||||||
|
if (this.reconnecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnecting = true;
|
||||||
|
this.should_resume = resume && exists(this.session_id);
|
||||||
|
this.clearHeartbeat();
|
||||||
|
|
||||||
|
if (exists(this.socket)) {
|
||||||
|
this.socket.removeAllListeners();
|
||||||
|
this.socket.terminate();
|
||||||
|
this.socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = this.getReconnectDelay();
|
||||||
|
log(`Reconnecting gateway in ${delay}ms${this.should_resume ? ' with resume' : ''}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const gatewayUrl =
|
||||||
|
this.should_resume && exists(this.resume_gateway_url) ? this.resume_gateway_url : undefined;
|
||||||
|
this.connect(gatewayUrl).catch((error) => {
|
||||||
|
log('Gateway reconnect failed:', error);
|
||||||
|
this.reconnecting = false;
|
||||||
|
this.reconnect(this.should_resume);
|
||||||
|
});
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getReconnectDelay() {
|
||||||
|
const baseDelay = 1_000;
|
||||||
|
const maxDelay = 60_000;
|
||||||
|
const delay = Math.min(maxDelay, baseDelay * 2 ** this.reconnect_attempts);
|
||||||
|
this.reconnect_attempts += 1;
|
||||||
|
return delay + Math.floor(Math.random() * 1_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private canResumeFromCloseCode(code: number) {
|
||||||
|
// Discord documents these close codes as non-resumable.
|
||||||
|
return ![4004, 4010, 4011, 4012, 4013, 4014].includes(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(gatewayUrl?: string) {
|
||||||
|
// Fetch the Discord gateway URL when no explicit URL was provided.
|
||||||
|
const response = gatewayUrl
|
||||||
|
? { url: gatewayUrl }
|
||||||
|
: await this.self.discordAPI.request<{ url: string }>('GET', '/gateway/bot');
|
||||||
|
// Append the query parameters to the URL
|
||||||
|
const url = new URL(response.url);
|
||||||
|
url.searchParams.set('v', '10');
|
||||||
|
url.searchParams.set('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);
|
||||||
|
this.socket.on('close', this.onClose);
|
||||||
|
this.socket.on('error', this.onError);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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 UnicordHydrateable = 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>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 UnicordHydrateable, 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<T, [Expectation.Message]>;
|
||||||
|
// 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<T, [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<T, [Expectation.Channel]>;
|
||||||
|
context.channel = await self.caches.channels.get(data.channel_id);
|
||||||
|
} else if (expectation === Expectation.Message && exists(data.message)) {
|
||||||
|
let context = data as unknown as UnicordHydratedObject<T, [Expectation.Message]>;
|
||||||
|
context.message = data.message;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid expectation requested for Context: ${expectation}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Hydration for the provided type is not implemented.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the result is properly typed with both old and new hydrated properties
|
||||||
|
return data as T & UnicordHydratedObject<T, K>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is kinda dumb maybe just remove
|
||||||
|
// use(client: Unicord): this {
|
||||||
|
// this.commandManager.merge(client.commandManager);
|
||||||
|
// // TODO: inherit event listeners too
|
||||||
|
// return this;
|
||||||
|
// }
|
||||||
|
|
||||||
|
emitSystem(event: string, payload: UnicordEventContext) {
|
||||||
|
return this.eventProcessor.emitSystem(event, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitCommand(
|
||||||
|
type: UnicordEventType.ChatCommands | UnicordEventType.ApplicationCommands,
|
||||||
|
event: string,
|
||||||
|
context: UnicordCommandContext,
|
||||||
|
args: any[],
|
||||||
|
) {
|
||||||
|
return this.eventProcessor.emitCommand(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSystemEvent(event: string, handler: UnicordEventHandler): void {
|
||||||
|
this.__onEvent(UnicordEventType.SystemEvent, event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,406 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
export type UnicordEventHandler = (payload: EventPayload) => 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> =>
|
||||||
|
value !== null && 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
+10
-3
@@ -1,10 +1,11 @@
|
|||||||
// Base classes
|
// Base classes
|
||||||
export { Client } from './services/client';
|
export { Client, Client as unicord } from './services/client';
|
||||||
export { Intent, Trait } from './types/common';
|
export { Intent, Trait } from './types/common';
|
||||||
// Consumable types
|
// Consumable types
|
||||||
export type { ClientConfig } from './types/common';
|
export type { ClientConfig } from './types/common';
|
||||||
export type { Context } from './types/context';
|
export type { Context } from './types/context';
|
||||||
export { Expectation } from './types/common';
|
export { Expectation } from './types/common';
|
||||||
|
export { ChannelType, ChannelFlags, VideoQualityMode, SortOrderType, ForumLayoutType } from './types/channel';
|
||||||
// Consumable util classes
|
// Consumable util classes
|
||||||
export { Guild } from './types/guild';
|
export { Guild } from './types/guild';
|
||||||
export { Role } from './types/role';
|
export { Role } from './types/role';
|
||||||
@@ -14,5 +15,11 @@ export { Message, type MessagePayload, ComponentStyle, ComponentType } from './t
|
|||||||
export { Embed } from './types/embed';
|
export { Embed } from './types/embed';
|
||||||
export { Emoji } from './types/emoji';
|
export { Emoji } from './types/emoji';
|
||||||
// Other
|
// Other
|
||||||
export { createCommandHandler, type InteractionData, type InteractionCommpoentData, type InteractionCommandData, type InteractionHandlerFunction } from './types/handler';
|
export {
|
||||||
export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand';
|
createCommandHandler,
|
||||||
|
type InteractionData,
|
||||||
|
type InteractionCommpoentData,
|
||||||
|
type InteractionCommandData,
|
||||||
|
type InteractionHandlerFunction,
|
||||||
|
} from './types/handler';
|
||||||
|
export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand';
|
||||||
|
|||||||
+7
-8
@@ -1,5 +1,5 @@
|
|||||||
import { exists, log } from "../utils";
|
import { exists, log } from '../utils';
|
||||||
import type { Client } from "./client";
|
import type { Client } from './client';
|
||||||
|
|
||||||
export class API {
|
export class API {
|
||||||
private readonly base_url: string = 'https://discord.com/api/v10';
|
private readonly base_url: string = 'https://discord.com/api/v10';
|
||||||
@@ -9,9 +9,9 @@ export class API {
|
|||||||
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
|
async request<T = any>(method: string, path: string, data?: any): Promise<T> {
|
||||||
const url = `${this.base_url}${path.startsWith('/') ? '' : '/'}${path}`;
|
const url = `${this.base_url}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Bot ${this.client.token}`,
|
Authorization: `Bot ${this.client.config.token}`,
|
||||||
'Accept': 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -28,7 +28,6 @@ export class API {
|
|||||||
|
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
|
|
||||||
|
|
||||||
// console.log(`API Request: ${method} ${url}`);
|
// console.log(`API Request: ${method} ${url}`);
|
||||||
// console.log(`API Body: ${JSON.stringify(data, null, 4)}`);
|
// console.log(`API Body: ${JSON.stringify(data, null, 4)}`);
|
||||||
// console.log('API Response:', JSON.stringify(body, null, 4));
|
// console.log('API Response:', JSON.stringify(body, null, 4));
|
||||||
@@ -49,7 +48,7 @@ export class API {
|
|||||||
const retryAfter = response.headers.get('Retry-After');
|
const retryAfter = response.headers.get('Retry-After');
|
||||||
if (exists(retryAfter)) {
|
if (exists(retryAfter)) {
|
||||||
// Wait for the retry-after time
|
// Wait for the retry-after time
|
||||||
await new Promise(resolve => setTimeout(resolve, parseInt(retryAfter) * 1000));
|
await new Promise((resolve) => setTimeout(resolve, parseInt(retryAfter) * 1000));
|
||||||
} else {
|
} else {
|
||||||
// TODO: Handle this case better
|
// TODO: Handle this case better
|
||||||
// Maybe wait some arbitrary time??
|
// Maybe wait some arbitrary time??
|
||||||
@@ -75,4 +74,4 @@ export class API {
|
|||||||
async delete<T = any>(path: string): Promise<T> {
|
async delete<T = any>(path: string): Promise<T> {
|
||||||
return this.request('DELETE', path);
|
return this.request('DELETE', path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -3,8 +3,13 @@ import type { Context } from '../types/context';
|
|||||||
|
|
||||||
const EventBus = new EventEmitter() as Emitter;
|
const EventBus = new EventEmitter() as Emitter;
|
||||||
|
|
||||||
export type Emitter = EventEmitter & {
|
export class Emitter extends EventEmitter {
|
||||||
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean
|
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 };
|
export { EventBus };
|
||||||
+108
-15
@@ -1,15 +1,22 @@
|
|||||||
import { fromDiscord } from "../../types/common";
|
import { fromDiscord } from '../../types/common';
|
||||||
import type { Context } from "../../types/context";
|
import type { Context } from '../../types/context';
|
||||||
import type { API } from "../api";
|
import type { API } from '../api';
|
||||||
import type { Processor } from "../processor";
|
import type { Processor } from '../processor';
|
||||||
|
|
||||||
|
|
||||||
export class ObjectCache<T, K extends T> {
|
export class ObjectCache<T, K extends T> {
|
||||||
cache = new Map<string, K>();
|
cache: LRUCache<string, K>;
|
||||||
requests = new Map<string, Promise<K>>();
|
requests = new Map<string, Promise<K>>();
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
|
|
||||||
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: 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;
|
this.type = type;
|
||||||
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
|
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
|
||||||
this.cache.set(e.id, factory[fromDiscord](e));
|
this.cache.set(e.id, factory[fromDiscord](e));
|
||||||
@@ -34,8 +41,8 @@ export class ObjectCache<T, K extends T> {
|
|||||||
async fetch(object_id: string, ..._args: any[]): Promise<K> {
|
async fetch(object_id: string, ..._args: any[]): Promise<K> {
|
||||||
if (this.requests.has(object_id)) {
|
if (this.requests.has(object_id)) {
|
||||||
return this.requests.get(object_id)!;
|
return this.requests.get(object_id)!;
|
||||||
// } else if (this.cache.has(object_id)) {
|
// } else if (this.cache.has(object_id)) {
|
||||||
// return Promise.resolve(this.cache.get(object_id)!);
|
// return Promise.resolve(this.cache.get(object_id)!);
|
||||||
} else {
|
} else {
|
||||||
// Dispatch the request
|
// Dispatch the request
|
||||||
const req = this.api.get(`/${this.type}/${object_id}`).then((data) => {
|
const req = this.api.get(`/${this.type}/${object_id}`).then((data) => {
|
||||||
@@ -48,10 +55,8 @@ export class ObjectCache<T, K extends T> {
|
|||||||
});
|
});
|
||||||
// Save the request
|
// Save the request
|
||||||
this.requests.set(object_id, req);
|
this.requests.set(object_id, req);
|
||||||
// Remove the request from the cache if it succeeds
|
// Remove the request from the cache when it finishes
|
||||||
req.then(() => this.requests.delete(object_id));
|
req.finally(() => this.requests.delete(object_id));
|
||||||
// Remove the request from the cache if it fails
|
|
||||||
req.catch(() => this.requests.delete(object_id));
|
|
||||||
// Return the request
|
// Return the request
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
@@ -64,8 +69,96 @@ export class ObjectCache<T, K extends T> {
|
|||||||
return this.fetch(object_id);
|
return this.fetch(object_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list(): Map<string, Promise<T> | T> {
|
class ListNode<K, V> {
|
||||||
return this.cache;
|
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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { fromDiscord } from "../../types/common";
|
import { fromDiscord } from "../../types/common";
|
||||||
import { Message, type MessagePayload } from "../../types/message";
|
import { Message, type MessagePayload } from "../../types/message";
|
||||||
import type { API } from "../api";
|
import type { API } from "../api";
|
||||||
import type { Client } from "../client";
|
|
||||||
import type { Processor } from "../processor";
|
import type { Processor } from "../processor";
|
||||||
import { ObjectCache } from "./cache";
|
import { ObjectCache } from "./cache";
|
||||||
|
|
||||||
|
|
||||||
export class Messages extends ObjectCache<Message, MessagePayload> {
|
export class Messages extends ObjectCache<Message, MessagePayload> {
|
||||||
constructor (protected client: Client, api: API, processor: Processor) {
|
constructor (api: API, processor: Processor) {
|
||||||
super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
|
super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
|
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
|
||||||
|
|||||||
+83
-46
@@ -1,52 +1,53 @@
|
|||||||
import { API } from './api';
|
import { type DiscordAPIEvent, Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
|
||||||
import { Gateway } from './connectors/gateway';
|
import { Message, type DiscordMessage } from '../types/message';
|
||||||
import { Processor } from './processor';
|
|
||||||
import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
|
|
||||||
import { exists, isNil } from '../utils/index';
|
import { exists, isNil } from '../utils/index';
|
||||||
import { type DiscordMessage, Message } from '../types/message';
|
import { API } from './api';
|
||||||
|
import { EventBus, type Emitter } from './bus';
|
||||||
|
import { Channels } from './caches/channels';
|
||||||
import { Guilds } from './caches/guilds';
|
import { Guilds } from './caches/guilds';
|
||||||
import { Messages } from './caches/messages';
|
import { Messages } from './caches/messages';
|
||||||
import { Users } from './caches/users';
|
import { Users } from './caches/users';
|
||||||
|
import { Gateway } from './connectors/gateway';
|
||||||
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||||
import { EventBus, type Emitter } from './bus';
|
import { Processor } from './processor';
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
readonly token: string;
|
readonly config: Readonly<Required<ClientConfig>>;
|
||||||
readonly application_id: string;
|
|
||||||
readonly intents: Intent[] = [Intent.DEFAULT];
|
|
||||||
prefix: string;
|
|
||||||
// Internal services
|
// Internal services
|
||||||
protected bus: Emitter;
|
protected bus: Emitter;
|
||||||
protected api: API;
|
protected api: API;
|
||||||
protected processor: Processor;
|
protected processor: Processor;
|
||||||
protected gateway: Gateway;
|
protected gateway: Gateway;
|
||||||
guilds: Guilds;
|
// Caches
|
||||||
users: Users;
|
readonly caches: {
|
||||||
messages: Messages;
|
readonly guilds: Guilds;
|
||||||
chatCommands: Processor[HandlerType.ChatCommands];
|
readonly users: Users;
|
||||||
applicationCommands: Processor[HandlerType.ApplicationCommands];
|
readonly messages: Messages;
|
||||||
interactions: Processor[HandlerType.Interactions];
|
readonly channels: Channels;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(config: ClientConfig) {
|
constructor(config: ClientConfig) {
|
||||||
// Validate the configuration
|
// Validate the configuration
|
||||||
Client.validateConfig(config);
|
Client.validateConfig(config);
|
||||||
// Save the configuration
|
// Save the configuration
|
||||||
this.token = config.token;
|
this.config = Object.freeze({
|
||||||
this.application_id = config.application_id;
|
token: config.token,
|
||||||
this.intents = config.intents ?? this.intents;
|
application_id: config.application_id,
|
||||||
this.prefix = config.prefix ?? '!';
|
intents: config.intents ?? [Intent.DEFAULT],
|
||||||
|
prefix: config.prefix ?? '!',
|
||||||
|
});
|
||||||
// Initialize internal services
|
// Initialize internal services
|
||||||
this.bus = EventBus;
|
this.bus = EventBus;
|
||||||
this.api = new API(this);
|
this.api = new API(this);
|
||||||
this.processor = new Processor(this, this.api, this.bus);
|
this.processor = new Processor(this, this.api, this.bus);
|
||||||
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
||||||
this.guilds = new Guilds(this.api, this.processor);
|
// Initialize caches
|
||||||
this.users = new Users(this.api, this.processor);
|
this.caches = {
|
||||||
this.messages = new Messages(this, this.api, this.processor);
|
guilds: new Guilds(this.api, this.processor),
|
||||||
// Save references to command and event handlers
|
users: new Users(this.api, this.processor),
|
||||||
this.chatCommands = this.processor[HandlerType.ChatCommands];
|
messages: new Messages(this.api, this.processor),
|
||||||
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
|
channels: new Channels(this.api, this.processor),
|
||||||
this.interactions = this.processor[HandlerType.Interactions];
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static validateConfig(config: ClientConfig) {
|
private static validateConfig(config: ClientConfig) {
|
||||||
@@ -79,45 +80,81 @@ export class Client {
|
|||||||
return this.gateway.user;
|
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.
|
* 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 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.
|
* @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 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 expectations - An array of expectations that specify what properties should be hydrated.
|
||||||
*
|
*
|
||||||
* @returns A promise that resolves to the hydrated data object.
|
* @returns A promise that resolves to the hydrated data object.
|
||||||
*
|
*
|
||||||
* @throws Will throw an error if an expectation cannot be resolved.
|
* @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) => {
|
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);
|
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.
|
* 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 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.
|
* @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 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 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`.
|
* @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) => {
|
hydrator = async <T extends Hydrateable, K extends Expectation[]>(data: T, expectations: K) => {
|
||||||
return hydrator<T, K>(data, expectations, this, this.api);
|
return hydrator<T, K>(data, expectations, this, this.api);
|
||||||
}
|
};
|
||||||
|
|
||||||
sendMessage = async (channel_id: string, message: Message | string) => {
|
sendMessage = async (channel_id: string, message: Message | string) => {
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
message = new Message().setContent(message);
|
message = new Message().setContent(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Message.fromDiscord(await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()));
|
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 = {
|
// chatCommands = {
|
||||||
// register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
|
// register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
|
||||||
@@ -131,11 +168,11 @@ export class Client {
|
|||||||
// has: (name: string) => this.processor.application_commands.has(name)
|
// has: (name: string) => this.processor.application_commands.has(name)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
on = (event: string, handler: (...args: any[]) => void) => this.bus.on(event, handler);
|
on = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.on(event, handler);
|
||||||
|
|
||||||
once = (event: string, handler: (...args: any[]) => void) => this.bus.once(event, handler);
|
once = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.once(event, handler);
|
||||||
|
|
||||||
off = (event: string, handler: (...args: any[]) => void) => this.bus.off(event, handler);
|
off = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.off(event, handler);
|
||||||
|
|
||||||
connect = () => this.gateway.connect();
|
connect = () => this.gateway.connect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,15 +205,15 @@ export class Gateway {
|
|||||||
// Generate context
|
// Generate context
|
||||||
const context = new Context(this.client, this.api, { message });
|
const context = new Context(this.client, this.api, { message });
|
||||||
// Dispatch event for the command
|
// Dispatch event for the command
|
||||||
this.bus.emit(`command:${command}`, context, args);
|
// this.bus.emit(`${HandlerType.ChatCommands}:${command}`, context, args);
|
||||||
// // Check if command exists
|
// // Check if command exists
|
||||||
// if (this.processor[HandlerType.ChatCommands].has(command)) {
|
if (this.processor[HandlerType.ChatCommands].has(command)) {
|
||||||
// // Execute the handler
|
// Execute the handler
|
||||||
// this.processor[HandlerType.ChatCommands].execute(command, context, args);
|
this.processor[HandlerType.ChatCommands].execute(command, context, args);
|
||||||
// } else {
|
} else {
|
||||||
// // TODO: Custom unknown command handler
|
// TODO: Custom unknown command handler
|
||||||
// context.reply(`Unknown command: ${command}`);
|
// context.reply(`Unknown command: ${command}`);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+25
-14
@@ -1,15 +1,16 @@
|
|||||||
import { Expectation } from "../types/common";
|
import { Expectation } from "../types/common";
|
||||||
import { Context, type HydratedContext } from "../types/context";
|
import { Context, type HydratedContext } from "../types/context";
|
||||||
import { Message, type HydratedMessage } from "../types/message";
|
import { Message, type HydratedMessage } from "../types/message";
|
||||||
|
import { Channel } from "../types/channel";
|
||||||
import { exists } from "../utils";
|
import { exists } from "../utils";
|
||||||
import type { API } from "./api";
|
import type { API } from "./api";
|
||||||
import type { Client } from "./client";
|
import type { Client } from "./client";
|
||||||
|
|
||||||
export type GatewayObject = Message;
|
export type GatewayObject = Message | Channel;
|
||||||
export type Hydrateable = Context | GatewayObject
|
export type Hydrateable = Context | GatewayObject
|
||||||
export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
|
export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
|
||||||
T extends Message ? HydratedMessage<K> :
|
T extends Message ? HydratedMessage<K> & Omit<T, keyof HydratedMessage<K>> :
|
||||||
T extends Context ? HydratedContext<K> : never;
|
T extends Context ? HydratedContext<K> & Omit<T, keyof HydratedContext<K>> : never;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hydrates a given data object based on the provided expectations.
|
* Hydrates a given data object based on the provided expectations.
|
||||||
@@ -26,12 +27,12 @@ export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
|
|||||||
*
|
*
|
||||||
* @throws Will throw an error if an expectation cannot be resolved.
|
* @throws Will throw an error if an expectation cannot be resolved.
|
||||||
*/
|
*/
|
||||||
export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
|
export async function hydrate<T extends Hydrateable, K extends Array<Expectation>>(
|
||||||
data: T,
|
data: T,
|
||||||
expectations: K,
|
expectations: K,
|
||||||
client: Client,
|
client: Client,
|
||||||
api: API
|
api: API
|
||||||
): Promise<T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
|
): Promise<Hydrated<T, K>> {
|
||||||
// TODO: add clone trait. Make it a requirement for hydrateable objects.
|
// TODO: add clone trait. Make it a requirement for hydrateable objects.
|
||||||
// Then clone the object and return the hydrated object.
|
// Then clone the object and return the hydrated object.
|
||||||
let hydrated: Hydrated<T, K>;
|
let hydrated: Hydrated<T, K>;
|
||||||
@@ -40,10 +41,13 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
|
|||||||
for (const expectation of expectations) {
|
for (const expectation of expectations) {
|
||||||
if (expectation === Expectation.Message && exists(data.message_id)) {
|
if (expectation === Expectation.Message && exists(data.message_id)) {
|
||||||
let context = data as unknown as HydratedContext<[Expectation.Message, ...Expectation[]]>;
|
let context = data as unknown as HydratedContext<[Expectation.Message, ...Expectation[]]>;
|
||||||
context.message = await client.messages.get(data.channel_id, data.message_id);
|
context.message = await client.caches.messages.get(data.channel_id, data.message_id);
|
||||||
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||||
let context = data as unknown as HydratedContext<[Expectation.Guild, ...Expectation[]]>;
|
let context = data as unknown as HydratedContext<[Expectation.Guild, ...Expectation[]]>;
|
||||||
context.guild = await client.guilds.get(data.guild_id);
|
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 {
|
} else {
|
||||||
throw new Error(`Invalid expectation requested for Context: ${expectation}`);
|
throw new Error(`Invalid expectation requested for Context: ${expectation}`);
|
||||||
}
|
}
|
||||||
@@ -52,17 +56,24 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
|
|||||||
for (const expectation of expectations) {
|
for (const expectation of expectations) {
|
||||||
if (expectation === Expectation.Message && exists(data.message_reference?.id)) {
|
if (expectation === Expectation.Message && exists(data.message_reference?.id)) {
|
||||||
let message = data as unknown as HydratedMessage<[Expectation.Message, ...Expectation[]]>;
|
let message = data as unknown as HydratedMessage<[Expectation.Message, ...Expectation[]]>;
|
||||||
message.reference = await client.messages.get(data.message_reference.channel_id, data.message_reference.id);
|
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)) {
|
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||||
let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>;
|
let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>;
|
||||||
message.guild = await client.guilds.get(data.guild_id);
|
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 {
|
} else {
|
||||||
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as unknown as (T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>);
|
// 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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,12 +89,12 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
|
|||||||
*
|
*
|
||||||
* @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`.
|
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
|
||||||
*/
|
*/
|
||||||
export async function hydrator<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
|
export async function hydrator<T extends Hydrateable, K extends Array<Expectation>>(
|
||||||
data: T,
|
data: T,
|
||||||
expectations: K,
|
expectations: K,
|
||||||
client: Client,
|
client: Client,
|
||||||
api: API
|
api: API
|
||||||
): Promise<(data: unknown) => data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
|
): Promise<(data: unknown) => data is Hydrated<T, K>> {
|
||||||
let hydrated = true;
|
let hydrated = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -95,7 +106,7 @@ export async function hydrator<T extends Hydrateable | Hydrated<any, any>, K ext
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BUG: Resolves to never after hydrations on an already hydrated object
|
// BUG: Resolves to never after hydrations on an already hydrated object
|
||||||
return function (data: unknown): data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K> {
|
return function (data: unknown): data is Hydrated<T, K> {
|
||||||
return hydrated;
|
return hydrated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-15
@@ -1,10 +1,10 @@
|
|||||||
import type { Client } from "./client";
|
|
||||||
import type { Context } from "../types/context";
|
|
||||||
import { HandlerType, Trait, type EventPayload } from "../types/common";
|
|
||||||
import { CommandHandler, type Handler, type ArgsFromOptions, OptionConstructorMap, type InteractionData } from "../types/handler";
|
|
||||||
import type { API } from "./api";
|
|
||||||
import { ApplicationCommandOptionType, ApplicationCommandType, type ApplicationCommandOption } from "../types/applicationCommand";
|
import { 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 { Emitter } from "./bus";
|
||||||
|
import type { Client } from "./client";
|
||||||
|
|
||||||
export class Processor {
|
export class Processor {
|
||||||
private handlers = {
|
private handlers = {
|
||||||
@@ -14,13 +14,13 @@ export class Processor {
|
|||||||
[HandlerType.Interactions]: new Map<string, Handler[]>()
|
[HandlerType.Interactions]: new Map<string, Handler[]>()
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(private client: Client, private api: API, private bus: Emitter) {};
|
constructor(private client: Client, private api: API, private bus: Emitter) { };
|
||||||
|
|
||||||
public register(type: HandlerType, event: string | string[], handler: Handler) {
|
public register(type: HandlerType, event: string | string[], handler: Handler) {
|
||||||
// Normalize events into an array
|
// Normalize events into an array
|
||||||
const events = Array.isArray(event) ? event : [event];
|
const events = Array.isArray(event) ? event : [event];
|
||||||
// Register the handler for each event
|
// Register the handler for each event
|
||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
const handlers = this.handlers[type].get(e) || [];
|
const handlers = this.handlers[type].get(e) || [];
|
||||||
handlers.push(handler);
|
handlers.push(handler);
|
||||||
this.handlers[type].set(e, handlers);
|
this.handlers[type].set(e, handlers);
|
||||||
@@ -64,6 +64,9 @@ export class Processor {
|
|||||||
throw new Error('Event handlers cannot be a CommandHandler');
|
throw new Error('Event handlers cannot be a CommandHandler');
|
||||||
}
|
}
|
||||||
// Validate and resolve the args
|
// 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);
|
const resolvedArgs = await this.validateAndResolveArgs(argsOrPayload, handler.args);
|
||||||
// Execute the handler
|
// Execute the handler
|
||||||
await Promise.resolve(handler[Trait.execute](context!, resolvedArgs));
|
await Promise.resolve(handler[Trait.execute](context!, resolvedArgs));
|
||||||
@@ -75,7 +78,7 @@ export class Processor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateAndResolveArgs = async <T extends readonly ApplicationCommandOption[]>(args: any[], definition: T): Promise<ArgsFromOptions<T>> => {
|
private validateAndResolveArgs = async <T extends readonly ApplicationCommandOption[]>(args: any[], definition: T): Promise<ArgsFromOptions<T>> => {
|
||||||
// Zip the args and definition together
|
// Zip the args and definition together
|
||||||
const zipped: [ApplicationCommandOption, any][] = args.map((arg, i) => [definition[i], arg]);
|
const zipped: [ApplicationCommandOption, any][] = args.map((arg, i) => [definition[i], arg]);
|
||||||
@@ -99,11 +102,11 @@ export class Processor {
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new Error(`Invalid user for ${def.name}: expected a user mention or snowflake id, got ${arg}`);
|
throw new Error(`Invalid user for ${def.name}: expected a user mention or snowflake id, got ${arg}`);
|
||||||
}
|
}
|
||||||
validated[def.name] = await this.client.users.get(userId);
|
validated[def.name] = await this.client.caches.users.get(userId);
|
||||||
} else if (typeof arg === 'object') {
|
} else if (typeof arg === 'object') {
|
||||||
// Check if the object has an ID
|
// Check if the object has an ID
|
||||||
if (arg.id) {
|
if (arg.id) {
|
||||||
validated[def.name] = await this.client.users.get(arg.id);
|
validated[def.name] = await this.client.caches.users.get(arg.id);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid user for ${def.name}: expected a user object with an ID, got ${arg}`);
|
throw new Error(`Invalid user for ${def.name}: expected a user object with an ID, got ${arg}`);
|
||||||
}
|
}
|
||||||
@@ -135,7 +138,7 @@ export class Processor {
|
|||||||
register: (event: string | string[], handler: Handler) => this.register(HandlerType.ChatCommands, event, handler),
|
register: (event: string | string[], handler: Handler) => this.register(HandlerType.ChatCommands, event, handler),
|
||||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(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),
|
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
|
has: (event: string) => this.handlers[HandlerType.ChatCommands].has(event) && this.handlers[HandlerType.ChatCommands].get(event)?.length! > 0
|
||||||
};
|
};
|
||||||
|
|
||||||
[HandlerType.ApplicationCommands] = {
|
[HandlerType.ApplicationCommands] = {
|
||||||
@@ -151,7 +154,7 @@ export class Processor {
|
|||||||
|
|
||||||
// Ensure the command name is valid
|
// Ensure the command name is valid
|
||||||
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
// @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming
|
||||||
// TODO: can this sus bind be removed? Is the only way to kill the arrow function?
|
// 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 commandNameRegex = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u;
|
||||||
const commandNameTest = commandNameRegex.test.bind(commandNameRegex)
|
const commandNameTest = commandNameRegex.test.bind(commandNameRegex)
|
||||||
if (!names.every(commandNameTest)) {
|
if (!names.every(commandNameTest)) {
|
||||||
@@ -163,21 +166,21 @@ export class Processor {
|
|||||||
const command: Record<string, any> = handler.toJSON();
|
const command: Record<string, any> = handler.toJSON();
|
||||||
command.name = n;
|
command.name = n;
|
||||||
command.type = type;
|
command.type = type;
|
||||||
this.api.post(`/applications/${this.client.application_id}/commands`, command);
|
this.api.post(`/applications/${this.client.config.application_id}/commands`, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.register(HandlerType.ApplicationCommands, names, handler)
|
this.register(HandlerType.ApplicationCommands, names, handler)
|
||||||
},
|
},
|
||||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(HandlerType.ApplicationCommands, event, 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),
|
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,
|
has: (event: string) => this.handlers[HandlerType.ApplicationCommands].has(event) && this.handlers[HandlerType.ApplicationCommands].get(event)?.length! > 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
[HandlerType.Interactions] = {
|
[HandlerType.Interactions] = {
|
||||||
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler),
|
register: (event: string | string[], handler: Handler) => this.register(HandlerType.Interactions, event, handler),
|
||||||
unregister: (event: string | string[], handler?: Handler) => this.unregister(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),
|
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
|
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[]> => {
|
static isCommandHandler = (handler: Handler): handler is CommandHandler<ApplicationCommandOption[]> => {
|
||||||
|
|||||||
+268
-29
@@ -1,39 +1,278 @@
|
|||||||
export class Channel {
|
import { GatewayObject } from "./base";
|
||||||
version: number;
|
import { Trait } from "./common";
|
||||||
type: number;
|
import { User, type DiscordUser } from "./user";
|
||||||
position: number;
|
|
||||||
permission_overwrites: any[];
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
flags: number;
|
|
||||||
topic?: string | null;
|
|
||||||
rate_limit_per_user?: number;
|
|
||||||
parent_id?: string;
|
|
||||||
last_message_id?: string | null;
|
|
||||||
user_limit?: number;
|
|
||||||
rtc_region?: string | null;
|
|
||||||
bitrate?: number;
|
|
||||||
last_pin_timestamp?: string;
|
|
||||||
|
|
||||||
constructor(data: any) {
|
export enum ChannelType {
|
||||||
this.version = data.version;
|
GUILD_TEXT = 0,
|
||||||
this.type = data.type;
|
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.position = data.position;
|
||||||
this.permission_overwrites = data.permission_overwrites;
|
this.permission_overwrites = data.permission_overwrites;
|
||||||
this.name = data.name;
|
this.name = data.name;
|
||||||
this.id = data.id;
|
|
||||||
this.flags = data.flags;
|
|
||||||
this.topic = data.topic;
|
this.topic = data.topic;
|
||||||
this.rate_limit_per_user = data.rate_limit_per_user;
|
this.nsfw = data.nsfw;
|
||||||
this.parent_id = data.parent_id;
|
|
||||||
this.last_message_id = data.last_message_id;
|
this.last_message_id = data.last_message_id;
|
||||||
this.user_limit = data.user_limit;
|
|
||||||
this.rtc_region = data.rtc_region;
|
|
||||||
this.bitrate = data.bitrate;
|
this.bitrate = data.bitrate;
|
||||||
this.last_pin_timestamp = data.last_pin_timestamp;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromAPIResponse(data: any): Channel {
|
public static [Trait.fromDiscord]<T = Channel>(data: DiscordChannel): T {
|
||||||
return new Channel(data);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+183
-102
@@ -3,7 +3,7 @@ export type ClientConfig = {
|
|||||||
application_id: string;
|
application_id: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
intents?: Intent[];
|
intents?: Intent[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export enum Expectation {
|
export enum Expectation {
|
||||||
Guild = 'guild',
|
Guild = 'guild',
|
||||||
@@ -16,24 +16,26 @@ export enum HandlerType {
|
|||||||
Events = 'events',
|
Events = 'events',
|
||||||
ChatCommands = 'chat_commands',
|
ChatCommands = 'chat_commands',
|
||||||
ApplicationCommands = 'application_commands',
|
ApplicationCommands = 'application_commands',
|
||||||
Interactions = 'interactions'
|
Interactions = 'interactions',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Expects<T extends Array<Expectation>, K extends Expectation> = Extract<K, T[number]> extends never ? false : true;
|
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 clone: unique symbol = Symbol('clone');
|
||||||
export const fromDiscord: unique symbol = Symbol('fromDiscord')
|
export const fromDiscord: unique symbol = Symbol('fromDiscord');
|
||||||
export const execute: unique symbol = Symbol('execute')
|
export const execute: unique symbol = Symbol('execute');
|
||||||
|
|
||||||
export const Trait: {
|
export const Trait: {
|
||||||
clone: typeof clone
|
clone: typeof clone;
|
||||||
fromDiscord: typeof fromDiscord
|
fromDiscord: typeof fromDiscord;
|
||||||
execute: typeof execute
|
execute: typeof execute;
|
||||||
} = {
|
} = {
|
||||||
clone,
|
clone,
|
||||||
fromDiscord,
|
fromDiscord,
|
||||||
execute
|
execute,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type EventPayload = any;
|
export type EventPayload = any;
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ export type Primitive = string | number | boolean | null;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a payload structure used in the Discord API.
|
* Represents a payload structure used in the Discord API.
|
||||||
*
|
*
|
||||||
* @typedef {Object} Payload
|
* @typedef {Object} Payload
|
||||||
* @property {number} op - The opcode of the payload.
|
* @property {number} op - The opcode of the payload.
|
||||||
* @property {Record<string, any>} d - The data associated with the payload.
|
* @property {Record<string, any>} d - The data associated with the payload.
|
||||||
@@ -58,7 +60,7 @@ export type Payload = {
|
|||||||
export enum InteractionResponseType {
|
export enum InteractionResponseType {
|
||||||
Pong = 1,
|
Pong = 1,
|
||||||
Message = 4,
|
Message = 4,
|
||||||
DeferredMessage = 5
|
DeferredMessage = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Intent {
|
export enum Intent {
|
||||||
@@ -79,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',
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { hydrate, hydrator, type Hydrateable, type Hydrated } from "../services/
|
|||||||
import { isNil, exists } from "../utils";
|
import { isNil, exists } from "../utils";
|
||||||
import { fromDiscord, InteractionResponseType, type Expectation } from "./common";
|
import { fromDiscord, InteractionResponseType, type Expectation } from "./common";
|
||||||
import type { Guild } from "./guild";
|
import type { Guild } from "./guild";
|
||||||
|
import { Channel } from "./channel";
|
||||||
import type { InteractionPayload } from "./handler";
|
import type { InteractionPayload } from "./handler";
|
||||||
import { type DiscordMessage, Message, type MessagePayload } from "./message";
|
import { type DiscordMessage, Message, type MessagePayload } from "./message";
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +29,7 @@ import { type DiscordMessage, Message, type MessagePayload } from "./message";
|
|||||||
export type HydratedContext<T extends Array<Expectation>> = Context & {
|
export type HydratedContext<T extends Array<Expectation>> = Context & {
|
||||||
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
||||||
message: Extract<Expectation.Message, T[number]> extends never ? undefined : Message;
|
message: Extract<Expectation.Message, T[number]> extends never ? undefined : Message;
|
||||||
|
channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextData =
|
export type ContextData =
|
||||||
@@ -92,7 +94,7 @@ export class Context<D extends ContextData = ContextData> {
|
|||||||
*
|
*
|
||||||
* @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`.
|
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
|
||||||
*/
|
*/
|
||||||
public hydrator = async <T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>, K extends Array<Expectation>>(data: T, expectations: K) => {
|
public hydrator = async <T extends Hydrateable, K extends Array<Expectation>>(data: T, expectations: K) => {
|
||||||
return hydrator<T, K>(data, expectations, this.client, this.api);
|
return hydrator<T, K>(data, expectations, this.client, this.api);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,4 +176,4 @@ export class Context<D extends ContextData = ContextData> {
|
|||||||
public defer = async () => {
|
public defer = async () => {
|
||||||
return this.ack(InteractionResponseType.DeferredMessage);
|
return this.ack(InteractionResponseType.DeferredMessage);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -1,9 +1,10 @@
|
|||||||
import { exists, isNil } from '../utils';
|
import { exists, isNil } from '../utils';
|
||||||
import { GatewayObject } from './base';
|
import { GatewayObject } from './base';
|
||||||
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
|
import type { Channel } from './channel';
|
||||||
|
import { fromDiscord, Trait, type Expectation } from './common';
|
||||||
import type { Embed } from './embed';
|
import type { Embed } from './embed';
|
||||||
import type { Guild } from './guild';
|
import type { Guild } from './guild';
|
||||||
import { type DiscordUser, User } from './user';
|
import { User, type DiscordUser } from './user';
|
||||||
|
|
||||||
export type Component = Partial<Button | SelectMenu> & {
|
export type Component = Partial<Button | SelectMenu> & {
|
||||||
type: ComponentType;
|
type: ComponentType;
|
||||||
@@ -34,6 +35,11 @@ export type Component = Partial<Button | SelectMenu> & {
|
|||||||
// components?: Component[];
|
// 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 = {
|
export type Emoji = {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
id: string | null;
|
id: string | null;
|
||||||
@@ -83,7 +89,7 @@ export enum ComponentType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type HydratedMessage<T extends Array<Expectation>> = MessagePayload & {
|
export type HydratedMessage<T extends Array<Expectation>> = MessagePayload & {
|
||||||
// channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
||||||
reference: Extract<Expectation.Message, T[number]> extends never ? undefined : MessagePayload;
|
reference: Extract<Expectation.Message, T[number]> extends never ? undefined : MessagePayload;
|
||||||
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
||||||
}
|
}
|
||||||
@@ -274,4 +280,4 @@ export type DiscordMessage = {
|
|||||||
preview_asset?: string;
|
preview_asset?: string;
|
||||||
format_type: number;
|
format_type: number;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-12
@@ -1,4 +1,4 @@
|
|||||||
import { ApplicationCommandType, Client, CommandOptionType, ComponentStyle, type Context, createCommandHandler, Expectation, Intent, InteractionCommpoentData, type InteractionData, Message, type MessagePayload } from '../src/index';
|
import { ApplicationCommandType, ChannelType, Client, CommandOptionType, ComponentStyle, type Context, createCommandHandler, Expectation, Intent, InteractionCommpoentData, type InteractionData, Message, type MessagePayload } from '../src/index';
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
token: process.env.BOT_TOKEN!,
|
token: process.env.BOT_TOKEN!,
|
||||||
@@ -7,7 +7,9 @@ const client = new Client({
|
|||||||
Intent.GUILDS,
|
Intent.GUILDS,
|
||||||
Intent.GUILD_MESSAGES,
|
Intent.GUILD_MESSAGES,
|
||||||
Intent.GUILD_MESSAGE_REACTIONS,
|
Intent.GUILD_MESSAGE_REACTIONS,
|
||||||
Intent.MESSAGE_CONTENT
|
Intent.MESSAGE_CONTENT,
|
||||||
|
Intent.DIRECT_MESSAGES,
|
||||||
|
Intent.DIRECT_MESSAGE_REACTIONS
|
||||||
],
|
],
|
||||||
prefix: '!!'
|
prefix: '!!'
|
||||||
});
|
});
|
||||||
@@ -76,23 +78,19 @@ Nice to meet you, I'm ${context.self.username}!`
|
|||||||
// Repeated hydration
|
// Repeated hydration
|
||||||
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
||||||
// Note: Testing repeated hydration
|
// Note: Testing repeated hydration
|
||||||
const { message } = await context.hydrate(context, [Expectation.Message])
|
const { message } = await context.hydrate(context, [Expectation.Message]);
|
||||||
const hydrated = await context.hydrate(message, [Expectation.Channel])
|
const { channel } = await context.hydrate(message, [Expectation.Channel]);
|
||||||
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
|
|
||||||
rehydrated.guild.name
|
|
||||||
|
|
||||||
const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
|
const hydrate = await context.hydrator(context, [Expectation.Guild]);
|
||||||
const hasGuild = hydrate(hydrated);
|
const hasGuild = hydrate(context);
|
||||||
|
|
||||||
if (hasGuild) {
|
if (hasGuild) {
|
||||||
// Send a message stating the user that they are in a guild
|
// Send a message stating the user that they are in a guild
|
||||||
await context.reply(`You are ${message.author.username} and this is ${hydrated.guild.name}`, true);
|
await context.reply(`You are ${message.author.username} and this is ${context.guild.name}`, true);
|
||||||
} else {
|
} else {
|
||||||
// Send a message stating the user that they are not in a guild
|
// Send a message stating the user that they are not in a guild
|
||||||
await context.reply(`You are ${message.author.username} and we are not in a server`, true);
|
await context.reply(`You are ${message.author.username} and we are in ${channel.type === ChannelType.DM ? 'our DM\'s 😘' : 'a Group 👀'}`, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hydrator - hydration function generator
|
// Hydrator - hydration function generator
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// 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, Intent } 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({
|
||||||
|
intents: [
|
||||||
|
Intent.GUILDS,
|
||||||
|
Intent.GUILD_MESSAGES,
|
||||||
|
Intent.GUILD_MESSAGE_REACTIONS,
|
||||||
|
Intent.MESSAGE_CONTENT,
|
||||||
|
Intent.DIRECT_MESSAGES,
|
||||||
|
Intent.DIRECT_MESSAGE_REACTIONS,
|
||||||
|
],
|
||||||
|
application_id: DISCORD_APPLICATION_ID,
|
||||||
|
token: DISCORD_CLIENT_TOKEN,
|
||||||
|
prefix: '!!',
|
||||||
|
})
|
||||||
|
.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();
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user