summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/index.ts2
-rw-r--r--src/commands/ping.ts145
-rw-r--r--src/index.ts43
-rw-r--r--src/register.ts17
-rw-r--r--src/sys/events/client.ts173
-rw-r--r--src/sys/events/index.ts2
-rw-r--r--src/sys/events/transport.ts332
-rw-r--r--src/sys/handler/builder.ts26
-rw-r--r--src/sys/handler/index.ts83
9 files changed, 421 insertions, 402 deletions
diff --git a/src/commands/index.ts b/src/commands/index.ts
index 366e781..56a879c 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -1,3 +1,3 @@
-import { ping } from "./ping";
+import {ping} from './ping';
export const commands = [ping];
diff --git a/src/commands/ping.ts b/src/commands/ping.ts
index a39e25a..8e03af8 100644
--- a/src/commands/ping.ts
+++ b/src/commands/ping.ts
@@ -1,86 +1,95 @@
import {
- APIApplicationCommandInteraction,
- APIInteractionResponse,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- InteractionResponseType,
-} from "discord-api-types/v10";
-import { CommandBuilder, HandlerFn } from "../sys/handler";
-import { promise } from "ping";
+ type APIApplicationCommandInteraction,
+ type APIInteractionResponse,
+ ApplicationCommandOptionType,
+ ApplicationCommandType,
+ InteractionResponseType,
+} from 'discord-api-types/v10.js';
+import {promise} from 'ping';
+import {CommandBuilder, type HandlerFn} from '../sys/handler';
type Messages = {
- latency_message: [number];
- failure_message: [string];
+ latencyMessage: [number];
+ failureMessage: [string];
};
type Locales = {
- [K in keyof Messages]: Partial<
- Record<
- APIApplicationCommandInteraction["locale"] & "_",
- (...args: Messages[K]) => string
- >
- >;
+ [K in keyof Messages]: Partial<
+ Record<
+ APIApplicationCommandInteraction['locale'] | '_',
+ (...args: Messages[K]) => string
+ >
+ >;
};
const locales: Locales = {
- latency_message: {
- fr: (latency: number) =>
- `**Temps de complétion du ping:** \`${latency}ms\``,
- _: (latency: number) => `**Ping time:** \`${latency}ms\``,
- },
- failure_message: {
- _: (error: string) => `**The host seems unreachable** \`\`\`${error}\`\`\``,
- },
+ latencyMessage: {
+ fr: (latency: number) =>
+ `**Temps de complétion du ping:** \`${latency}ms\``,
+ _: (latency: number) => `**Ping time:** \`${latency}ms\``,
+ },
+ failureMessage: {
+ _: (error: string) => `**The host seems unreachable** \`\`\`${error}\`\`\``,
+ },
};
-const resolve = (
- msg: keyof Messages,
- locale: APIApplicationCommandInteraction["locale"],
- ...args: Messages[typeof msg]
-) => (locales[msg][locale] ?? locales[msg]["_"])(args);
+const resolve = <T extends keyof Messages>(
+ message: T,
+ locale: APIApplicationCommandInteraction['locale'],
+ ...args: Messages[T]
+) =>
+ (
+ (locales[message][locale] ?? locales[message]._) as (
+ ...args: Messages[T]
+ ) => string
+ )(...args);
const handler: HandlerFn = async (
- data: APIApplicationCommandInteraction
+ data: APIApplicationCommandInteraction,
): Promise<APIInteractionResponse> => {
- if (
- data.data.type === ApplicationCommandType.ChatInput &&
- data.data.options[0].name === "host" &&
- data.data.options[0].type === ApplicationCommandOptionType.String
- ) {
- let host = data.data.options[0].value;
- let response = await promise.probe(host);
+ if (
+ data.data.type === ApplicationCommandType.ChatInput &&
+ data.data.options[0].name === 'host' &&
+ data.data.options[0].type === ApplicationCommandOptionType.String
+ ) {
+ const host = data.data.options[0].value;
+ const response = await promise.probe(host);
- if (response.alive) {
- return {
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- content: resolve("latency_message", data.locale, response.avg),
- },
- };
- } else {
- let output = response.output;
- return {
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- content: resolve("failure_message", data.locale, output),
- },
- };
- }
- }
+ if (response.alive) {
+ return {
+ type: InteractionResponseType.ChannelMessageWithSource,
+ data: {
+ content: resolve(
+ 'latencyMessage',
+ data.locale,
+ Number.parseInt(response.avg, 10),
+ ),
+ },
+ };
+ }
+
+ const output = response.output;
+ return {
+ type: InteractionResponseType.ChannelMessageWithSource,
+ data: {
+ content: resolve('failureMessage', data.locale, output),
+ },
+ };
+ }
};
export const ping = new CommandBuilder()
- .setName("ping")
- .handler(handler)
- .setDescription("Measure the bot network latency")
- .addStringOption((option) =>
- option
- .setName("host")
- .setDescription("The host to test pings against")
- .setRequired(true)
- )
- .setDescriptionLocalizations({
- fr: "Mesure la latence reseau du bot",
- })
- .setDMPermission(false)
- .build();
+ .setName('ping')
+ .handler(handler)
+ .setDescription('Measure the bot network latency')
+ .addStringOption((option) =>
+ option
+ .setName('host')
+ .setDescription('The host to test pings against')
+ .setRequired(true),
+ )
+ .setDescriptionLocalizations({
+ fr: 'Mesure la latence reseau du bot',
+ })
+ .setDMPermission(false)
+ .build();
diff --git a/src/index.ts b/src/index.ts
index c95e664..2a9ea9e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,34 +1,35 @@
-import { Client } from "./sys/events";
-import { buildHandler } from "./sys/handler";
-import { commands } from "./commands";
+import 'source-map-support';
+import {Client} from './sys/events';
+import {buildHandler} from './sys/handler';
+import {commands} from './commands';
/**
* We instanciate our nova broker client.
*/
const emitter = new Client({
- transport: {
- additionalEvents: [],
- nats: {
- servers: ["localhost:4222"],
- },
- queue: "nova-worker-common",
- },
- rest: {
- api: "http://localhost:8090/api",
- },
+ transport: {
+ additionalEvents: [],
+ nats: {
+ servers: ['localhost:4222'],
+ },
+ queue: 'nova-worker-common',
+ },
+ rest: {
+ api: 'http://localhost:8090/api',
+ },
});
// We register our slash command handler.
-emitter.on("interactionCreate", buildHandler(commands));
+emitter.on('interactionCreate', buildHandler(commands));
// Simple message handler
-emitter.on("messageCreate", (message) => {
- if (message.content === "~ping") {
- message.client.channels.createMessage(message.channel_id, {
- content: `Bonjour! <@${message.author.id}>`,
- });
- }
+emitter.on('messageCreate', async (message) => {
+ if (message.content === '~ping') {
+ await message.client.channels.createMessage(message.channel_id, {
+ content: `Bonjour! <@${message.author.id}>`,
+ });
+ }
});
// We connect ourselves to the nova nats broker.
-emitter.start().catch(console.log);
+(async () => emitter.start())();
diff --git a/src/register.ts b/src/register.ts
index 92ce36a..303f4d8 100644
--- a/src/register.ts
+++ b/src/register.ts
@@ -1,15 +1,14 @@
-import { REST } from "@discordjs/rest";
-import { commands } from "./commands";
-import { registerCommands } from "./sys/handler";
+import 'source-map-support';
+import {REST} from '@discordjs/rest';
+import {commands} from './commands';
+import {registerCommands} from './sys/handler';
const rest = new REST({
- version: "10",
- headers: { Authorization: "" },
- api: "http://localhost:8090/api",
- }).setToken("_");
+ version: '10',
+ api: 'http://localhost:8090/api',
+}).setToken('_');
-
/**
* We register the commands with discord
*/
-registerCommands(commands, rest, "807188335717384212"); \ No newline at end of file
+(async () => registerCommands(commands, rest, '807188335717384212'))();
diff --git a/src/sys/events/client.ts b/src/sys/events/client.ts
index 90d7447..73fe34f 100644
--- a/src/sys/events/client.ts
+++ b/src/sys/events/client.ts
@@ -1,17 +1,15 @@
+import {EventEmitter} from 'node:stream';
+import {type CamelCase, type PascalCase} from 'type-fest';
+import {REST, type RESTOptions} from '@discordjs/rest';
import {
- APIApplicationCommandPermissionsConstant,
- APIInteractionResponse,
- APIInteractionResponseCallbackData,
- GatewayDispatchPayload,
- GatewayInteractionCreateDispatchData,
-} from "discord-api-types/v10";
-import { Transport, TransportOptions } from "./transport";
-import { CamelCase, PascalCase } from "type-fest";
-import { REST, RESTOptions } from "@discordjs/rest";
-import { EventEmitter } from "stream";
-import TypedEmitter from "typed-emitter";
-
-import { API } from "@discordjs/core";
+ type APIInteractionResponse,
+ type APIInteractionResponseCallbackData,
+ type GatewayDispatchPayload,
+ type GatewayInteractionCreateDispatchData,
+} from 'discord-api-types/v10.js';
+import type TypedEmitter from 'typed-emitter';
+import {API} from '@discordjs/core';
+import {Transport, type TransportOptions} from './transport';
/**
* Maps an event name (O['t']) and a Union O and extracts alla the union members that have a matching O['t']
@@ -24,27 +22,31 @@ import { API } from "@discordjs/core";
* let variant2: ExtractVariant<ExampleUnion, 'type2'>; // Type of variant2 is Variant2
*
*/
-type ExtractVariant<O extends { t: string }, U extends O["t"]> = Extract<
- O & { t: Exclude<O["t"], Exclude<O["t"], U>> },
- { t: U }
+type ExtractVariant<O extends {t: string}, U extends O['t']> = Extract<
+ O & {t: Exclude<O['t'], Exclude<O['t'], U>>},
+ {t: U}
>;
/**
* Add intrisics properties to the event, such as `client` and `rest`
*/
-export type WithIntrisics<T> = T & { client: Client };
+export type WithIntrisics<T> = T & {client: Client};
+
+/**
+ * CamelCased event name
+ */
export type EventName = keyof EventsHandlerArguments;
/**
* Reprends a handler function with one argument
*/
export type HandlerFunction<Arg extends unknown[]> = (
- ...args: Arg
-) => unknown | Promise<unknown>;
+ ...args: Arg
+) => PromiseLike<void>;
export type EventTypes = {
- [P in GatewayDispatchPayload["t"]]: WithIntrisics<
- ExtractVariant<GatewayDispatchPayload, P>["d"]
- >;
+ [P in GatewayDispatchPayload['t']]: WithIntrisics<
+ ExtractVariant<GatewayDispatchPayload, P>['d']
+ >;
};
/**
@@ -52,16 +54,16 @@ export type EventTypes = {
* Also reteives the type of the event using ExtractEvent
*/
export type EventsHandlerArguments = {
- [P in keyof EventTypes as `${CamelCase<P>}`]: HandlerFunction<
- [EventTypes[P]]
- >;
+ [P in keyof EventTypes as `${CamelCase<P>}`]: HandlerFunction<
+ [EventTypes[P]]
+ >;
} & {
- interactionCreate: HandlerFunction<
- [
- WithIntrisics<GatewayInteractionCreateDispatchData>,
- (interactionCreate: APIInteractionResponseCallbackData) => void
- ]
- >;
+ interactionCreate: HandlerFunction<
+ [
+ WithIntrisics<GatewayInteractionCreateDispatchData>,
+ (interactionCreate: APIInteractionResponse) => void,
+ ]
+ >;
};
/**
@@ -69,74 +71,77 @@ export type EventsHandlerArguments = {
* This is implemented by a Proxy
*/
export type EventsFunctions = {
- [P in keyof EventsHandlerArguments as P extends string
- ? `on${PascalCase<P>}`
- : never]: (fn: EventsHandlerArguments[P]) => Client;
+ [P in keyof EventsHandlerArguments as P extends string
+ ? `on${PascalCase<P>}`
+ : never]: (fn: EventsHandlerArguments[P]) => Client;
};
/**
* Defines all the methods known to be implemented
*/
-interface ClientFunctions
- extends EventsFunctions,
- TypedEmitter<EventsHandlerArguments>,
- API {}
+type ClientFunctions = Record<string, unknown> &
+ EventsFunctions &
+ TypedEmitter<EventsHandlerArguments> &
+ API;
/**
* The real extended class is an EventEmitter.
*/
-const UndefinedClient: { new (): ClientFunctions } = EventEmitter as any;
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+const undefinedClient: new () => ClientFunctions = EventEmitter as any;
/**
- * nova.js client
+ * Nova.js client
*
* Used to interact with nova, emits events from nova
* Example:
* client.on('messageCreate', (message) => { console.log('Message received', message.content) });
* client.on('interactionCreate', (message) => { });
*/
-export class Client extends UndefinedClient {
- private readonly transport: Transport;
- private readonly api: API;
- public readonly rest: REST;
-
- constructor(options: {
- rest?: Partial<RESTOptions>;
- transport: TransportOptions;
- }) {
- super();
- this.rest = new REST(options.rest);
- this.transport = new Transport(this, options.transport);
- this.api = new API(this.rest);
-
- // This is safe because this event is emitted by the EventEmitter itself.
- this.on("newListener" as any, (event: EventName) => {
- this.transport.subscribe(event);
- });
-
- // Using a proxy to provide the 'on...' functionality
- return new Proxy(this, {
- get(self, symbol) {
- let name = symbol.toString();
- if (name.startsWith("on") && name.length > 2) {
- // Get the event name
- let eventName = [name[2].toLowerCase(), name.slice(3)].join(
- ""
- ) as EventName;
- return (fn: EventsHandlerArguments[typeof eventName]) =>
- self.on(eventName, fn);
- }
-
- if (self.api[symbol] && self[symbol]) {
- return self.api[symbol];
- }
-
- return self[symbol];
- },
- });
- }
-
- public start() {
- return this.transport.start();
- }
+export class Client extends undefinedClient {
+ public readonly rest: REST;
+
+ private readonly transport: Transport;
+ private readonly api: API;
+
+ constructor(options: {
+ rest?: Partial<RESTOptions>;
+ transport: TransportOptions;
+ }) {
+ super();
+ this.rest = new REST(options.rest);
+ this.transport = new Transport(this, options.transport);
+ this.api = new API(this.rest);
+
+ // This is safe because this event is emitted by the EventEmitter itself.
+ this.on('newListener' as any, async (event: EventName) => {
+ await this.transport.subscribe(event);
+ });
+
+ // Using a proxy to provide the 'on...' functionality
+ return new Proxy(this, {
+ get(self, symbol: keyof typeof Client) {
+ const name = symbol.toString();
+ if (name.startsWith('on') && name.length > 2) {
+ // Get the event name
+ const eventName = [name[2].toLowerCase(), name.slice(3)].join(
+ '',
+ ) as EventName;
+ return (fn: EventsHandlerArguments[typeof eventName]) =>
+ self.on(eventName, fn);
+ }
+
+ if (self.api[symbol] && self[symbol as string]) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ return self.api[symbol];
+ }
+
+ return self[symbol as string];
+ },
+ });
+ }
+
+ public async start() {
+ return this.transport.start();
+ }
}
diff --git a/src/sys/events/index.ts b/src/sys/events/index.ts
index 5ec7692..4f1cce4 100644
--- a/src/sys/events/index.ts
+++ b/src/sys/events/index.ts
@@ -1 +1 @@
-export * from "./client";
+export * from './client';
diff --git a/src/sys/events/transport.ts b/src/sys/events/transport.ts
index 28be5cf..1a2e86a 100644
--- a/src/sys/events/transport.ts
+++ b/src/sys/events/transport.ts
@@ -1,175 +1,185 @@
-import { connect, ConnectionOptions, NatsConnection, Subscription } from "nats";
+import {Buffer} from 'node:buffer';
import {
- Client,
- EventName,
- EventTypes,
- EventsHandlerArguments,
- WithIntrisics,
-} from ".";
-import globRegex from "glob-regex";
+ connect,
+ type ConnectionOptions,
+ type NatsConnection,
+ type Subscription,
+} from 'nats';
+import globRegex from 'glob-regex';
import {
- APIInteraction,
- APIInteractionResponse,
- APIInteractionResponseCallbackData,
- GatewayDispatchPayload,
- GatewayInteractionCreateDispatch,
- Routes,
-} from "discord-api-types/v10";
-import { CamelCase } from "type-fest";
+ type APIInteractionResponse,
+ InteractionResponseType,
+ type APIInteractionResponseCallbackData,
+ type GatewayDispatchPayload,
+ Routes,
+} from 'discord-api-types/v10.js';
+import {type CamelCase} from 'type-fest';
+import {type Client, type EventName, type EventsHandlerArguments} from '.';
/**
* Options for the nats transport layer
*/
export type TransportOptions = {
- additionalEvents?: (keyof EventsHandlerArguments)[];
- nats?: ConnectionOptions;
- queue: string;
+ additionalEvents?: Array<keyof EventsHandlerArguments>;
+ nats?: ConnectionOptions;
+ queue: string;
};
/**
* Transport implements all the communication to Nova using Nats
*/
export class Transport {
- // Nats connection
- private nats: NatsConnection | null = null;
- // Current subscriptions
- private subscriptions: Map<string, Subscription> = new Map();
- // Current subscribed events
- private events: Set<EventName> = new Set();
-
- // Creats a new Transport instance.
- constructor(
- private emitter: Client,
- private config: Partial<TransportOptions>
- ) {}
-
- /**
- * Starts a new nats client.
- */
- public async start() {
- this.nats = await connect(this.config?.nats);
-
- for (let eventName of this.events) {
- await this.subscribe(eventName);
- }
- if (this.config.additionalEvents) {
- for (let eventName of this.config.additionalEvents) {
- await this.subscribe(eventName);
- }
- }
- }
-
- /**
- * Subscribe to a new topic
- * @param event Event to subscribe to
- * @returns
- */
- public async subscribe(event: EventName) {
- // If nats is not connected, we simply request to subscribe to it at startup
- if (!this.nats) {
- console.log("Requesting event " + event);
- this.events.add(event);
- return;
- }
-
- // Since the event names used by this library are camelCase'd we need to
- // re-transform it to the UPPER_CASE used by nova.
- let dashed = event.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase());
- // Construct the topic name used by nova.
- // This **is going to change** as we implement the caching component.
- let topic = `nova.cache.dispatch.${dashed.toUpperCase()}`;
-
- // To avoid having multiple subscriptions covering this event
- // we check if each of our subscriptions covers this scope.
- let isAlreadyPresent = [...this.subscriptions.keys()].reduce(
- (previous, current) => {
- if (previous) return previous;
- let regex = globRegex(current);
-
- return regex.test(topic);
- },
- false
- );
-
- // We abord the subscriptions if it's already covered.
- if (isAlreadyPresent) {
- console.warn("nats subscription already covered.");
- return;
- }
-
- // We remove all the subscriptions that are covered by out current subsciptions.
- let regex = globRegex(topic);
- [...this.subscriptions.keys()].map((key) => {
- if (regex.test(key)) {
- let subsciption = this.subscriptions.get(key);
- if (!subsciption) {
- return;
- }
-
- console.log(`unsubscribing from ${key}`);
- subsciption.unsubscribe();
- }
- });
-
- this._subscriptionTask(topic);
- }
-
- // Task that monitors the subscription
- // It also listens for a subscription end.
- private async _subscriptionTask(topic: string) {
- if (!this.nats) {
- throw new Error("nats connection is not started");
- }
-
- console.log(`subscribing to ${topic}`);
- // Create the nats subscription
- let subscription = this.nats.subscribe(topic, { queue: this.config.queue });
- this.subscriptions.set(topic, subscription);
- // Handle each event in the subscription stream.
- for await (let publish of subscription) {
- try {
- // Decode the payload
- let event: GatewayDispatchPayload = JSON.parse(
- Buffer.from(publish.data).toString("utf-8")
- );
- // Transform the event name to a camclCased name
- const camelCasedName = event.t
- .toLowerCase()
- .replace(/_([a-z])/g, function (g) {
- return g[1].toUpperCase();
- }) as CamelCase<typeof event.t>;
-
- // Since an interaction need a reponse,
- // we need to handle the case where nova is not configured
- // with a webhook endpoint, hence we need to use a post request
- // against webhook execute endpoint with the interaction data.
- if (event.t === "INTERACTION_CREATE") {
- let interaction = event.d;
- let respond = async (respond: APIInteractionResponseCallbackData) => {
- if (publish.reply) {
- publish.respond(Buffer.from(JSON.stringify(respond), "utf-8"));
- } else {
- await this.emitter.interactions.reply(
- interaction.id,
- interaction.token,
- respond
- );
- }
- };
- // Emit the
- this.emitter.emit(
- camelCasedName,
- { ...event.d, client: this.emitter },
- respond
- );
- } else {
- // Typescript refuses to infer this, whyyy
- this.emitter.emit(camelCasedName, {
- ...event.d,
- client: this.emitter,
- } as any);
- }
- } catch (e) {}
- }
- }
+ // Nats connection
+ private nats: NatsConnection | undefined = null;
+ // Current subscriptions
+ private readonly subscriptions = new Map<string, Subscription>();
+ // Current subscribed events
+ private readonly events = new Set<EventName>();
+
+ // Creats a new Transport instance.
+ constructor(
+ private readonly emitter: Client,
+ private readonly config: Partial<TransportOptions>,
+ ) {}
+
+ /**
+ * Starts a new nats client.
+ */
+ public async start() {
+ this.nats = await connect(this.config?.nats);
+
+ await Promise.all(
+ [...this.events].map(async (eventName) => this.subscribe(eventName)),
+ );
+
+ if (this.config.additionalEvents) {
+ await Promise.all(
+ this.config.additionalEvents.map(async (eventName) =>
+ this.subscribe(eventName),
+ ),
+ );
+ }
+ }
+
+ /**
+ * Subscribe to a new topic
+ * @param event Event to subscribe to
+ * @returns
+ */
+ public async subscribe(event: EventName) {
+ // If nats is not connected, we simply request to subscribe to it at startup
+ if (!this.nats) {
+ console.log('Requesting event ' + event);
+ this.events.add(event);
+ return;
+ }
+
+ // Since the event names used by this library are camelCase'd we need to
+ // re-transform it to the UPPER_CASE used by nova.
+ const dashed = event.replace(/[A-Z]/g, (m) => '_' + m.toLowerCase());
+ // Construct the topic name used by nova.
+ // This **is going to change** as we implement the caching component.
+ const topic = `nova.cache.dispatch.${dashed.toUpperCase()}`;
+
+ // To avoid having multiple subscriptions covering this event
+ // we check if each of our subscriptions covers this scope.
+ const isAlreadyPresent = [...this.subscriptions.keys()].reduce(
+ (previous, current) => {
+ if (previous) {
+ return previous;
+ }
+
+ const regex = globRegex(current);
+
+ return regex.test(topic);
+ },
+ false,
+ );
+
+ // We abord the subscriptions if it's already covered.
+ if (isAlreadyPresent) {
+ console.warn('nats subscription already covered.');
+ return;
+ }
+
+ // We remove all the subscriptions that are covered by out current subsciptions.
+ const regex = globRegex(topic);
+ for (const key of this.subscriptions.keys()) {
+ if (regex.test(key)) {
+ const subsciption = this.subscriptions.get(key);
+ if (!subsciption) {
+ continue;
+ }
+
+ console.log(`unsubscribing from ${key}`);
+ subsciption.unsubscribe();
+ }
+ }
+
+ void this._subscriptionTask(topic);
+ }
+
+ // Task that monitors the subscription
+ // It also listens for a subscription end.
+ private async _subscriptionTask(topic: string) {
+ if (!this.nats) {
+ throw new Error('nats connection is not started');
+ }
+
+ console.log(`subscribing to ${topic}`);
+ // Create the nats subscription
+ const subscription = this.nats.subscribe(topic, {
+ queue: this.config.queue,
+ });
+ this.subscriptions.set(topic, subscription);
+ // Handle each event in the subscription stream.
+ for await (const publish of subscription) {
+ try {
+ // Decode the payload
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const event: GatewayDispatchPayload = JSON.parse(
+ Buffer.from(publish.data).toString('utf8'),
+ );
+ // Transform the event name to a camclCased name
+ const camelCasedName = event.t
+ .toLowerCase()
+ .replace(/_([a-z])/g, (g) => g[1].toUpperCase()) as CamelCase<
+ typeof event.t
+ >;
+
+ // Since an interaction need a reponse,
+ // we need to handle the case where nova is not configured
+ // with a webhook endpoint, hence we need to use a post request
+ // against webhook execute endpoint with the interaction data.
+ if (event.t === 'INTERACTION_CREATE') {
+ const interaction = event.d;
+ const respond = async (respond: APIInteractionResponse) => {
+ if (publish.reply) {
+ publish.respond(Buffer.from(JSON.stringify(respond), 'utf8'));
+ } else {
+ await this.emitter.rest.post(
+ Routes.interactionCallback(interaction.id, interaction.token),
+ {
+ body: respond,
+ },
+ );
+ }
+ };
+
+ // Emit the
+ this.emitter.emit(
+ camelCasedName,
+ {...event.d, client: this.emitter},
+ respond,
+ );
+ } else {
+ // Typescript refuses to infer this, whyyy
+ this.emitter.emit(camelCasedName, {
+ ...event.d,
+ client: this.emitter,
+ } as any);
+ }
+ } catch {}
+ }
+ }
}
diff --git a/src/sys/handler/builder.ts b/src/sys/handler/builder.ts
index feb5780..71732a8 100644
--- a/src/sys/handler/builder.ts
+++ b/src/sys/handler/builder.ts
@@ -1,22 +1,18 @@
-import { SlashCommandBuilder } from "@discordjs/builders";
-import { Command, HandlerFn } from ".";
+import {SlashCommandBuilder} from '@discordjs/builders';
+import {type Command, type HandlerFn} from '.';
/**
- * Simple wrapper around the SlashCommandBuilder provided by Discord.js
+ * Simple wrapper around the SlashCommandBuilder provided by Discord.js
*/
export class CommandBuilder extends SlashCommandBuilder {
- private _handler: HandlerFn;
+ private _handler: HandlerFn;
- constructor() {
- super();
- }
+ handler(handler: HandlerFn): this {
+ this._handler = handler;
+ return this;
+ }
- handler(handler: HandlerFn): this {
- this._handler = handler;
- return this;
- }
-
- build(): Command {
- return { json: this.toJSON(), handler: this._handler };
- }
+ build(): Command {
+ return {json: this.toJSON(), handler: this._handler};
+ }
}
diff --git a/src/sys/handler/index.ts b/src/sys/handler/index.ts
index c9a5343..2d5e232 100644
--- a/src/sys/handler/index.ts
+++ b/src/sys/handler/index.ts
@@ -1,28 +1,27 @@
-import { REST } from "@discordjs/rest";
+import {type REST} from '@discordjs/rest';
import {
- APIApplicationCommandInteraction,
- APIInteraction,
- APIInteractionResponse,
- APIInteractionResponseCallbackData,
- InteractionType,
- RESTPostAPIApplicationCommandsJSONBody,
- RESTPostAPIChatInputApplicationCommandsJSONBody,
- Routes,
-} from "discord-api-types/v10";
+ type APIApplicationCommandInteraction,
+ type APIInteraction,
+ InteractionType,
+ type RESTPostAPIApplicationCommandsJSONBody,
+ type RESTPostAPIChatInputApplicationCommandsJSONBody,
+ Routes,
+ type APIInteractionResponse,
+} from 'discord-api-types/v10.js';
-export * from "./builder";
+export * from './builder';
export type PromiseLike<T> = T | Promise<T>;
/**
* A simple function that executes a slash command.
*/
export type HandlerFn = (
- data: APIApplicationCommandInteraction
-) => PromiseLike<APIInteractionResponseCallbackData>;
+ data: APIApplicationCommandInteraction,
+) => PromiseLike<APIInteractionResponse>;
export type Command = {
- json: RESTPostAPIChatInputApplicationCommandsJSONBody;
- handler: HandlerFn;
+ json: RESTPostAPIChatInputApplicationCommandsJSONBody;
+ handler: HandlerFn;
};
/**
@@ -32,15 +31,15 @@ export type Command = {
* @param applicationId Current application id
*/
export const registerCommands = async (
- commands: Iterable<Command>,
- rest: REST,
- applicationId: string
+ commands: Iterable<Command>,
+ rest: REST,
+ appId: string,
) => {
- await rest.post(Routes.applicationCommands(applicationId), {
- body: [...commands].map(
- (x) => x.json
- ) as RESTPostAPIApplicationCommandsJSONBody[],
- });
+ await rest.post(Routes.applicationCommands(appId), {
+ body: [...commands].map(
+ (x) => x.json,
+ ) as RESTPostAPIApplicationCommandsJSONBody[],
+ });
};
/**
@@ -49,26 +48,26 @@ export const registerCommands = async (
* @returns Handler function
*/
export const buildHandler = (commands: Iterable<Command>) => {
- let internal: Map<String, Command> = new Map();
- for (const command of commands) {
- internal.set(command.json.name, command);
- }
+ const internal = new Map<string, Command>();
+ for (const command of commands) {
+ internal.set(command.json.name, command);
+ }
- return async (
- event: APIInteraction,
- reply?: (data: APIInteractionResponseCallbackData) => void
- ) => {
- console.log("executing: ", event.data);
- if (event.type === InteractionType.ApplicationCommand) {
- console.log("executing: ", event.data);
- let command = internal.get(event.data.name);
+ return async (
+ event: APIInteraction,
+ reply?: (data: APIInteractionResponse) => void,
+ ) => {
+ console.log('executing:', event.data);
+ if (event.type === InteractionType.ApplicationCommand) {
+ console.log('executing:', event.data);
+ const command = internal.get(event.data.name);
- if (command) {
- let data = await command.handler(event);
- console.log("sending reply", data);
+ if (command) {
+ const data = await command.handler(event);
+ console.log('sending reply', data);
- reply(data);
- }
- }
- };
+ reply(data);
+ }
+ }
+ };
};