61 Commits

Author SHA1 Message Date
jaron cef02241d6 fix typo in README example 2026-06-21 15:27:56 -05:00
jaron 565eece049 fixing some website css 2025-10-14 09:44:53 -05:00
jaron 012a08cb9e style: Adjust hero section padding and alignment for improved layout 2025-10-14 00:40:39 -05:00
jaron 3da661743c Merge pull request #6 from jaronpate/ft/website
ft/website — getting out of hand
2025-10-14 00:01:26 -05:00
jaron 7b07c9561c Update website/src/app/page.module.css
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 00:00:59 -05:00
jaron 5e2ff898c0 Update website/src/components/CodeExample.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 00:00:15 -05:00
jaron fb5509cf79 feat: Add Prettier configuration and improve code formatting across multiple files 2025-10-13 23:53:20 -05:00
jaron ff6ef6930e Add new image to website 2025-08-17 15:54:03 -05:00
jaron 91ed529f28 Website tweaks and new logo 2025-08-17 15:37:43 -05:00
jaron 3a6c3acaef Fixed some metadata and things 2025-02-23 02:28:41 -06:00
jaron db30e0d0ec Final prototype of website 2025-02-23 01:08:05 -06:00
jaron 25f7baa1d5 feat: Add navigation links to header with responsive styling 2025-02-23 01:02:01 -06:00
jaron b33c32c0d8 fix: Resolve TypeScript type error in package manager command copying 2025-02-23 01:01:59 -06:00
jaron 969c4a916e refactor: Migrate component styles to page.module.css
This commit updates the components to use styles from page.module.css instead of separate module CSS files, simplifying the styling approach and reducing the number of CSS files in the project.

