1 Commits

Author SHA1 Message Date
jaron d8b85e60ec Moving to an event bus strategy for commands 2025-02-05 20:43:43 -06:00
34 changed files with 220 additions and 3857 deletions
-1
View File
@@ -173,4 +173,3 @@ dist
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
.aider*
+2 -45
View File
@@ -112,9 +112,9 @@ You can also generate a hydration function that returns a type narrowing boolean
```typescript ```typescript
client.chatCommands.register('me', async (context: Context, _args: any[]) => { client.chatCommands.register('me', async (context: Context, _args: any[]) => {
// Hydrate the message id from the context // Hydrate the message id from the context
const hydrate = await context.hydrator(context, [Expectation.Guild]); const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
// Check if the object is hydrated // Check if the object is hydrated
const hasGuild = hydrate(context); const hasGuild = hydrate(hydrated);
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
@@ -181,46 +181,3 @@ 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: 1.7 MiB

After

Width:  |  Height:  |  Size: 1012 KiB

-7
View File
@@ -9,7 +9,6 @@
"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",
@@ -35,11 +34,5 @@
}, },
"dependencies": { "dependencies": {
"ws": "^8.18.0" "ws": "^8.18.0"
},
"prettier": {
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 4
} }
} }
+2 -9
View File
@@ -1,11 +1,10 @@
// Base classes // Base classes
export { Client, Client as unicord } from './services/client'; export { Client } 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';
@@ -15,11 +14,5 @@ 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 { export { createCommandHandler, type InteractionData, type InteractionCommpoentData, type InteractionCommandData, type InteractionHandlerFunction } from './types/handler';
createCommandHandler,
type InteractionData,
type InteractionCommpoentData,
type InteractionCommandData,
type InteractionHandlerFunction,
} from './types/handler';
export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand'; export { ApplicationCommandType, ApplicationCommandOptionType as CommandOptionType } from './types/applicationCommand';
+3 -7
View File
@@ -3,13 +3,9 @@ import type { Context } from '../types/context';
const EventBus = new EventEmitter() as Emitter; const EventBus = new EventEmitter() as Emitter;
export class Emitter extends EventEmitter { export type Emitter = 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: Function) => Emitter
}
on(event_name: string, listener: (context: Context | null, ...args: any[]) => void): this {
return super.on(event_name, listener);
}
} }
export { EventBus }; export { EventBus };
+13 -106
View File
@@ -1,22 +1,15 @@
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: LRUCache<string, K>; cache = new Map<string, K>();
requests = new Map<string, Promise<K>>(); requests = new Map<string, Promise<K>>();
readonly type: string; readonly type: string;
constructor( constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[]) {
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));
@@ -55,8 +48,10 @@ 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 when it finishes // Remove the request from the cache if it succeeds
req.finally(() => this.requests.delete(object_id)); req.then(() => this.requests.delete(object_id));
// Remove the request from the cache if it fails
req.catch(() => this.requests.delete(object_id));
// Return the request // Return the request
return req; return req;
} }
@@ -69,96 +64,8 @@ export class ObjectCache<T, K extends T> {
return this.fetch(object_id); return this.fetch(object_id);
} }
} }
}
class ListNode<K, V> { list(): Map<string, Promise<T> | T> {
key: K; return this.cache;
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;
}
} }
} }
-10
View File
@@ -1,10 +0,0 @@
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']);
}
}
+3 -2
View File
@@ -1,13 +1,14 @@
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 (api: API, processor: Processor) { constructor (protected client: Client, api: API, processor: Processor) {
super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); super(api, processor, 'message', 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> {
+16 -39
View File
@@ -1,15 +1,14 @@
import { type DiscordAPIEvent, Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
import { Message, type DiscordMessage } from '../types/message';
import { exists, isNil } from '../utils/index';
import { API } from './api'; import { API } from './api';
import { EventBus, type Emitter } from './bus'; import { Gateway } from './connectors/gateway';
import { Channels } from './caches/channels'; import { Processor } from './processor';
import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
import { exists, isNil } from '../utils/index';
import { type DiscordMessage, Message } from '../types/message';
import { Guilds } from './caches/guilds'; import { 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 { Processor } from './processor'; import { EventBus, type Emitter } from './bus';
export class Client { export class Client {
readonly token: string; readonly token: string;
@@ -24,7 +23,6 @@ export class Client {
guilds: Guilds; guilds: Guilds;
users: Users; users: Users;
messages: Messages; messages: Messages;
channels: Channels;
chatCommands: Processor[HandlerType.ChatCommands]; chatCommands: Processor[HandlerType.ChatCommands];
applicationCommands: Processor[HandlerType.ApplicationCommands]; applicationCommands: Processor[HandlerType.ApplicationCommands];
interactions: Processor[HandlerType.Interactions]; interactions: Processor[HandlerType.Interactions];
@@ -44,8 +42,7 @@ export class Client {
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); this.guilds = new Guilds(this.api, this.processor);
this.users = new Users(this.api, this.processor); this.users = new Users(this.api, this.processor);
this.messages = new Messages(this.api, this.processor); this.messages = new Messages(this, this.api, this.processor);
this.channels = new Channels(this.api, this.processor);
// Save references to command and event handlers // Save references to command and event handlers
this.chatCommands = this.processor[HandlerType.ChatCommands]; this.chatCommands = this.processor[HandlerType.ChatCommands];
this.applicationCommands = this.processor[HandlerType.ApplicationCommands]; this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
@@ -95,12 +92,9 @@ export class Client {
* *
* @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>>( hydrate = async <T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>, K extends Array<Expectation>>(data: T, expectations: K) => {
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.
@@ -113,34 +107,17 @@ export class Client {
* *
* @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( return Message.fromDiscord(await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()));
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),
@@ -154,11 +131,11 @@ export class Client {
// has: (name: string) => this.processor.application_commands.has(name) // has: (name: string) => this.processor.application_commands.has(name)
// } // }
on = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.on(event, handler); on = (event: string, handler: (...args: any[]) => void) => this.bus.on(event, handler);
once = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.once(event, handler); once = (event: string, handler: (...args: any[]) => void) => this.bus.once(event, handler);
off = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.off(event, handler); off = (event: string, handler: (...args: any[]) => void) => this.bus.off(event, handler);
connect = () => this.gateway.connect(); connect = () => this.gateway.connect();
} }
+8 -8
View File
@@ -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(`${HandlerType.ChatCommands}:${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}`);
} // }
} }
}; };
+9 -20
View File
@@ -1,16 +1,15 @@
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 | Channel; export type GatewayObject = Message;
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> & Omit<T, keyof HydratedMessage<K>> : T extends Message ? HydratedMessage<K> :
T extends Context ? HydratedContext<K> & Omit<T, keyof HydratedContext<K>> : never; T extends Context ? HydratedContext<K> : never;
/** /**
* Hydrates a given data object based on the provided expectations. * Hydrates a given data object based on the provided expectations.
@@ -27,12 +26,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, K extends Array<Expectation>>( export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
data: T, data: T,
expectations: K, expectations: K,
client: Client, client: Client,
api: API api: API
): Promise<Hydrated<T, K>> { ): Promise<T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
// TODO: add clone trait. Make it a requirement for hydrateable objects. // 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>;
@@ -45,9 +44,6 @@ export async function hydrate<T extends Hydrateable, K extends Array<Expectation
} 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.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.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}`);
} }
@@ -60,20 +56,13 @@ export async function hydrate<T extends Hydrateable, K extends Array<Expectation
} 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.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.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}`);
} }
} }
} }
// Create a new object that preserves all existing properties return data as unknown as (T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>);
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>;
} }
/** /**
@@ -89,12 +78,12 @@ export async function hydrate<T extends Hydrateable, K extends Array<Expectation
* *
* @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, K extends Array<Expectation>>( export async function hydrator<T extends Hydrateable | Hydrated<any, any>, 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 Hydrated<T, K>> { ): Promise<(data: unknown) => data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
let hydrated = true; let hydrated = true;
try { try {
@@ -106,7 +95,7 @@ export async function hydrator<T extends Hydrateable, K extends Array<Expectatio
} }
// 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 Hydrated<T, K> { return function (data: unknown): data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K> {
return hydrated; return hydrated;
} }
} }
+5 -3
View File
@@ -24,6 +24,7 @@ export class Processor {
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);
this.bus.on(`${type}:${e}`, async (context: Context | null, payload: any) => { this.execute(type, e, context, payload) });
} }
}; };
@@ -50,9 +51,10 @@ export class Processor {
} }
}; };
public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void> // TODO: Fix overloads
public async execute(type: HandlerType.Interactions, event: string, context: Context | null, payload: InteractionData): Promise<void> // public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
public async execute(type: Omit<HandlerType, HandlerType.Events | HandlerType.Interactions>, event: string, context: Context, args: any[]): Promise<void> // public async execute(type: HandlerType.Interactions, event: string, context: Context | null, payload: InteractionData): Promise<void>
// public async execute(type: Omit<HandlerType, HandlerType.Events | HandlerType.Interactions>, event: string, context: Context, args: any[]): Promise<void>
public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) { public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
const handlers = this.handlers[type].get(event); const handlers = this.handlers[type].get(event);
+24 -263
View File
@@ -1,278 +1,39 @@
import { GatewayObject } from "./base"; export class Channel {
import { Trait } from "./common"; version: number;
import { User, type DiscordUser } from "./user";
export enum ChannelType {
GUILD_TEXT = 0,
DM = 1,
GUILD_VOICE = 2,
GROUP_DM = 3,
GUILD_CATEGORY = 4,
GUILD_ANNOUNCEMENT = 5,
ANNOUNCEMENT_THREAD = 10,
PUBLIC_THREAD = 11,
PRIVATE_THREAD = 12,
GUILD_STAGE_VOICE = 13,
GUILD_DIRECTORY = 14,
GUILD_FORUM = 15,
GUILD_MEDIA = 16
}
export enum ChannelFlags {
PINNED = 1 << 1,
REQUIRE_TAG = 1 << 4,
HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15
}
export enum VideoQualityMode {
AUTO = 1,
FULL = 2
}
export enum SortOrderType {
LATEST_ACTIVITY = 0,
CREATION_DATE = 1
}
export enum ForumLayoutType {
NOT_SET = 0,
LIST_VIEW = 1,
GALLERY_VIEW = 2
}
export type PermissionOverwrite = {
id: string;
type: 0 | 1; // 0 for role, 1 for member
allow: string;
deny: string;
}
export type ThreadMetadata = {
archived: boolean;
auto_archive_duration: number;
archive_timestamp: string;
locked: boolean;
invitable?: boolean;
create_timestamp?: string | null;
}
export type ThreadMember = {
id?: string;
user_id?: string;
join_timestamp: string;
flags: number;
}
export type ForumTag = {
id: string;
name: string;
moderated: boolean;
emoji_id?: string | null;
emoji_name?: string | null;
}
export type DefaultReaction = {
emoji_id?: string | null; // the id of a guild's custom emoji
emoji_name?: string | null; // the unicode character of the emoji
}
export class Channel extends GatewayObject {
id: string;
type: ChannelType;
guild_id?: string;
position?: number;
permission_overwrites?: PermissionOverwrite[];
name?: string;
topic?: string | null;
nsfw?: boolean;
last_message_id?: string | null;
bitrate?: number;
user_limit?: number;
rate_limit_per_user?: number;
recipients?: User[];
icon?: string | null;
owner_id?: string;
application_id?: string;
managed?: boolean;
parent_id?: string | null;
last_pin_timestamp?: Date | null;
rtc_region?: string | null;
video_quality_mode?: VideoQualityMode;
message_count?: number;
member_count?: number;
thread_metadata?: ThreadMetadata;
member?: ThreadMember;
default_auto_archive_duration?: number;
permissions?: string;
flags?: number;
total_message_sent?: number;
available_tags?: ForumTag[];
applied_tags?: string[];
default_reaction_emoji?: DefaultReaction;
default_thread_rate_limit_per_user?: number;
default_sort_order?: SortOrderType | null;
default_forum_layout?: ForumLayoutType;
constructor(data: Partial<Channel>) {
super();
this.id = data.id!;
this.type = data.type!;
this.guild_id = data.guild_id;
this.position = data.position;
this.permission_overwrites = data.permission_overwrites;
this.name = data.name;
this.topic = data.topic;
this.nsfw = data.nsfw;
this.last_message_id = data.last_message_id;
this.bitrate = data.bitrate;
this.user_limit = data.user_limit;
this.rate_limit_per_user = data.rate_limit_per_user;
this.recipients = data.recipients;
this.icon = data.icon;
this.owner_id = data.owner_id;
this.application_id = data.application_id;
this.managed = data.managed;
this.parent_id = data.parent_id;
this.last_pin_timestamp = data.last_pin_timestamp instanceof Date
? data.last_pin_timestamp
: data.last_pin_timestamp
? new Date(data.last_pin_timestamp)
: null;
this.rtc_region = data.rtc_region;
this.video_quality_mode = data.video_quality_mode;
this.message_count = data.message_count;
this.member_count = data.member_count;
this.thread_metadata = data.thread_metadata;
this.member = data.member;
this.default_auto_archive_duration = data.default_auto_archive_duration;
this.permissions = data.permissions;
this.flags = data.flags;
this.total_message_sent = data.total_message_sent;
this.available_tags = data.available_tags;
this.applied_tags = data.applied_tags;
this.default_reaction_emoji = data.default_reaction_emoji;
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
this.default_sort_order = data.default_sort_order;
this.default_forum_layout = data.default_forum_layout;
}
public static [Trait.fromDiscord]<T = Channel>(data: DiscordChannel): T {
return new Channel({
id: data.id,
type: data.type,
guild_id: data.guild_id,
position: data.position,
permission_overwrites: data.permission_overwrites,
name: data.name,
topic: data.topic,
nsfw: data.nsfw,
last_message_id: data.last_message_id,
bitrate: data.bitrate,
user_limit: data.user_limit,
rate_limit_per_user: data.rate_limit_per_user,
recipients: data.recipients?.map(u => User.fromDiscord(u)),
icon: data.icon,
owner_id: data.owner_id,
application_id: data.application_id,
managed: data.managed,
parent_id: data.parent_id,
last_pin_timestamp: data.last_pin_timestamp ? new Date(data.last_pin_timestamp) : null,
rtc_region: data.rtc_region,
video_quality_mode: data.video_quality_mode,
message_count: data.message_count,
member_count: data.member_count,
thread_metadata: data.thread_metadata,
member: data.member,
default_auto_archive_duration: data.default_auto_archive_duration,
permissions: data.permissions,
flags: data.flags,
total_message_sent: data.total_message_sent,
available_tags: data.available_tags,
applied_tags: data.applied_tags,
default_reaction_emoji: data.default_reaction_emoji,
default_thread_rate_limit_per_user: data.default_thread_rate_limit_per_user,
default_sort_order: data.default_sort_order,
default_forum_layout: data.default_forum_layout
}) as T;
}
}
export class DiscordChannel {
id: string;
type: number; type: number;
guild_id?: string; position: number;
position?: number; permission_overwrites: any[];
permission_overwrites?: PermissionOverwrite[]; name: string;
name?: string; id: string;
flags: number;
topic?: string | null; topic?: string | null;
nsfw?: boolean;
last_message_id?: string | null;
bitrate?: number;
user_limit?: number;
rate_limit_per_user?: number; rate_limit_per_user?: number;
recipients?: DiscordUser[]; parent_id?: string;
icon?: string | null; last_message_id?: string | null;
owner_id?: string; user_limit?: number;
application_id?: string;
managed?: boolean;
parent_id?: string | null;
last_pin_timestamp?: string | null;
rtc_region?: string | null; rtc_region?: string | null;
video_quality_mode?: number; bitrate?: number;
message_count?: number; last_pin_timestamp?: string;
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) { constructor(data: any) {
this.id = data.id; this.version = data.version;
this.type = data.type; 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.topic = data.topic; this.id = data.id;
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.flags = data.flags;
this.total_message_sent = data.total_message_sent; this.topic = data.topic;
this.available_tags = data.available_tags; this.rate_limit_per_user = data.rate_limit_per_user;
this.applied_tags = data.applied_tags; this.parent_id = data.parent_id;
this.default_reaction_emoji = data.default_reaction_emoji; this.last_message_id = data.last_message_id;
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user; this.user_limit = data.user_limit;
this.default_sort_order = data.default_sort_order; this.rtc_region = data.rtc_region;
this.default_forum_layout = data.default_forum_layout; this.bitrate = data.bitrate;
this.last_pin_timestamp = data.last_pin_timestamp;
} }
static fromAPIResponse(data: any): DiscordChannel { static fromAPIResponse(data: any): Channel {
return new DiscordChannel(data); return new Channel(data);
} }
} }
+100 -181
View File
@@ -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,26 +16,24 @@ 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 export type Expects<T extends Array<Expectation>, K extends Expectation> = Extract<K, T[number]> extends never ? false : true;
? 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;
@@ -60,7 +58,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 {
@@ -81,189 +79,110 @@ 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_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', "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', ], 0],
],
0,
],
[Intent.GUILD_MEMBERS]: [ [Intent.GUILD_MEMBERS]: [[
['GUILD_MEMBER_ADD', 'GUILD_MEMBER_UPDATE', 'GUILD_MEMBER_REMOVE', 'THREAD_MEMBERS_UPDATE *'], "GUILD_MEMBER_ADD",
1, "GUILD_MEMBER_UPDATE",
], "GUILD_MEMBER_REMOVE",
"THREAD_MEMBERS_UPDATE *",
], 1],
[Intent.GUILD_BANS]: [['GUILD_BAN_ADD', 'GUILD_BAN_REMOVE'], 2], [Intent.GUILD_BANS]: [[
"GUILD_BAN_ADD",
"GUILD_BAN_REMOVE",
], 2],
[Intent.GUILD_EMOJIS_AND_STICKERS]: [['GUILD_EMOJIS_UPDATE', 'GUILD_STICKERS_UPDATE'], 3], [Intent.GUILD_EMOJIS_AND_STICKERS]: [[
"GUILD_EMOJIS_UPDATE",
"GUILD_STICKERS_UPDATE",
], 3],
[Intent.GUILD_INTEGRATIONS]: [ [Intent.GUILD_INTEGRATIONS]: [[
['GUILD_INTEGRATIONS_UPDATE', 'INTEGRATION_CREATE', 'INTEGRATION_UPDATE', 'INTEGRATION_DELETE'], "GUILD_INTEGRATIONS_UPDATE",
4, "INTEGRATION_CREATE",
], "INTEGRATION_UPDATE",
"INTEGRATION_DELETE",
], 4],
[Intent.GUILD_WEBHOOKS]: [['WEBHOOKS_UPDATE'], 5], [Intent.GUILD_WEBHOOKS]: [[
"WEBHOOKS_UPDATE"
], 5],
[Intent.GUILD_INVITES]: [['INVITE_CREATE', 'INVITE_DELETE'], 6], [Intent.GUILD_INVITES]: [[
"INVITE_CREATE",
"INVITE_DELETE",
], 6],
[Intent.GUILD_VOICE_STATES]: [['VOICE_STATE_UPDATE'], 7], [Intent.GUILD_VOICE_STATES]: [[
"VOICE_STATE_UPDATE"
], 7],
[Intent.GUILD_PRESENCES]: [['PRESENCE_UPDATE'], 8], [Intent.GUILD_PRESENCES]: [[
"PRESENCE_UPDATE"
], 8],
[Intent.GUILD_MESSAGES]: [['MESSAGE_CREATE', 'MESSAGE_UPDATE', 'MESSAGE_DELETE', 'MESSAGE_DELETE_BULK'], 9], [Intent.GUILD_MESSAGES]: [[
"MESSAGE_CREATE",
"MESSAGE_UPDATE",
"MESSAGE_DELETE",
"MESSAGE_DELETE_BULK",
], 9],
[Intent.GUILD_MESSAGE_REACTIONS]: [ [Intent.GUILD_MESSAGE_REACTIONS]: [[
[ "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', ], 10],
],
10,
],
[Intent.GUILD_MESSAGE_TYPING]: [['TYPING_START'], 11], [Intent.GUILD_MESSAGE_TYPING]: [[
"TYPING_START"
], 11],
[Intent.DIRECT_MESSAGES]: [['MESSAGE_CREATE', 'MESSAGE_UPDATE', 'MESSAGE_DELETE', 'CHANNEL_PINS_UPDATE'], 12], [Intent.DIRECT_MESSAGES]: [[
"MESSAGE_CREATE",
"MESSAGE_UPDATE",
"MESSAGE_DELETE",
"CHANNEL_PINS_UPDATE",
], 12],
[Intent.DIRECT_MESSAGE_REACTIONS]: [ [Intent.DIRECT_MESSAGE_REACTIONS]: [[
[ "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', ], 13],
],
13,
],
[Intent.DIRECT_MESSAGE_TYPING]: [['TYPING_START'], 14], [Intent.DIRECT_MESSAGE_TYPING]: [[
"TYPING_START"
], 14],
[Intent.MESSAGE_CONTENT]: [['MESSAGE_CONTENT'], 15], [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',
} }
+1 -3
View File
@@ -4,7 +4,6 @@ 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";
/** /**
@@ -29,7 +28,6 @@ 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 =
@@ -94,7 +92,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, K extends Array<Expectation>>(data: T, expectations: K) => { public hydrator = async <T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>, K extends Array<Expectation>>(data: T, expectations: K) => {
return hydrator<T, K>(data, expectations, this.client, this.api); return hydrator<T, K>(data, expectations, this.client, this.api);
} }
+3 -9
View File
@@ -1,10 +1,9 @@
import { exists, isNil } from '../utils'; import { exists, isNil } from '../utils';
import { GatewayObject } from './base'; import { GatewayObject } from './base';
import type { Channel } from './channel'; import { fromDiscord, Trait, type Expectation, type Expects } from './common';
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 { User, type DiscordUser } from './user'; import { type DiscordUser, User } from './user';
export type Component = Partial<Button | SelectMenu> & { export type Component = Partial<Button | SelectMenu> & {
type: ComponentType; type: ComponentType;
@@ -35,11 +34,6 @@ 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;
@@ -89,7 +83,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;
} }
+12 -10
View File
@@ -1,4 +1,4 @@
import { ApplicationCommandType, ChannelType, Client, CommandOptionType, ComponentStyle, type Context, createCommandHandler, Expectation, Intent, InteractionCommpoentData, type InteractionData, Message, type MessagePayload } from '../src/index'; import { ApplicationCommandType, 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,9 +7,7 @@ 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: '!!'
}); });
@@ -78,19 +76,23 @@ 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 { channel } = await context.hydrate(message, [Expectation.Channel]); const hydrated = await context.hydrate(message, [Expectation.Channel])
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
rehydrated.guild.name
const hydrate = await context.hydrator(context, [Expectation.Guild]); const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
const hasGuild = hydrate(context); const hasGuild = hydrate(hydrated);
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 ${context.guild.name}`, true); await context.reply(`You are ${message.author.username} and this is ${hydrated.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 in ${channel.type === ChannelType.DM ? 'our DM\'s 😘' : 'a Group 👀'}`, true); await context.reply(`You are ${message.author.username} and we are not in a server`, true);
} }
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
}); });
// Hydrator - hydration function generator // Hydrator - hydration function generator
-13
View File
@@ -1,13 +0,0 @@
{
"folders": [
{
"name": "unicord",
"path": "."
},
{
"name": "quotes-bot",
"path": "../quotes-bot"
}
],
"settings": {}
}
-36
View File
@@ -1,36 +0,0 @@
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.
-5
View File
@@ -1,5 +0,0 @@
/// <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.
-8
View File
@@ -1,8 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'export'
};
export default nextConfig;
-2057
View File
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 1.7 MiB

-31
View File
@@ -1,31 +0,0 @@
: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.

Before

Width:  |  Height:  |  Size: 1012 KiB

-45
View File
@@ -1,45 +0,0 @@
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>
);
}
-499
View File
@@ -1,499 +0,0 @@
.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 6em;
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 6em;
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; */
min-width: 400px;
}
.hydrationText h2 {
color: #ff0080;
margin: 0 0 1.5rem 0;
font-size: 2rem;
min-width: 400px;
}
.hydrationText p {
color: #fff;
opacity: 0.8;
margin: 1rem 0;
font-size: 1.1rem;
line-height: 1.6;
min-width: 400px;
}
.typeSection {
padding: 6rem 6em;
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;
min-width: 400px;
}
.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 6em;
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;
}
}
-174
View File
@@ -1,174 +0,0 @@
'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>
);
}
@@ -1,13 +0,0 @@
.editorWrapper {
position: relative;
width: 100%;
height: 100%;
}
.editorContainer {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
border: 1px solid #2d2d2d;
}
-154
View File
@@ -1,154 +0,0 @@
'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);
}
});`;
-27
View File
@@ -1,27 +0,0 @@
: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;
}
-27
View File
@@ -1,27 +0,0 @@
{
"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"]
}