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
.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
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
// 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
const hasGuild = hydrate(context);
const hasGuild = hydrate(hydrated);
if (hasGuild) {
// 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_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": [
"out"
],
"repository": "github:jaronpate/unicord",
"scripts": {
"test": "bun run testing/demo.test.ts",
"test:inspect": "bun --inspect testing/demo.test.ts",
@@ -35,11 +34,5 @@
},
"dependencies": {
"ws": "^8.18.0"
},
"prettier": {
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 4
}
}
+2 -9
View File
@@ -1,11 +1,10 @@
// Base classes
export { Client, Client as unicord } from './services/client';
export { Client } from './services/client';
export { Intent, Trait } from './types/common';
// Consumable types
export type { ClientConfig } from './types/common';
export type { Context } from './types/context';
export { Expectation } from './types/common';
export { ChannelType, ChannelFlags, VideoQualityMode, SortOrderType, ForumLayoutType } from './types/channel';
// Consumable util classes
export { Guild } from './types/guild';
export { Role } from './types/role';
@@ -15,11 +14,5 @@ export { Message, type MessagePayload, ComponentStyle, ComponentType } from './t
export { Embed } from './types/embed';
export { Emoji } from './types/emoji';
// Other
export {
createCommandHandler,
type InteractionData,
type InteractionCommpoentData,
type InteractionCommandData,
type InteractionHandlerFunction,
} from './types/handler';
export { createCommandHandler, type InteractionData, type InteractionCommpoentData, type InteractionCommandData, type InteractionHandlerFunction } from './types/handler';
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;
export class Emitter extends EventEmitter {
emit(event_name: string, context: Context | null, ...args: any[]): boolean {
return super.emit(event_name, context, ...args);
}
on(event_name: string, listener: (context: Context | null, ...args: any[]) => void): this {
return super.on(event_name, listener);
}
export type Emitter = EventEmitter & {
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean
on: (event_name: string, listener: Function) => Emitter
}
export { EventBus };
+15 -108
View File
@@ -1,22 +1,15 @@
import { fromDiscord } from '../../types/common';
import type { Context } from '../../types/context';
import type { API } from '../api';
import type { Processor } from '../processor';
import { fromDiscord } from "../../types/common";
import type { Context } from "../../types/context";
import type { API } from "../api";
import type { Processor } from "../processor";
export class ObjectCache<T, K extends T> {
cache: LRUCache<string, K>;
cache = new Map<string, K>();
requests = new Map<string, Promise<K>>();
readonly type: string;
constructor(
protected api: API,
processor: Processor,
type: string,
private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T),
events: string[],
maxCapacity: number = 10_000,
) {
this.cache = new LRUCache<string, K>(maxCapacity);
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[]) {
this.type = type;
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, factory[fromDiscord](e));
@@ -41,8 +34,8 @@ export class ObjectCache<T, K extends T> {
async fetch(object_id: string, ..._args: any[]): Promise<K> {
if (this.requests.has(object_id)) {
return this.requests.get(object_id)!;
// } else if (this.cache.has(object_id)) {
// return Promise.resolve(this.cache.get(object_id)!);
// } else if (this.cache.has(object_id)) {
// return Promise.resolve(this.cache.get(object_id)!);
} else {
// Dispatch the request
const req = this.api.get(`/${this.type}/${object_id}`).then((data) => {
@@ -55,8 +48,10 @@ export class ObjectCache<T, K extends T> {
});
// 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));
// Remove the request from the cache if it succeeds
req.then(() => this.requests.delete(object_id));
// Remove the request from the cache if it fails
req.catch(() => this.requests.delete(object_id));
// Return the request
return req;
}
@@ -69,96 +64,8 @@ export class ObjectCache<T, K extends T> {
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;
}
list(): Map<string, Promise<T> | T> {
return this.cache;
}
}
-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 { Message, type MessagePayload } from "../../types/message";
import type { API } from "../api";
import type { Client } from "../client";
import type { Processor } from "../processor";
import { ObjectCache } from "./cache";
export class Messages extends ObjectCache<Message, MessagePayload> {
constructor (api: API, processor: Processor) {
super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
constructor (protected client: Client, api: API, processor: Processor) {
super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
}
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
+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 { EventBus, type Emitter } from './bus';
import { Channels } from './caches/channels';
import { Gateway } from './connectors/gateway';
import { Processor } from './processor';
import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
import { exists, isNil } from '../utils/index';
import { type DiscordMessage, Message } from '../types/message';
import { Guilds } from './caches/guilds';
import { Messages } from './caches/messages';
import { Users } from './caches/users';
import { Gateway } from './connectors/gateway';
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
import { Processor } from './processor';
import { EventBus, type Emitter } from './bus';
export class Client {
readonly token: string;
@@ -24,7 +23,6 @@ export class Client {
guilds: Guilds;
users: Users;
messages: Messages;
channels: Channels;
chatCommands: Processor[HandlerType.ChatCommands];
applicationCommands: Processor[HandlerType.ApplicationCommands];
interactions: Processor[HandlerType.Interactions];
@@ -44,8 +42,7 @@ export class Client {
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
this.guilds = new Guilds(this.api, this.processor);
this.users = new Users(this.api, this.processor);
this.messages = new Messages(this.api, this.processor);
this.channels = new Channels(this.api, this.processor);
this.messages = new Messages(this, this.api, this.processor);
// Save references to command and event handlers
this.chatCommands = this.processor[HandlerType.ChatCommands];
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
@@ -95,12 +92,9 @@ export class Client {
*
* @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);
};
}
/**
* 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`.
*/
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);
};
}
sendMessage = async (channel_id: string, message: Message | string) => {
if (typeof message === 'string') {
message = new Message().setContent(message);
}
return Message.fromDiscord(
await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()),
);
};
/**
* Makes a raw HTTP request to the Discord API.
* @param method HTTP method to use for the request (GET, POST, PUT, DELETE).
* @param endpoint The API endpoint to request; can start with a slash or be a full URL.
* @param body Optional body to send with the request, typically used for POST or PUT requests.
* @returns A promise that resolves to the response data of type T.
*/
rawRequest = async <T = Record<string, any>>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
body?: Record<string, any>,
): Promise<T> => {
return this.api.request<T>(method, endpoint, body);
};
return Message.fromDiscord(await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()));
}
// chatCommands = {
// register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
@@ -154,11 +131,11 @@ export class Client {
// 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();
}
+8 -8
View File
@@ -205,15 +205,15 @@ export class Gateway {
// Generate context
const context = new Context(this.client, this.api, { message });
// 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
if (this.processor[HandlerType.ChatCommands].has(command)) {
// Execute the handler
this.processor[HandlerType.ChatCommands].execute(command, context, args);
} else {
// TODO: Custom unknown command handler
// context.reply(`Unknown command: ${command}`);
}
// if (this.processor[HandlerType.ChatCommands].has(command)) {
// // Execute the handler
// this.processor[HandlerType.ChatCommands].execute(command, context, args);
// } else {
// // TODO: Custom unknown command handler
// // context.reply(`Unknown command: ${command}`);
// }
}
};
+9 -20
View File
@@ -1,16 +1,15 @@
import { Expectation } from "../types/common";
import { Context, type HydratedContext } from "../types/context";
import { Message, type HydratedMessage } from "../types/message";
import { Channel } from "../types/channel";
import { exists } from "../utils";
import type { API } from "./api";
import type { Client } from "./client";
export type GatewayObject = Message | Channel;
export type GatewayObject = Message;
export type Hydrateable = Context | GatewayObject
export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
T extends Message ? HydratedMessage<K> & Omit<T, keyof HydratedMessage<K>> :
T extends Context ? HydratedContext<K> & Omit<T, keyof HydratedContext<K>> : never;
T extends Message ? HydratedMessage<K> :
T extends Context ? HydratedContext<K> : never;
/**
* 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.
*/
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,
expectations: K,
client: Client,
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.
// Then clone the object and return the hydrated object.
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)) {
let context = data as unknown as HydratedContext<[Expectation.Guild, ...Expectation[]]>;
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 {
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)) {
let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>;
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 {
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
}
}
}
// Create a new object that preserves all existing properties
const result = { ...data } as any;
// Ensure the result is properly typed with both old and new hydrated properties
return result as T & Hydrated<T, K>;
return data as unknown as (T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : 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`.
*/
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,
expectations: K,
client: Client,
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;
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
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;
}
}
+5 -3
View File
@@ -24,6 +24,7 @@ export class Processor {
const handlers = this.handlers[type].get(e) || [];
handlers.push(handler);
this.handlers[type].set(e, handlers);
this.bus.on(`${type}:${e}`, async (context: Context | null, payload: any) => { this.execute(type, e, context, payload) });
}
};
@@ -50,9 +51,10 @@ export class Processor {
}
};
public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
public async execute(type: HandlerType.Interactions, event: string, context: Context | null, payload: InteractionData): Promise<void>
public async execute(type: Omit<HandlerType, HandlerType.Events | HandlerType.Interactions>, event: string, context: Context, args: any[]): Promise<void>
// TODO: Fix overloads
// public async execute(type: HandlerType.Events, event: string, context: Context | null, payload: EventPayload): Promise<void>
// public async execute(type: HandlerType.Interactions, event: string, context: Context | null, payload: InteractionData): Promise<void>
// public async execute(type: Omit<HandlerType, HandlerType.Events | HandlerType.Interactions>, event: string, context: Context, args: any[]): Promise<void>
public async execute(type: HandlerType, event: string, context: Context | null, argsOrPayload: any[] | EventPayload) {
const handlers = this.handlers[type].get(event);
+24 -263
View File
@@ -1,278 +1,39 @@
import { GatewayObject } from "./base";
import { Trait } from "./common";
import { User, type DiscordUser } from "./user";
export enum ChannelType {
GUILD_TEXT = 0,
DM = 1,
GUILD_VOICE = 2,
GROUP_DM = 3,
GUILD_CATEGORY = 4,
GUILD_ANNOUNCEMENT = 5,
ANNOUNCEMENT_THREAD = 10,
PUBLIC_THREAD = 11,
PRIVATE_THREAD = 12,
GUILD_STAGE_VOICE = 13,
GUILD_DIRECTORY = 14,
GUILD_FORUM = 15,
GUILD_MEDIA = 16
}
export enum ChannelFlags {
PINNED = 1 << 1,
REQUIRE_TAG = 1 << 4,
HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15
}
export enum VideoQualityMode {
AUTO = 1,
FULL = 2
}
export enum SortOrderType {
LATEST_ACTIVITY = 0,
CREATION_DATE = 1
}
export enum ForumLayoutType {
NOT_SET = 0,
LIST_VIEW = 1,
GALLERY_VIEW = 2
}
export type PermissionOverwrite = {
id: string;
type: 0 | 1; // 0 for role, 1 for member
allow: string;
deny: string;
}
export type ThreadMetadata = {
archived: boolean;
auto_archive_duration: number;
archive_timestamp: string;
locked: boolean;
invitable?: boolean;
create_timestamp?: string | null;
}
export type ThreadMember = {
id?: string;
user_id?: string;
join_timestamp: string;
flags: number;
}
export type ForumTag = {
id: string;
name: string;
moderated: boolean;
emoji_id?: string | null;
emoji_name?: string | null;
}
export type DefaultReaction = {
emoji_id?: string | null; // the id of a guild's custom emoji
emoji_name?: string | null; // the unicode character of the emoji
}
export class Channel extends GatewayObject {
id: string;
type: ChannelType;
guild_id?: string;
position?: number;
permission_overwrites?: PermissionOverwrite[];
name?: string;
topic?: string | null;
nsfw?: boolean;
last_message_id?: string | null;
bitrate?: number;
user_limit?: number;
rate_limit_per_user?: number;
recipients?: User[];
icon?: string | null;
owner_id?: string;
application_id?: string;
managed?: boolean;
parent_id?: string | null;
last_pin_timestamp?: Date | null;
rtc_region?: string | null;
video_quality_mode?: VideoQualityMode;
message_count?: number;
member_count?: number;
thread_metadata?: ThreadMetadata;
member?: ThreadMember;
default_auto_archive_duration?: number;
permissions?: string;
flags?: number;
total_message_sent?: number;
available_tags?: ForumTag[];
applied_tags?: string[];
default_reaction_emoji?: DefaultReaction;
default_thread_rate_limit_per_user?: number;
default_sort_order?: SortOrderType | null;
default_forum_layout?: ForumLayoutType;
constructor(data: Partial<Channel>) {
super();
this.id = data.id!;
this.type = data.type!;
this.guild_id = data.guild_id;
this.position = data.position;
this.permission_overwrites = data.permission_overwrites;
this.name = data.name;
this.topic = data.topic;
this.nsfw = data.nsfw;
this.last_message_id = data.last_message_id;
this.bitrate = data.bitrate;
this.user_limit = data.user_limit;
this.rate_limit_per_user = data.rate_limit_per_user;
this.recipients = data.recipients;
this.icon = data.icon;
this.owner_id = data.owner_id;
this.application_id = data.application_id;
this.managed = data.managed;
this.parent_id = data.parent_id;
this.last_pin_timestamp = data.last_pin_timestamp instanceof Date
? data.last_pin_timestamp
: data.last_pin_timestamp
? new Date(data.last_pin_timestamp)
: null;
this.rtc_region = data.rtc_region;
this.video_quality_mode = data.video_quality_mode;
this.message_count = data.message_count;
this.member_count = data.member_count;
this.thread_metadata = data.thread_metadata;
this.member = data.member;
this.default_auto_archive_duration = data.default_auto_archive_duration;
this.permissions = data.permissions;
this.flags = data.flags;
this.total_message_sent = data.total_message_sent;
this.available_tags = data.available_tags;
this.applied_tags = data.applied_tags;
this.default_reaction_emoji = data.default_reaction_emoji;
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
this.default_sort_order = data.default_sort_order;
this.default_forum_layout = data.default_forum_layout;
}
public static [Trait.fromDiscord]<T = Channel>(data: DiscordChannel): T {
return new Channel({
id: data.id,
type: data.type,
guild_id: data.guild_id,
position: data.position,
permission_overwrites: data.permission_overwrites,
name: data.name,
topic: data.topic,
nsfw: data.nsfw,
last_message_id: data.last_message_id,
bitrate: data.bitrate,
user_limit: data.user_limit,
rate_limit_per_user: data.rate_limit_per_user,
recipients: data.recipients?.map(u => User.fromDiscord(u)),
icon: data.icon,
owner_id: data.owner_id,
application_id: data.application_id,
managed: data.managed,
parent_id: data.parent_id,
last_pin_timestamp: data.last_pin_timestamp ? new Date(data.last_pin_timestamp) : null,
rtc_region: data.rtc_region,
video_quality_mode: data.video_quality_mode,
message_count: data.message_count,
member_count: data.member_count,
thread_metadata: data.thread_metadata,
member: data.member,
default_auto_archive_duration: data.default_auto_archive_duration,
permissions: data.permissions,
flags: data.flags,
total_message_sent: data.total_message_sent,
available_tags: data.available_tags,
applied_tags: data.applied_tags,
default_reaction_emoji: data.default_reaction_emoji,
default_thread_rate_limit_per_user: data.default_thread_rate_limit_per_user,
default_sort_order: data.default_sort_order,
default_forum_layout: data.default_forum_layout
}) as T;
}
}
export class DiscordChannel {
id: string;
export class Channel {
version: number;
type: number;
guild_id?: string;
position?: number;
permission_overwrites?: PermissionOverwrite[];
name?: string;
position: number;
permission_overwrites: any[];
name: string;
id: string;
flags: number;
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;
parent_id?: string;
last_message_id?: string | null;
user_limit?: number;
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;
bitrate?: number;
last_pin_timestamp?: string;
constructor(data: any) {
this.id = data.id;
this.version = data.version;
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.id = data.id;
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;
this.topic = data.topic;
this.rate_limit_per_user = data.rate_limit_per_user;
this.parent_id = data.parent_id;
this.last_message_id = data.last_message_id;
this.user_limit = data.user_limit;
this.rtc_region = data.rtc_region;
this.bitrate = data.bitrate;
this.last_pin_timestamp = data.last_pin_timestamp;
}
static fromAPIResponse(data: any): DiscordChannel {
return new DiscordChannel(data);
static fromAPIResponse(data: any): Channel {
return new Channel(data);
}
}
+100 -181
View File
@@ -3,7 +3,7 @@ export type ClientConfig = {
application_id: string;
prefix?: string;
intents?: Intent[];
};
}
export enum Expectation {
Guild = 'guild',
@@ -16,26 +16,24 @@ export enum HandlerType {
Events = 'events',
ChatCommands = 'chat_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 fromDiscord: unique symbol = Symbol('fromDiscord');
export const execute: unique symbol = Symbol('execute');
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: typeof clone
fromDiscord: typeof fromDiscord
execute: typeof execute
} = {
clone,
fromDiscord,
execute,
};
execute
}
export type EventPayload = any;
@@ -60,7 +58,7 @@ export type Payload = {
export enum InteractionResponseType {
Pong = 1,
Message = 4,
DeferredMessage = 5,
DeferredMessage = 5
}
export enum Intent {
@@ -81,189 +79,110 @@ export enum Intent {
DIRECT_MESSAGE_TYPING = 1 << 14,
MESSAGE_CONTENT = 1 << 15,
DEFAULT = 513,
ALL = 32767,
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.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_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_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]: [
['GUILD_INTEGRATIONS_UPDATE', 'INTEGRATION_CREATE', 'INTEGRATION_UPDATE', 'INTEGRATION_DELETE'],
4,
],
[Intent.GUILD_INTEGRATIONS]: [[
"GUILD_INTEGRATIONS_UPDATE",
"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]: [
[
'MESSAGE_REACTION_ADD',
'MESSAGE_REACTION_REMOVE',
'MESSAGE_REACTION_REMOVE_ALL',
'MESSAGE_REACTION_REMOVE_EMOJI',
],
10,
],
[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.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]: [
[
'MESSAGE_REACTION_ADD',
'MESSAGE_REACTION_REMOVE',
'MESSAGE_REACTION_REMOVE_ALL',
'MESSAGE_REACTION_REMOVE_EMOJI',
],
13,
],
[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.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',
[Intent.MESSAGE_CONTENT]: [[
"MESSAGE_CONTENT"
], 15]
}
+1 -3
View File
@@ -4,7 +4,6 @@ import { hydrate, hydrator, type Hydrateable, type Hydrated } from "../services/
import { isNil, exists } from "../utils";
import { fromDiscord, InteractionResponseType, type Expectation } from "./common";
import type { Guild } from "./guild";
import { Channel } from "./channel";
import type { InteractionPayload } from "./handler";
import { type DiscordMessage, Message, type MessagePayload } from "./message";
/**
@@ -29,7 +28,6 @@ import { type DiscordMessage, Message, type MessagePayload } from "./message";
export type HydratedContext<T extends Array<Expectation>> = Context & {
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
message: Extract<Expectation.Message, T[number]> extends never ? undefined : Message;
channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
};
export type ContextData =
@@ -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`.
*/
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);
}
+3 -9
View File
@@ -1,10 +1,9 @@
import { exists, isNil } from '../utils';
import { GatewayObject } from './base';
import type { Channel } from './channel';
import { fromDiscord, Trait, type Expectation } from './common';
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
import type { Embed } from './embed';
import type { Guild } from './guild';
import { User, type DiscordUser } from './user';
import { type DiscordUser, User } from './user';
export type Component = Partial<Button | SelectMenu> & {
type: ComponentType;
@@ -35,11 +34,6 @@ export type Component = Partial<Button | SelectMenu> & {
// components?: Component[];
// }
export type DefaultReaction = {
emoji_id?: string | null; // the id of a guild's custom emoji
emoji_name?: string | null; // the unicode character of the emoji
};
export type Emoji = {
name: string | null;
id: string | null;
@@ -89,7 +83,7 @@ export enum ComponentType {
}
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;
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({
token: process.env.BOT_TOKEN!,
@@ -7,9 +7,7 @@ const client = new Client({
Intent.GUILDS,
Intent.GUILD_MESSAGES,
Intent.GUILD_MESSAGE_REACTIONS,
Intent.MESSAGE_CONTENT,
Intent.DIRECT_MESSAGES,
Intent.DIRECT_MESSAGE_REACTIONS
Intent.MESSAGE_CONTENT
],
prefix: '!!'
});
@@ -78,19 +76,23 @@ Nice to meet you, I'm ${context.self.username}!`
// Repeated hydration
client.chatCommands.register('me', async (context: Context, _args: any[]) => {
// Note: Testing repeated hydration
const { message } = await context.hydrate(context, [Expectation.Message]);
const { channel } = await context.hydrate(message, [Expectation.Channel]);
const { message } = await context.hydrate(context, [Expectation.Message])
const hydrated = await context.hydrate(message, [Expectation.Channel])
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
rehydrated.guild.name
const hydrate = await context.hydrator(context, [Expectation.Guild]);
const hasGuild = hydrate(context);
const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
const hasGuild = hydrate(hydrated);
if (hasGuild) {
// Send a message stating the user that they are in a guild
await context.reply(`You are ${message.author.username} and this is ${context.guild.name}`, true);
await context.reply(`You are ${message.author.username} and this is ${hydrated.guild.name}`, true);
} else {
// Send a message stating the user that they are not in a guild
await context.reply(`You are ${message.author.username} and we are 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
-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"]
}