mirror of
https://github.com/jaronpate/unicord.git
synced 2026-10-18 21:47:40 +00:00
Added channel api shape and class
This commit is contained in:
+7
-3
@@ -3,9 +3,13 @@ import type { Context } from '../types/context';
|
|||||||
|
|
||||||
const EventBus = new EventEmitter() as Emitter;
|
const EventBus = new EventEmitter() as Emitter;
|
||||||
|
|
||||||
export type Emitter = EventEmitter & {
|
export class Emitter extends EventEmitter {
|
||||||
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean
|
emit(event_name: string, context: Context | null, ...args: any[]): boolean {
|
||||||
on: (event_name: string, listener: Function) => Emitter
|
return super.emit(event_name, context, ...args);
|
||||||
|
}
|
||||||
|
on(event_name: string, listener: (context: Context | null, ...args: any[]) => void): this {
|
||||||
|
return super.on(event_name, listener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { EventBus };
|
export { EventBus };
|
||||||
@@ -5,11 +5,12 @@ import type { Processor } from "../processor";
|
|||||||
|
|
||||||
|
|
||||||
export class ObjectCache<T, K extends T> {
|
export class ObjectCache<T, K extends T> {
|
||||||
cache = new Map<string, K>();
|
cache: LRUCache<string, K>;
|
||||||
requests = new Map<string, Promise<K>>();
|
requests = new Map<string, Promise<K>>();
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
|
|
||||||
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[]) {
|
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[], capacity: number = 10_000) {
|
||||||
|
this.cache = new LRUCache<string, K>(10_000)
|
||||||
this.type = type;
|
this.type = type;
|
||||||
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
|
processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
|
||||||
this.cache.set(e.id, factory[fromDiscord](e));
|
this.cache.set(e.id, factory[fromDiscord](e));
|
||||||
@@ -64,8 +65,96 @@ export class ObjectCache<T, K extends T> {
|
|||||||
return this.fetch(object_id);
|
return this.fetch(object_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list(): Map<string, Promise<T> | T> {
|
class ListNode<K, V> {
|
||||||
return this.cache;
|
key: K;
|
||||||
|
value: V;
|
||||||
|
prev: ListNode<K, V> | null = null;
|
||||||
|
next: ListNode<K, V> | null = null;
|
||||||
|
|
||||||
|
constructor(key: K, value: V) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LRUCache<K, V> {
|
||||||
|
private capacity: number;
|
||||||
|
private map: Map<K, ListNode<K, V>>;
|
||||||
|
private head: ListNode<K, V>;
|
||||||
|
private tail: ListNode<K, V>;
|
||||||
|
|
||||||
|
constructor(capacity: number) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.map = new Map();
|
||||||
|
|
||||||
|
// Dummy head and tail to simplify boundary conditions
|
||||||
|
this.head = new ListNode<K, V>(null as any, null as any);
|
||||||
|
this.tail = new ListNode<K, V>(null as any, null as any);
|
||||||
|
this.head.next = this.tail;
|
||||||
|
this.tail.prev = this.head;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
if (!this.map.has(key)) return undefined;
|
||||||
|
|
||||||
|
const node = this.map.get(key)!;
|
||||||
|
this.moveToFront(node);
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: K): boolean {
|
||||||
|
return this.map.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): void {
|
||||||
|
if (this.map.has(key)) {
|
||||||
|
// Update existing node value and move it to front
|
||||||
|
const node = this.map.get(key)!;
|
||||||
|
node.value = value;
|
||||||
|
this.moveToFront(node);
|
||||||
|
} else {
|
||||||
|
// Create a new node
|
||||||
|
const newNode = new ListNode(key, value);
|
||||||
|
this.map.set(key, newNode);
|
||||||
|
this.addToFront(newNode);
|
||||||
|
|
||||||
|
// If over capacity, remove the least recently used (LRU) node
|
||||||
|
if (this.map.size > this.capacity) {
|
||||||
|
this.removeLRU();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveToFront(node: ListNode<K, V>): void {
|
||||||
|
this.removeNode(node);
|
||||||
|
this.addToFront(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addToFront(node: ListNode<K, V>): void {
|
||||||
|
node.next = this.head.next;
|
||||||
|
node.prev = this.head;
|
||||||
|
this.head.next!.prev = node;
|
||||||
|
this.head.next = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeNode(node: ListNode<K, V>): void {
|
||||||
|
node.prev!.next = node.next;
|
||||||
|
node.next!.prev = node.prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeLRU(): void {
|
||||||
|
const lruNode = this.tail.prev!;
|
||||||
|
this.removeNode(lruNode);
|
||||||
|
this.map.delete(lruNode.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||||
|
let current = this.head.next;
|
||||||
|
while (current && current !== this.tail) {
|
||||||
|
yield [current.key, current.value];
|
||||||
|
current = current.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Channel } from "../../types/channel";
|
||||||
|
import type { API } from "../api";
|
||||||
|
import type { Processor } from "../processor";
|
||||||
|
import { ObjectCache } from "./cache";
|
||||||
|
|
||||||
|
export class Channels extends ObjectCache<Channel, Channel> {
|
||||||
|
constructor (api: API, processor: Processor) {
|
||||||
|
super(api, processor, 'channels', Channel, ['CHANNEL_CREATE', 'CHANNEL_UPDATE']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { fromDiscord } from "../../types/common";
|
import { fromDiscord } from "../../types/common";
|
||||||
import { Message, type MessagePayload } from "../../types/message";
|
import { Message, type MessagePayload } from "../../types/message";
|
||||||
import type { API } from "../api";
|
import type { API } from "../api";
|
||||||
import type { Client } from "../client";
|
|
||||||
import type { Processor } from "../processor";
|
import type { Processor } from "../processor";
|
||||||
import { ObjectCache } from "./cache";
|
import { ObjectCache } from "./cache";
|
||||||
|
|
||||||
|
|
||||||
export class Messages extends ObjectCache<Message, MessagePayload> {
|
export class Messages extends ObjectCache<Message, MessagePayload> {
|
||||||
constructor (protected client: Client, api: API, processor: Processor) {
|
constructor (api: API, processor: Processor) {
|
||||||
super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
|
super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
|
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { type DiscordMessage, Message } from '../types/message';
|
|||||||
import { Guilds } from './caches/guilds';
|
import { Guilds } from './caches/guilds';
|
||||||
import { Messages } from './caches/messages';
|
import { Messages } from './caches/messages';
|
||||||
import { Users } from './caches/users';
|
import { Users } from './caches/users';
|
||||||
|
import { Channels } from './caches/channels';
|
||||||
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
|
||||||
import { EventBus, type Emitter } from './bus';
|
import { EventBus, type Emitter } from './bus';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export class Client {
|
|||||||
guilds: Guilds;
|
guilds: Guilds;
|
||||||
users: Users;
|
users: Users;
|
||||||
messages: Messages;
|
messages: Messages;
|
||||||
|
channels: Channels;
|
||||||
chatCommands: Processor[HandlerType.ChatCommands];
|
chatCommands: Processor[HandlerType.ChatCommands];
|
||||||
applicationCommands: Processor[HandlerType.ApplicationCommands];
|
applicationCommands: Processor[HandlerType.ApplicationCommands];
|
||||||
interactions: Processor[HandlerType.Interactions];
|
interactions: Processor[HandlerType.Interactions];
|
||||||
@@ -42,7 +44,8 @@ export class Client {
|
|||||||
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
this.gateway = new Gateway(this, this.processor, this.api, this.bus);
|
||||||
this.guilds = new Guilds(this.api, this.processor);
|
this.guilds = new Guilds(this.api, this.processor);
|
||||||
this.users = new Users(this.api, this.processor);
|
this.users = new Users(this.api, this.processor);
|
||||||
this.messages = new Messages(this, 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
|
// Save references to command and event handlers
|
||||||
this.chatCommands = this.processor[HandlerType.ChatCommands];
|
this.chatCommands = this.processor[HandlerType.ChatCommands];
|
||||||
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
|
this.applicationCommands = this.processor[HandlerType.ApplicationCommands];
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export class Gateway {
|
|||||||
throw new Error('Event name is required');
|
throw new Error('Event name is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
// log('Event:', payload.t);
|
log('Event:', payload.t);
|
||||||
|
|
||||||
// Extract the event name and data
|
// Extract the event name and data
|
||||||
const event_name = payload.t;
|
const event_name = payload.t;
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export async function hydrate<T extends Hydrateable | Hydrated<any, any>, K exte
|
|||||||
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
} else if (expectation === Expectation.Guild && exists(data.guild_id)) {
|
||||||
let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>;
|
let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>;
|
||||||
message.guild = await client.guilds.get(data.guild_id);
|
message.guild = await client.guilds.get(data.guild_id);
|
||||||
|
} else if (expectation === Expectation.Channel && exists(data.channel_id)) {
|
||||||
|
let message = data as unknown as HydratedMessage<[Expectation.Channel, ...Expectation[]]>;
|
||||||
|
message.channel = await client.channels.get(data.channel_id);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
throw new Error(`Invalid expectation requested for Message: ${expectation}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Trait } from "./common";
|
||||||
|
|
||||||
export class Channel {
|
export class Channel {
|
||||||
version: number;
|
version: number;
|
||||||
type: number;
|
type: number;
|
||||||
@@ -33,7 +35,45 @@ export class Channel {
|
|||||||
this.last_pin_timestamp = data.last_pin_timestamp;
|
this.last_pin_timestamp = data.last_pin_timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static [Trait.fromDiscord](data: DiscordChannel): Channel {
|
||||||
|
return new Channel({
|
||||||
|
version: data.version,
|
||||||
|
type: data.type,
|
||||||
|
position: data.position,
|
||||||
|
permission_overwrites: data.permission_overwrites,
|
||||||
|
name: data.name,
|
||||||
|
id: data.id,
|
||||||
|
flags: data.flags,
|
||||||
|
topic: data.topic,
|
||||||
|
rate_limit_per_user: data.rate_limit_per_user,
|
||||||
|
parent_id: data.parent_id,
|
||||||
|
last_message_id: data.last_message_id,
|
||||||
|
user_limit: data.user_limit,
|
||||||
|
rtc_region: data.rtc_region,
|
||||||
|
bitrate: data.bitrate,
|
||||||
|
last_pin_timestamp: data.last_pin_timestamp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static fromAPIResponse(data: any): Channel {
|
static fromAPIResponse(data: any): Channel {
|
||||||
return new Channel(data);
|
return new Channel(data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DiscordChannel = {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { exists, isNil } from '../utils';
|
import { exists, isNil } from '../utils';
|
||||||
import { GatewayObject } from './base';
|
import { GatewayObject } from './base';
|
||||||
import { fromDiscord, Trait, type Expectation, type Expects } from './common';
|
import type { Channel } from './channel';
|
||||||
|
import { fromDiscord, Trait, type Expectation } from './common';
|
||||||
import type { Embed } from './embed';
|
import type { Embed } from './embed';
|
||||||
import type { Guild } from './guild';
|
import type { Guild } from './guild';
|
||||||
import { type DiscordUser, User } from './user';
|
import { User, type DiscordUser } from './user';
|
||||||
|
|
||||||
export type Component = Partial<Button | SelectMenu> & {
|
export type Component = Partial<Button | SelectMenu> & {
|
||||||
type: ComponentType;
|
type: ComponentType;
|
||||||
@@ -83,7 +84,7 @@ export enum ComponentType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type HydratedMessage<T extends Array<Expectation>> = MessagePayload & {
|
export type HydratedMessage<T extends Array<Expectation>> = MessagePayload & {
|
||||||
// channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
channel: Extract<Expectation.Channel, T[number]> extends never ? undefined : Channel;
|
||||||
reference: Extract<Expectation.Message, T[number]> extends never ? undefined : MessagePayload;
|
reference: Extract<Expectation.Message, T[number]> extends never ? undefined : MessagePayload;
|
||||||
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
guild: Extract<Expectation.Guild, T[number]> extends never ? undefined : Guild;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const client = new Client({
|
|||||||
Intent.GUILDS,
|
Intent.GUILDS,
|
||||||
Intent.GUILD_MESSAGES,
|
Intent.GUILD_MESSAGES,
|
||||||
Intent.GUILD_MESSAGE_REACTIONS,
|
Intent.GUILD_MESSAGE_REACTIONS,
|
||||||
Intent.MESSAGE_CONTENT
|
Intent.MESSAGE_CONTENT,
|
||||||
|
Intent.DIRECT_MESSAGES,
|
||||||
|
Intent.DIRECT_MESSAGE_REACTIONS
|
||||||
],
|
],
|
||||||
prefix: '!!'
|
prefix: '!!'
|
||||||
});
|
});
|
||||||
@@ -78,8 +80,8 @@ client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
|||||||
// Note: Testing repeated hydration
|
// Note: Testing repeated hydration
|
||||||
const { message } = await context.hydrate(context, [Expectation.Message])
|
const { message } = await context.hydrate(context, [Expectation.Message])
|
||||||
const hydrated = await context.hydrate(message, [Expectation.Channel])
|
const hydrated = await context.hydrate(message, [Expectation.Channel])
|
||||||
const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
|
// const rehydrated = await context.hydrate(hydrated, [Expectation.Guild]);
|
||||||
rehydrated.guild.name
|
// rehydrated.guild.name
|
||||||
|
|
||||||
const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
|
const hydrate = await context.hydrator(hydrated, [Expectation.Guild]);
|
||||||
const hasGuild = hydrate(hydrated);
|
const hasGuild = hydrate(hydrated);
|
||||||
@@ -89,10 +91,8 @@ client.chatCommands.register('me', async (context: Context, _args: any[]) => {
|
|||||||
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 ${hydrated.guild.name}`, true);
|
||||||
} else {
|
} else {
|
||||||
// Send a message stating the user that they are not in a guild
|
// Send a message stating the user that they are not in a guild
|
||||||
await context.reply(`You are ${message.author.username} and we are not in a server`, true);
|
await context.reply(`You are ${message.author.username} and we are in ${hydrated.channel.type === 1 ? 'our DM\'s <3' : 'a Group DM 👀'}`, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// await context.reply(`You are ${message.author.username} ${hasGuild ? message.guild.name : ''}`, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hydrator - hydration function generator
|
// Hydrator - hydration function generator
|
||||||
|
|||||||
Reference in New Issue
Block a user