Key changes:
- Removed separate module CSS files for components
- Updated import statements to use page.module.css
- Adjusted class names to match existing styles in page.module.css
- Maintained component functionality and structure
2025-02-23 00:46:09 -06:00
jaron 2e6ab53d34 Small style change 2025-02-23 00:43:51 -06:00
jaron 6eefe763e3 refactor: Update Section and PackageManager components for improved type safety and consistency
This commit includes the following changes:
- Refactored PackageManager to use a more type-safe approach
- Updated Section component to use semantic `<section>` tag
- Adjusted background prop types and default values
- Simplified type definitions
- Improved code readability and maintainability
2025-02-23 00:42:07 -06:00
jaron c200bff8d0 refactor: Create shared components and global styles for improved code organization 2025-02-23 00:38:48 -06:00
jaron bd8b7c4fd8 refactor: Remove redundant install section from page 2025-02-23 00:36:00 -06:00
jaron f58c14d51c feat: Relocate and style install section with wordmark title 2025-02-23 00:35:49 -06:00
jaron f6115b0d17 feat: Add interactive package manager installation section with tabs and copy functionality 2025-02-23 00:32:17 -06:00
jaron 934db0c285 Tweaked type section 2025-02-23 00:27:31 -06:00
jaron ddfcfa798a feat: Add type coercion section with code example and styling 2025-02-23 00:24:20 -06:00
jaron aab2f7a546 More tweaks 2025-02-23 00:21:53 -06:00
jaron 26e9a7addf Fix scrolling over code examples 2025-02-22 23:56:47 -06:00
jaron 9f1498b3ad feat: Prevent scroll capture in Monaco Editor with wrapper and CSS 2025-02-22 23:53:20 -06:00
jaron e080d368cc added hydration section 2025-02-22 23:50:08 -06:00
jaron 6338f3ab0d feat: Add reusable CodeExample component with hydration section 2025-02-22 23:32:23 -06:00
jaron 72f05e762a style: Format CSS with consistent indentation and spacing 2025-02-22 23:32:21 -06:00
jaron d169adbf3c feat: Add three-pillar section with responsive design and hover effects 2025-02-22 23:26:41 -06:00
jaron 6c7fc895f8 Monaco code example working 2025-02-22 23:24:31 -06:00
jaron ae4bc3e0a9 feat: Adjust code example styling for dynamic height and responsive layout 2025-02-22 23:11:34 -06:00
jaron 713761386c feat: Migrate CodeExample to @monaco-editor/react for Turbopack compatibility 2025-02-22 23:06:37 -06:00
jaron fda26a892c Trying to add monaco 2025-02-22 23:05:30 -06:00
jaron 16c81c7a80 feat: Add Monaco Editor component for code example 2025-02-22 23:00:37 -06:00
jaron 161b78f097 Website init 2025-02-22 22:56:09 -06:00
jaron 62254a7576 Merge pull request #5 from jaronpate/ft/lru-cache
Add LRU cache implementation
2025-02-15 17:52:28 -06:00
jaron b001b1b756 Too much logging 2025-02-15 17:51:50 -06:00
jaron fc77394786 Merge branch 'master' of github.com:jaronpate/unicord into ft/lru-cache 2025-02-15 17:47:27 -06:00
jaron 1fca25ef31 Merge pull request #4 from jaronpate/ft/channel-class-aider
Added a Channel class and channel hydration on message & context
2025-02-15 17:44:41 -06:00
jaron aaa99f9120 feat: Expose additional channel-related enums in library exports 2025-02-15 15:51:01 -06:00
jaron 1dc1371e34 improved repeated hydration test case 2025-02-15 15:49:04 -06:00
jaron 211c1ca4c6 refactor: Improve hydration type handling with Omit and preserve original properties 2025-02-15 12:14:07 -06:00
jaron 69f32aa067 refactor: Improve hydration to preserve previous properties and types 2025-02-15 12:11:27 -06:00
jaron 9043c637c0 refactor: Simplify hydration type inference in hydrate function 2025-02-15 12:09:39 -06:00
jaron 929cd71881 refactor: Simplify hydrator type signature in Context 2025-02-15 12:07:52 -06:00
jaron c756bbbc30 refactor: Simplify hydrator type signature to fix type conflicts 2025-02-15 12:04:48 -06:00
jaron b4a8f412a1 feat: Add channel property to HydratedMessage and HydratedContext types 2025-02-15 12:00:28 -06:00
jaron 817e37c4e4 feat: Add channel hydration support for Message and Context objects 2025-02-15 11:59:15 -06:00
jaron 1cafdbdcc8 feat: Add Channels cache to Client with ObjectCache implementation 2025-02-15 11:57:07 -06:00
jaron 7f745d9a38 fix: Handle Date conversion for last_pin_timestamp in Channel 2025-02-15 11:43:03 -06:00
jaron 962a1e9909 fix channel fromDiscord implementation 2025-02-15 11:41:45 -06:00
jaron a3514801fe feat: Make Channel.fromDiscord method generic to match base class signature 2025-02-15 11:40:38 -06:00
jaron 1227580083 feat: Add DefaultReaction type to channel types 2025-02-15 11:39:20 -06:00
jaron 835271a4c7 feat: Add DefaultReaction type to message types 2025-02-15 11:33:51 -06:00
jaron 6f385a68e7 reformat channel type 2025-02-15 11:32:25 -06:00
jaron dbad7168e8 feat: Add Channel types with enums and Discord/library interfaces 2025-02-15 11:30:41 -06:00
jaron 43f83a1964 Fix import in channel class 2025-02-15 11:19:59 -06:00
jaron 1480905468 feat: Enhance Channel type with comprehensive Discord channel structure support 2025-02-15 11:18:20 -06:00
jaron fb81a55035 Added channel api shape and class 2025-02-15 11:05:59 -06:00
jaron 3bb42e3751 Added aider to .gitignore 2025-02-15 11:03:11 -06:00
jaron 8333ab2d44 Merge pull request #3 from jaronpate/ft/v0.0.4
Added workflow and fixed bug with command handling
2025-01-15 13:51:16 -06:00
32 changed files with 3850 additions and 211 deletions
+1
View File
@@ -173,3 +173,4 @@ dist
# Finder (MacOS) folder config
.DS_Store
.aider*
+45 -2
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(hydrated, [Expectation.Guild]);
const hydrate = await context.hydrator(context, [Expectation.Guild]);
// Check if the object is hydrated
const hasGuild = hydrate(hydrated);
const hasGuild = hydrate(context);
if (hasGuild) {
// Send a message stating the user that they are in a guild
@@ -181,3 +181,46 @@ 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: 1012 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+7
View File
@@ -9,6 +9,7 @@
"files": [
"out"
],
"repository": "github:jaronpate/unicord",
"scripts": {
"test": "bun run testing/demo.test.ts",
"test:inspect": "bun --inspect testing/demo.test.ts",
@@ -34,5 +35,11 @@
},
"dependencies": {
"ws": "^8.18.0"
},
"prettier": {
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"tabWidth": 4
}
}
+9 -2
View File
@@ -1,10 +1,11 @@
// Base classes
export { Client } from './services/client';
export { Client, Client as unicord } 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';
@@ -14,5 +15,11 @@ 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';
+7 -3
View File
@@ -3,9 +3,13 @@ import type { Context } from '../types/context';
const EventBus = new EventEmitter() as Emitter;
export type Emitter = EventEmitter & {
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean
on: (event_name: string, listener: Function) => Emitter
export class Emitter extends EventEmitter {
emit(event_name: string, context: Context | null, ...args: any[]): boolean {
return super.emit(event_name, context, ...args);
}
on(event_name: string, listener: (context: Context | null, ...args: any[]) => void): this {
return super.on(event_name, listener);
}
}
export { EventBus };
+108 -15
View File
@@ -1,15 +1,22 @@
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 = new Map<string, K>();
cache: LRUCache<string, K>;
requests = new Map<string, Promise<K>>();
readonly type: string;
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[]) {
constructor(
protected api: API,
processor: Processor,
type: string,
private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T),
events: string[],
maxCapacity: number = 10_000,
) {
this.cache = new LRUCache<string, K>(maxCapacity);
this.type = type;
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, factory[fromDiscord](e));
@@ -34,8 +41,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) => {
@@ -48,10 +55,8 @@ export class ObjectCache<T, K extends T> {
});
// Save the request
this.requests.set(object_id, req);
// Remove the request from the cache if it succeeds
req.then(() => this.requests.delete(object_id));
// Remove the request from the cache if it fails
req.catch(() => this.requests.delete(object_id));
// Remove the request from the cache when it finishes
req.finally(() => this.requests.delete(object_id));
// Return the request
return req;
}
@@ -64,8 +69,96 @@ export class ObjectCache<T, K extends T> {
return this.fetch(object_id);
}
}
}
list(): Map<string, Promise<T> | T> {
return this.cache;
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;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
import { API } from "../api";
import { Processor } from "../processor";
import { Channel } from "../../types/channel";
import { ObjectCache } from "./cache";
export class Channels extends ObjectCache<Channel, Channel> {
constructor(api: API, processor: Processor) {
super(api, processor, 'channels', Channel, ['CHANNEL_CREATE', 'CHANNEL_UPDATE']);
}
}
+2 -3
View File
@@ -1,14 +1,13 @@
import { fromDiscord } from "../../types/common";
import { Message, type MessagePayload } from "../../types/message";
import type { API } from "../api";
import type { Client } from "../client";
import type { Processor } from "../processor";
import { ObjectCache } from "./cache";
export class Messages extends ObjectCache<Message, MessagePayload> {
constructor (protected client: Client, api: API, processor: Processor) {
super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
constructor (api: API, processor: Processor) {
super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
}
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
+39 -16
View File
@@ -1,14 +1,15 @@
import { API } from './api';
import { Gateway } from './connectors/gateway';
import { Processor } from './processor';
import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
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 { type DiscordMessage, Message } from '../types/message';
import { API } from './api';
import { EventBus, type Emitter } from './bus';
import { Channels } from './caches/channels';
import { Guilds } from './caches/guilds';
import { Messages } from './caches/messages';
import { Users } from './caches/users';
import { Gateway } from './connectors/gateway';
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
import { EventBus, type Emitter } from './bus';
import { Processor } from './processor';
export class Client {
readonly token: string;
@@ -23,6 +24,7 @@ export class Client {
guilds: Guilds;
users: Users;
messages: Messages;
channels: Channels;
chatCommands: Processor[HandlerType.ChatCommands];
applicationCommands: Processor[HandlerType.ApplicationCommands];
interactions: Processor[HandlerType.Interactions];
@@ -42,7 +44,8 @@ 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, this.api, this.processor);
this.messages = new Messages(this.api, this.processor);
this.channels = new Channels(this.api, this.processor);
// Save references to command and event handlers
this.chatCommands = this.processor[HandlerType.ChatCommands];
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
@@ -92,9 +95,12 @@ 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.
@@ -107,17 +113,34 @@ 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()));
}
return Message.fromDiscord(
await this.api.post<DiscordMessage>(`/channels/${channel_id}/messages`, message.toJSON()),
);
};
/**
* Makes a raw HTTP request to the Discord API.
* @param method HTTP method to use for the request (GET, POST, PUT, DELETE).
* @param endpoint The API endpoint to request; can start with a slash or be a full URL.
* @param body Optional body to send with the request, typically used for POST or PUT requests.
* @returns A promise that resolves to the response data of type T.
*/
rawRequest = async <T = Record<string, any>>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
body?: Record<string, any>,
): Promise<T> => {
return this.api.request<T>(method, endpoint, body);
};
// chatCommands = {
// register: (name: string | string[], handler: Handler) => this.processor.chat_commands.register(name, handler),
@@ -131,11 +154,11 @@ export class Client {
// has: (name: string) => this.processor.application_commands.has(name)
// }
on = (event: string, handler: (...args: any[]) => void) => this.bus.on(event, handler);
on = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.on(event, handler);
once = (event: string, handler: (...args: any[]) => void) => this.bus.once(event, handler);
once = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.once(event, handler);
off = (event: string, handler: (...args: any[]) => void) => this.bus.off(event, handler);
off = (event: DiscordAPIEvent, handler: (...args: any[]) => void) => this.bus.off(event, handler);
connect = () => this.gateway.connect();
}
+20 -9
View File
@@ -1,15 +1,16 @@
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;
export type GatewayObject = Message | Channel;
export type Hydrateable = Context | GatewayObject
export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
T extends Message ? HydratedMessage<K> :
T extends Context ? HydratedContext<K> : never;
T extends Message ? HydratedMessage<K> & Omit<T, keyof HydratedMessage<K>> :
T extends Context ? HydratedContext<K> & Omit<T, keyof HydratedContext<K>> : never;
/**
* Hydrates a given data object based on the provided expectations.
@@ -26,12 +27,12 @@ export type Hydrated<T extends Hydrateable, K extends Array<Expectation>> =
*
* @throws Will throw an error if an expectation cannot be resolved.
*/
export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
export async function hydrate<T extends Hydrateable, K extends Array<Expectation>>(
data: T,
expectations: K,
client: Client,
api: API
): Promise<T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
): Promise<Hydrated<T, K>> {
// TODO: add clone trait. Make it a requirement for hydrateable objects.
// Then clone the object and return the hydrated object.
let hydrated: Hydrated<T, K>;
@@ -44,6 +45,9 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
} 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}`);
}
@@ -56,13 +60,20 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
} 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}`);
}
}
}
return data as unknown as (T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>);
// Create a new object that preserves all existing properties
const result = { ...data } as any;
// Ensure the result is properly typed with both old and new hydrated properties
return result as T & Hydrated<T, K>;
}
/**
@@ -78,12 +89,12 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
*
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
*/
export async function hydrator<T extends Hydrateable | Hydrated<any, any>, K extends Array<Expectation>>(
export async function hydrator<T extends Hydrateable, K extends Array<Expectation>>(
data: T,
expectations: K,
client: Client,
api: API
): Promise<(data: unknown) => data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K>> {
): Promise<(data: unknown) => data is Hydrated<T, K>> {
let hydrated = true;
try {
@@ -95,7 +106,7 @@ export async function hydrator<T extends Hydrateable | Hydrated<any, any>, K ext
}
// BUG: Resolves to never after hydrations on an already hydrated object
return function (data: unknown): data is T extends Hydrated<infer U, infer H> ? Hydrated<T & U, [...H, ...K]> : Hydrated<T, K> {
return function (data: unknown): data is Hydrated<T, K> {
return hydrated;
}
}
+267 -28
View File
@@ -1,39 +1,278 @@
export class Channel {
version: number;
type: number;
position: number;
permission_overwrites: any[];
name: string;
id: string;
flags: number;
topic?: string | null;
rate_limit_per_user?: number;
parent_id?: string;
last_message_id?: string | null;
user_limit?: number;
rtc_region?: string | null;
bitrate?: number;
last_pin_timestamp?: string;
import { GatewayObject } from "./base";
import { Trait } from "./common";
import { User, type DiscordUser } from "./user";
constructor(data: any) {
this.version = data.version;
this.type = data.type;
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.id = data.id;
this.flags = data.flags;
this.topic = data.topic;
this.rate_limit_per_user = data.rate_limit_per_user;
this.parent_id = data.parent_id;
this.nsfw = data.nsfw;
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;
this.user_limit = data.user_limit;
this.rate_limit_per_user = data.rate_limit_per_user;
this.recipients = data.recipients;
this.icon = data.icon;
this.owner_id = data.owner_id;
this.application_id = data.application_id;
this.managed = data.managed;
this.parent_id = data.parent_id;
this.last_pin_timestamp = data.last_pin_timestamp instanceof Date
? data.last_pin_timestamp
: data.last_pin_timestamp
? new Date(data.last_pin_timestamp)
: null;
this.rtc_region = data.rtc_region;
this.video_quality_mode = data.video_quality_mode;
this.message_count = data.message_count;
this.member_count = data.member_count;
this.thread_metadata = data.thread_metadata;
this.member = data.member;
this.default_auto_archive_duration = data.default_auto_archive_duration;
this.permissions = data.permissions;
this.flags = data.flags;
this.total_message_sent = data.total_message_sent;
this.available_tags = data.available_tags;
this.applied_tags = data.applied_tags;
this.default_reaction_emoji = data.default_reaction_emoji;
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
this.default_sort_order = data.default_sort_order;
this.default_forum_layout = data.default_forum_layout;
}
static fromAPIResponse(data: any): Channel {
return new Channel(data);
public static [Trait.fromDiscord]<T = Channel>(data: DiscordChannel): T {
return new Channel({
id: data.id,
type: data.type,
guild_id: data.guild_id,
position: data.position,
permission_overwrites: data.permission_overwrites,
name: data.name,
topic: data.topic,
nsfw: data.nsfw,
last_message_id: data.last_message_id,
bitrate: data.bitrate,
user_limit: data.user_limit,
rate_limit_per_user: data.rate_limit_per_user,
recipients: data.recipients?.map(u => User.fromDiscord(u)),
icon: data.icon,
owner_id: data.owner_id,
application_id: data.application_id,
managed: data.managed,
parent_id: data.parent_id,
last_pin_timestamp: data.last_pin_timestamp ? new Date(data.last_pin_timestamp) : null,
rtc_region: data.rtc_region,
video_quality_mode: data.video_quality_mode,
message_count: data.message_count,
member_count: data.member_count,
thread_metadata: data.thread_metadata,
member: data.member,
default_auto_archive_duration: data.default_auto_archive_duration,
permissions: data.permissions,
flags: data.flags,
total_message_sent: data.total_message_sent,
available_tags: data.available_tags,
applied_tags: data.applied_tags,
default_reaction_emoji: data.default_reaction_emoji,
default_thread_rate_limit_per_user: data.default_thread_rate_limit_per_user,
default_sort_order: data.default_sort_order,
default_forum_layout: data.default_forum_layout
}) as T;
}
}
export class DiscordChannel {
id: string;
type: number;
guild_id?: string;
position?: number;
permission_overwrites?: PermissionOverwrite[];
name?: string;
topic?: string | null;
nsfw?: boolean;
last_message_id?: string | null;
bitrate?: number;
user_limit?: number;
rate_limit_per_user?: number;
recipients?: DiscordUser[];
icon?: string | null;
owner_id?: string;
application_id?: string;
managed?: boolean;
parent_id?: string | null;
last_pin_timestamp?: string | null;
rtc_region?: string | null;
video_quality_mode?: number;
message_count?: number;
member_count?: number;
thread_metadata?: ThreadMetadata;
member?: ThreadMember;
default_auto_archive_duration?: number;
permissions?: string;
flags?: number;
total_message_sent?: number;
available_tags?: ForumTag[];
applied_tags?: string[];
default_reaction_emoji?: DefaultReaction;
default_thread_rate_limit_per_user?: number;
default_sort_order?: number | null;
default_forum_layout?: number;
constructor(data: any) {
this.id = data.id;
this.type = data.type;
this.guild_id = data.guild_id;
this.position = data.position;
this.permission_overwrites = data.permission_overwrites;
this.name = data.name;
this.topic = data.topic;
this.nsfw = data.nsfw;
this.last_message_id = data.last_message_id;
this.bitrate = data.bitrate;
this.user_limit = data.user_limit;
this.rate_limit_per_user = data.rate_limit_per_user;
this.recipients = data.recipients;
this.icon = data.icon;
this.owner_id = data.owner_id;
this.application_id = data.application_id;
this.managed = data.managed;
this.parent_id = data.parent_id;
this.last_pin_timestamp = data.last_pin_timestamp;
this.rtc_region = data.rtc_region;
this.video_quality_mode = data.video_quality_mode;
this.message_count = data.message_count;
this.member_count = data.member_count;
this.thread_metadata = data.thread_metadata;
this.member = data.member;
this.default_auto_archive_duration = data.default_auto_archive_duration;
this.permissions = data.permissions;
this.flags = data.flags;
this.total_message_sent = data.total_message_sent;
this.available_tags = data.available_tags;
this.applied_tags = data.applied_tags;
this.default_reaction_emoji = data.default_reaction_emoji;
this.default_thread_rate_limit_per_user = data.default_thread_rate_limit_per_user;
this.default_sort_order = data.default_sort_order;
this.default_forum_layout = data.default_forum_layout;
}
static fromAPIResponse(data: any): DiscordChannel {
return new DiscordChannel(data);
}
}
+181 -100
View File
@@ -3,7 +3,7 @@ export type ClientConfig = {
application_id: string;
prefix?: string;
intents?: Intent[];
}
};
export enum Expectation {
Guild = 'guild',
@@ -16,24 +16,26 @@ 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;
@@ -58,7 +60,7 @@ export type Payload = {
export enum InteractionResponseType {
Pong = 1,
Message = 4,
DeferredMessage = 5
DeferredMessage = 5,
}
export enum Intent {
@@ -79,110 +81,189 @@ 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]
[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',
}
+3 -1
View File
@@ -4,6 +4,7 @@ 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";
/**
@@ -28,6 +29,7 @@ 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 =
@@ -92,7 +94,7 @@ export class Context<D extends ContextData = ContextData> {
*
* @returns A promise that resolves to a type guard function. The type guard function returns `true` if the data object was successfully hydrated, otherwise `false`.
*/
public hydrator = async <T extends Hydrateable | Hydrated<Hydrateable, Array<Expectation>>, K extends Array<Expectation>>(data: T, expectations: K) => {
public hydrator = async <T extends Hydrateable, K extends Array<Expectation>>(data: T, expectations: K) => {
return hydrator<T, K>(data, expectations, this.client, this.api);
}
+9 -3
View File
@@ -1,9 +1,10 @@
import { exists, isNil } from '../utils';
import { GatewayObject } from './base';
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
import type { Channel } from './channel';
import { fromDiscord, Trait, type Expectation } from './common';
import type { Embed } from './embed';
import type { Guild } from './guild';
import { type DiscordUser, User } from './user';
import { User, type DiscordUser } from './user';
export type Component = Partial<Button | SelectMenu> & {
type: ComponentType;
@@ -34,6 +35,11 @@ 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;
@@ -83,7 +89,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;
}
+10 -12
View File
@@ -1,4 +1,4 @@
import { ApplicationCommandType, Client, CommandOptionType, ComponentStyle, type Context, createCommandHandler, Expectation, Intent, InteractionCommpoentData, type InteractionData, Message, type MessagePayload } from '../src/index';
import { ApplicationCommandType, ChannelType, Client, CommandOptionType, ComponentStyle, type Context, createCommandHandler, Expectation, Intent, InteractionCommpoentData, type InteractionData, Message, type MessagePayload } from '../src/index';
const client = new Client({
token: process.env.BOT_TOKEN!,
@@ -7,7 +7,9 @@ const client = new Client({
Intent.GUILDS,
Intent.GUILD_MESSAGES,
Intent.GUILD_MESSAGE_REACTIONS,
Intent.MESSAGE_CONTENT
Intent.MESSAGE_CONTENT,
Intent.DIRECT_MESSAGES,
Intent.DIRECT_MESSAGE_REACTIONS
],
prefix: '!!'
});
@@ -76,23 +78,19 @@ 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 hydrated = await context.hydrate(message, [Expectation.Channel])
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
rehydrated.guild.name
const { message } = await context.hydrate(context, [Expectation.Message]);
const { channel } = await context.hydrate(message, [Expectation.Channel]);
const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
const hasGuild = hydrate(hydrated);
const hydrate = await context.hydrator(context, [Expectation.Guild]);
const hasGuild = hydrate(context);
if (hasGuild) {
// Send a message stating the user that they are in a guild
await context.reply(`You are ${message.author.username} and this is ${hydrated.guild.name}`, true);
await context.reply(`You are ${message.author.username} and this is ${context.guild.name}`, true);
} else {
// Send a message stating the user that they are not in a guild
await context.reply(`You are ${message.author.username} and we are not in a server`, true);
await context.reply(`You are ${message.author.username} and we are in ${channel.type === ChannelType.DM ? 'our DM\'s 😘' : 'a Group 👀'}`, true);
}
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
});
// Hydrator - hydration function generator
+13
View File
@@ -0,0 +1,13 @@
{
"folders": [
{
"name": "unicord",
"path": "."
},
{
"name": "quotes-bot",
"path": "../quotes-bot"
}
],
"settings": {}
}
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+8
View File
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,26 @@
{
"name": "unicord-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"monaco-editor": "^0.52.2",
"monaco-editor-webpack-plugin": "^7.1.0",
"next": "15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-syntax-highlighter": "^15.4.3"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+31
View File
@@ -0,0 +1,31 @@
:root {
--background: #252525;
--foreground: #ededed;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
color-scheme: dark;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from "next";
import { Space_Grotesk, Space_Mono } from "next/font/google";
import "./globals.css";
const spaceGrotesk = Space_Grotesk({
variable: "--font-space-grotesk",
subsets: ["latin"]
});
const spaceMono = Space_Mono({
variable: "--font-mono",
subsets: ["latin"],
weight: ["400"]
});
export const metadata: Metadata = {
title: 'Unicord',
description: 'Unicord is a simple, type-safe, and modular framework for building Discord bots in TypeScript.',
openGraph: {
title: 'Unicord',
description: 'Unicord is a simple, type-safe, and modular framework for building Discord bots in TypeScript.',
images: [
{
url: 'https://unicord.hat.fish/unicord.png',
width: 1024,
height: 1024,
alt: 'Unicord logo'
}
]
}
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${spaceGrotesk.variable} ${spaceMono.variable}`}>
{children}
</body>
</html>
);
}
+499
View File
@@ -0,0 +1,499 @@
.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
@@ -0,0 +1,174 @@
'use client';
import { useEffect, useState } from 'react';
import styles from "./page.module.css";
import dynamic from 'next/dynamic';
const CodeExample = dynamic(
() => import('../components/CodeExample')
);
import { simpleExample, hydrationExample, typeCoercionExample } from '../components/CodeExample';
export default function Home() {
const [activeTab, setActiveTab] = useState('npm');
const [windowWidth, setWindowWidth] = useState(1200);
useEffect(() => {
setWindowWidth(window.innerWidth);
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div className={[styles.page, styles.pageWithHeader].join(" ")}>
<header className={styles.header}>
<div className={styles.logoMark}>
<img src="/unicord.png" alt="Unicord logo"></img>
<div><span className={styles.logoMarkSpan}>uni</span>cord</div>
</div>
<nav className={styles.nav}>
{/* <a href="/docs" className={styles.navLink}>Docs</a> */}
<a href="https://github.com/jaronpate/unicord" className={styles.navLink}>Source</a>
<a href="https://discord.gg/6kFfqa5bde" target='_blank' className={styles.navLink}>Discord</a>
</nav>
</header>
<main className={styles.main}>
<div className={styles.hero}>
<div>
<h1 className={styles.title}>
Welcome to <span className={styles.logoMarkSpan}>uni</span>cord
</h1>
<p className={styles.description}>
Build good bots fast.
</p>
</div>
<div className={styles.codeExample}>
<CodeExample code={simpleExample} noScroll={false} />
</div>
</div>
<div className={styles.pillars}>
<div className={styles.pillarContainer}>
<div className={styles.pillar}>
<div className={styles.pillarIcon}>⚡</div>
<h2>Simple</h2>
<p>Build Discord bots with minimal boilerplate and intuitive APIs</p>
</div>
<div className={styles.pillar}>
<div className={styles.pillarIcon}>🔧</div>
<h2>Maintainable</h2>
<p>Type-safe, modular architecture that scales with your project</p>
</div>
<div className={styles.pillar}>
<div className={styles.pillarIcon}>📈</div>
<h2>Scalable</h2>
<p>Efficient caching and event handling for bots of any size</p>
</div>
</div>
</div>
<div className={styles.hydrationSection}>
<div className={styles.hydrationContainer}>
<div className={styles.hydrationCode}>
<CodeExample code={hydrationExample} height={260} noScroll={windowWidth < 1200 ? false : true} />
</div>
<div className={styles.hydrationText}>
<h2>Smart Hydration</h2>
<p>
Unicord's hydration system automatically manages Discord object lifecycles.
The <code>context.hydrate</code> method intelligently fetches and caches Discord objects
like messages, users, and guilds, ensuring your bot stays efficient while
keeping your code clean and predictable.
</p>
</div>
</div>
</div>
<div className={styles.typeSection}>
<div className={styles.typeContainer}>
<div className={styles.typeText}>
<h2>Type Coercion</h2>
<p>
Unicord automatically handles type coercion for command arguments,
ensuring you always get the correct types in your handlers. Define your
command structure once, and get full type safety and autocompletion
throughout your codebase.
</p>
<p>
In this example, the <code>user</code> argument is automatically typed
as a <code>User</code> object, providing access to all user properties
with full TypeScript support.
</p>
</div>
<div className={styles.typeCode}>
<CodeExample code={typeCoercionExample} height={370} noScroll={windowWidth < 1200 ? false : true} />
</div>
</div>
</div>
<div className={styles.installSection}>
<div className={styles.installContainer}>
<h2>Install <span className={styles.logoMarkSpan}>uni</span>cord</h2>
<div className={styles.packageManagerTabs}>
<button
className={`${styles.tabButton} ${activeTab === 'npm' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('npm')}
>
npm
</button>
<button
className={`${styles.tabButton} ${activeTab === 'yarn' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('yarn')}
>
yarn
</button>
<button
className={`${styles.tabButton} ${activeTab === 'pnpm' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('pnpm')}
>
pnpm
</button>
<button
className={`${styles.tabButton} ${activeTab === 'bun' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('bun')}
>
bun
</button>
</div>
<div className={styles.installCommandWrapper}>
<code className={styles.installCommand}>
{activeTab === 'npm' && 'npm install unicord'}
{activeTab === 'yarn' && 'yarn add unicord'}
{activeTab === 'pnpm' && 'pnpm add unicord'}
{activeTab === 'bun' && 'bun add unicord'}
</code>
<button
className={styles.copyButton}
onClick={() => {
const commands = {
npm: 'npm install unicord',
yarn: 'yarn add unicord',
pnpm: 'pnpm add unicord',
bun: 'bun add unicord'
};
navigator.clipboard.writeText(commands[activeTab as keyof typeof commands]);
}}
>
Copy
</button>
</div>
</div>
</div>
</main>
</div>
);
}
@@ -0,0 +1,13 @@
.editorWrapper {
position: relative;
width: 100%;
height: 100%;
}
.editorContainer {
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
border: 1px solid #2d2d2d;
}
+154
View File
@@ -0,0 +1,154 @@
'use client';
import Editor, { Monaco } from "@monaco-editor/react";
import styles from './CodeExample.module.css';
import { useEffect, useState } from "react";
type CodeExampleProps = {
code: string;
height?: string | number;
noScroll?: boolean;
}
export default function CodeExample({ code, height = "100%", noScroll = false }: CodeExampleProps) {
// const [unicordLib, setUnicordLib] = useState("");
// useEffect(() => {
// setUnicordLib(`
// declare module 'unicord' {
// export enum Intent {
// GUILDS = 1 << 0,
// }
// }
// `);
// }, []);
// const handleEditorDidMount = (monaco: Monaco) => {
// monaco.languages.typescript.typescriptDefaults.addExtraLib(unicordLib);
// monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
// noSemanticValidation: false,
// noSyntaxValidation: false,
// noSuggestionDiagnostics: false,
// });
// monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
// target: monaco.languages.typescript.ScriptTarget.ESNext,
// module: monaco.languages.typescript.ModuleKind.ESNext,
// allowJs: true,
// checkJs: true,
// allowNonTsExtensions: true,
// strict: true,
// noEmit: true,
// lib: [
// "ESNext",
// "DOM",
// "DOM.Iterable",
// "ESNext.AsyncIterable",
// "ESNext.BigInt"
// ],
// types: ["node", "unicord"],
// baseUrl: "./",
// paths: {
// "index": ["./src/index.ts"],
// "unicord": ["./node_modules/unicord/dist/index.d.ts"]
// }
// });
// };
// useEffect(() => {
// fetch("https://www.unpkg.com/unicord@0.0.4/out/types/common.d.ts")
// .then((response) => response.text())
// .then((text) => {
// setUnicordLib(text);
// console.log("Fetched extra lib:", text);
// })
// .catch((error) => {
// console.error("Error fetching extra lib:", error);
// });
// }, []);
return (
<div className={styles.editorWrapper}>
<div className={styles.editorContainer}>
<Editor
height={height}
defaultLanguage="typescript"
defaultValue={code}
theme="vs-dark"
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false,
roundedSelection: false,
padding: { top: 16, bottom: 16 },
automaticLayout: true,
scrollbar: {
alwaysConsumeMouseWheel: !noScroll,
}
}}
// beforeMount={handleEditorDidMount}
/>
</div>
</div>
);
}
// Export the example code snippets for reuse
export const simpleExample = `\
import { Client, type Context, Intent } from 'unicord';
// Initialize and configure
const client = new Client({
token: '<token>',
application_id: '<application_id>',
intents: [
Intent.GUILDS,
Intent.GUILD_MESSAGES,
Intent.GUILD_MESSAGE_REACTIONS,
Intent.MESSAGE_CONTENT
],
prefix: '??'
});
// Register a command
client.chatCommands.register('ping', (context: Context, args) => {
context.reply('Pong!', true);
});
// Connect
client.connect();`;
export const hydrationExample = `\
client.chatCommands.register('ping', async (context: Context, args: any[]) => {
// Hydrate the message id from the context
const { message } = await context.hydrate(context, [Expectation.Message]);
// Send a message to the channel
const response = await context.reply(\`Pong!: got \${args.join(', ')}\`, true);
// Calculate the time it took to send the message
const time = Date.now() - message.timestamp.getTime();
// Edit the message to include the time
await context.editMessage(response, \`Pong! Latency: \${time}ms\`);
});`;
export const typeCoercionExample = `\
const avatarCommand = createCommandHandler({
description: 'Get a users avatar',
args: [
{
id: 'user',
name: 'user',
type: CommandOptionType.User,
description: 'The user to get the avatar for',
required: true
}
],
execute: async (context: Context, args) => {
// Args will be typed here as { user: User }
context.reply(args.user.avatarURL);
}
});`;
+27
View File
@@ -0,0 +1,27 @@
:root {
--color-primary: #ff0080;
--color-background: #000;
--color-text: #fff;
--gray-rgb: 255, 255, 255;
--gray-alpha-200: rgba(var(--gray-rgb), 0.145);
--gray-alpha-100: rgba(var(--gray-rgb), 0.06);
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 2rem;
--spacing-lg: 4rem;
--spacing-xl: 6rem;
--border-radius-sm: 6px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--font-size-sm: 0.9rem;
--font-size-md: 1.1rem;
--font-size-lg: 1.5rem;
--font-size-xl: 2rem;
--section-padding-desktop: 6rem 12em;
--section-padding-mobile: 4rem 2rem;
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}