diff --git a/mod.ts b/mod.ts index e69de29..6f38a80 100644 --- a/mod.ts +++ b/mod.ts @@ -0,0 +1,5 @@ +export * from "./session/mod.ts"; +export * from "./util/mod.ts"; +export * from "./structures/mod.ts"; +export * from "./vendor/external.ts"; +export * from "./handlers/mod.ts"; \ No newline at end of file diff --git a/session/Events.ts b/session/Events.ts new file mode 100644 index 0000000..44d65f3 --- /dev/null +++ b/session/Events.ts @@ -0,0 +1,6 @@ +import type { + DiscordGatewayPayload, + Shard, +} from "../vendor/external.ts"; + +export type DiscordRawEventHandler = (shard: Shard, data: DiscordGatewayPayload) => unknown; \ No newline at end of file diff --git a/session/Session.ts b/session/Session.ts new file mode 100644 index 0000000..eb0561e --- /dev/null +++ b/session/Session.ts @@ -0,0 +1,129 @@ +import type { + GatewayIntents, + DiscordGatewayPayload, + DiscordGetGatewayBot, + DiscordReady, + DiscordMessage, + GatewayDispatchEventNames, + GatewayBot, + Shard +} from "../vendor/external.ts"; + +import { + EventEmitter, + Snowflake, + Routes +} from "../util/mod.ts"; + +import type { + DiscordRawEventHandler, +} from "./Events.ts"; + +import { + createRestManager, + createGatewayManager +} from "../vendor/external.ts"; + +export interface RestOptions { + secretKey?: string; + applicationId?: Snowflake; +} + +export interface GatewayOptions { + botId?: Snowflake; + data?: GatewayBot; +} + +export interface SessionOptions { + token: string; + rawHandler?: DiscordRawEventHandler; + intents?: GatewayIntents; + rest?: RestOptions; + gateway?: GatewayOptions; +} + +/** + * Receives a Token, connects + * */ +export class Session extends EventEmitter { + options: SessionOptions; + + // TODO: improve this with CreateShardManager etc + rest: ReturnType; + gateway: ReturnType; + + constructor(options: SessionOptions) { + super(); + this.options = options; + + const defHandler: DiscordRawEventHandler = (shard, data) => { + this.emit("raw", data, shard.id); + + if (!data.t) return; + + this.emit(data.t as GatewayDispatchEventNames, data, shard.id); + }; + + this.rest = createRestManager({ + token: this.options.token, + debug: (text) => { + // TODO: set this using the event emitter + super.rawListeners("debug")?.forEach((fn) => fn(text)); + }, + secretKey: this.options.rest?.secretKey ?? undefined + }); + + this.gateway = createGatewayManager({ + gatewayBot: options.gateway?.data ?? {} as GatewayBot, // TODO + gatewayConfig: { + token: options.token, + intents: options.intents + }, + handleDiscordPayload: options.rawHandler ?? defHandler + }); + + // TODO: set botId in Session.botId or something + } + + override on(event: "ready", func: (payload: DiscordReady) => unknown): this; + override on(event: "raw", func: (shard: Shard, data: DiscordGatewayPayload) => unknown): this; + override on(event: "message", func: (message: DiscordMessage) => unknown): this; + override on(event: "debug", func: (text: string) => unknown): this; + override on(event: string, func: Function): this { + return super.on(event, func); + } + + override off(event: string, func: Function): this { + return super.off(event, func); + } + + override once(event: string, func: Function): this { + return super.once(event, func); + } + + async start() { + const getGatewayBot = () => this.rest.runMethod(this.rest, "GET", Routes.GATEWAY_BOT()); + + // check if is empty + if (!Object.keys(this.options.gateway?.data ?? {}).length) { + const nonParsed = await getGatewayBot(); + + this.gateway.gatewayBot = { + url: nonParsed.url, + shards: nonParsed.shards, + sessionStartLimit: { + total: nonParsed.session_start_limit.total, + remaining: nonParsed.session_start_limit.remaining, + resetAfter: nonParsed.session_start_limit.reset_after, + maxConcurrency: nonParsed.session_start_limit.max_concurrency, + }, + }; + this.gateway.lastShardId = this.gateway.gatewayBot.shards - 1; + this.gateway.manager.totalShards = this.gateway.gatewayBot.shards; + } + + this.gateway.spawnShards(); + } +} + + diff --git a/session/mod.ts b/session/mod.ts index e69de29..5162dca 100644 --- a/session/mod.ts +++ b/session/mod.ts @@ -0,0 +1,2 @@ +export * from "./Session.ts"; +export * from "./Events.ts"; \ No newline at end of file diff --git a/tests/deps.ts b/tests/deps.ts new file mode 100644 index 0000000..77680a3 --- /dev/null +++ b/tests/deps.ts @@ -0,0 +1 @@ +export * from "../mod.ts"; \ No newline at end of file diff --git a/tests/mod.ts b/tests/mod.ts new file mode 100644 index 0000000..5845f44 --- /dev/null +++ b/tests/mod.ts @@ -0,0 +1,11 @@ +import * as Discord from "./deps.ts"; + +const session = new Discord.Session({ + token: Deno.args[0], +}); + +session.on("ready", (payload) => console.log(payload)); +session.on("raw", (shard, data) => console.log(shard, data)); +session.on("debug", (text) => console.log(text)); + +session.start(); \ No newline at end of file diff --git a/util/EventEmmiter.ts b/util/EventEmmiter.ts new file mode 100644 index 0000000..350b60a --- /dev/null +++ b/util/EventEmmiter.ts @@ -0,0 +1,77 @@ +// deno-lint-ignore-file ban-types + + +/** + * An event emitter (observer pattern) + * */ +export class EventEmitter { + listeners = new Map; + + #addListener(event: string, func: Function) { + this.listeners.set(event, this.listeners.get(event) || []); + this.listeners.get(event)?.push(func); + return this; + } + + on(event: string, func: Function) { + return this.#addListener(event, func); + } + + #removeListener(event: string, func: Function) { + if (this.listeners.has(event)) { + const listener = this.listeners.get(event); + + if (listener?.includes(func)) { + listener.splice(listener.indexOf(func), 1); + + if (listener.length === 0) { + this.listeners.delete(event); + } + } + } + + return this; + } + + off(event: string, func: Function) { + return this.#removeListener(event, func); + } + + once(event: string, func: Function) { + // it is important for this to be an arrow function + const closure = () => { + func(); + this.off(event, func); + } + + const listener = this.listeners.get(event) ?? []; + + listener.push(closure); + + return this; + } + + emit(event: string, ...args: unknown[]) { + const listener = this.listeners.get(event); + + if (!listener) { + return false; + } + + listener.forEach((f) => f(...args)); + + return true; + } + + listenerCount(eventName: string) { + return this.listeners.get(eventName)?.length ?? 0; + } + + rawListeners(eventName: string): Function[] | undefined { + return this.listeners.get(eventName); + } + +} + + +export default EventEmitter; \ No newline at end of file diff --git a/util/Routes.ts b/util/Routes.ts new file mode 100644 index 0000000..29506c4 --- /dev/null +++ b/util/Routes.ts @@ -0,0 +1,3 @@ +export function GATEWAY_BOT() { + return "/gateway/bot"; +} \ No newline at end of file diff --git a/util/Snowflake.ts b/util/Snowflake.ts new file mode 100644 index 0000000..0b0ee61 --- /dev/null +++ b/util/Snowflake.ts @@ -0,0 +1,11 @@ +// snowflake type +export type Snowflake = string; + +export const DiscordEpoch = 14200704e5; + +// utilities for Snowflakes +export const Snowflake = { + snowflakeToTimestamp(id: Snowflake) { + return (Number(id) >> 22) + DiscordEpoch; + } +} \ No newline at end of file diff --git a/util/mod.ts b/util/mod.ts new file mode 100644 index 0000000..9300663 --- /dev/null +++ b/util/mod.ts @@ -0,0 +1,3 @@ +export * from "./EventEmmiter.ts"; +export * from "./Snowflake.ts"; +export * as Routes from "./Routes.ts"; \ No newline at end of file