summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/index.ts3
-rw-r--r--src/commands/ping.ts86
-rw-r--r--src/events/client.ts33
-rw-r--r--src/events/event-emitter.ts51
-rw-r--r--src/events/index.ts1
-rw-r--r--src/events/transport.ts111
-rw-r--r--src/handler/builder.ts19
-rw-r--r--src/handler/index.ts57
-rw-r--r--src/index.ts52
-rw-r--r--src/register.ts5
-rw-r--r--src/rest.ts8
11 files changed, 426 insertions, 0 deletions
diff --git a/src/commands/index.ts b/src/commands/index.ts
new file mode 100644
index 0000000..366e781
--- /dev/null
+++ b/src/commands/index.ts
@@ -0,0 +1,3 @@
+import { ping } from "./ping";
+
+export const commands = [ping];
diff --git a/src/commands/ping.ts b/src/commands/ping.ts
new file mode 100644
index 0000000..9bf6bc0
--- /dev/null
+++ b/src/commands/ping.ts
@@ -0,0 +1,86 @@
+import {
+ APIApplicationCommandInteraction,
+ APIInteractionResponse,
+ ApplicationCommandOptionType,
+ ApplicationCommandType,
+ InteractionResponseType,
+} from "discord-api-types/v10";
+import { CommandBuilder, HandlerFn } from "../handler";
+import { promise } from "ping";
+
+type Messages = {
+ latency_message: [number];
+ failure_message: [string];
+};
+
+type Locales = {
+ [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}\`\`\``,
+ },
+};
+
+const resolve = (
+ msg: keyof Messages,
+ locale: APIApplicationCommandInteraction["locale"],
+ ...args: Messages[typeof msg]
+) => (locales[msg][locale] ?? locales[msg]["_"])(args);
+
+const handler: HandlerFn = async (
+ 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 (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),
+ },
+ };
+ }
+ }
+};
+
+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();
diff --git a/src/events/client.ts b/src/events/client.ts
new file mode 100644
index 0000000..b4045a0
--- /dev/null
+++ b/src/events/client.ts
@@ -0,0 +1,33 @@
+import { GatewayDispatchPayload } from "discord-api-types/v10";
+import { BaseEventEmitter, HandlerFunction } from "./event-emitter";
+import { Transport, TransportOptions } from "./transport";
+import { CamelCase } from "type-fest";
+
+type ExtractEvent<O extends GatewayDispatchPayload, U extends O["t"]> = Extract<
+ O & { t: Exclude<O["t"], Exclude<O["t"], U>> },
+ { t: U }
+>;
+
+export type Events = {
+ [P in GatewayDispatchPayload["t"] as `${CamelCase<P>}` | P]: [
+ ExtractEvent<GatewayDispatchPayload, P>["d"]
+ ];
+};
+
+export class EventClient extends BaseEventEmitter {
+ public transport: Transport;
+ // constructs
+ constructor() {
+ super();
+ this.transport = new Transport(this);
+ }
+
+ public async start(options: TransportOptions) {
+ await this.transport.start(options);
+ }
+
+ on<K extends keyof Events>(name: K, fn: HandlerFunction<Events[K]>): this {
+ this.transport.subscribe(name);
+ return super.on(name, fn);
+ }
+}
diff --git a/src/events/event-emitter.ts b/src/events/event-emitter.ts
new file mode 100644
index 0000000..8a106c9
--- /dev/null
+++ b/src/events/event-emitter.ts
@@ -0,0 +1,51 @@
+import { EventEmitter } from "events";
+import { PascalCase } from "type-fest";
+import { Events } from ".";
+
+export type HandlerFunction<Args extends unknown[]> = (
+ ...args: [...Args, ...[resolve?: (data: object) => void]]
+) => unknown | Promise<unknown>;
+
+export type EventsFunctions = {
+ [P in keyof Events as P extends string ? `on${PascalCase<P>}` : never]: (
+ fn: HandlerFunction<Events[P]>
+ ) => BaseEventEmitter;
+};
+
+// Typings for the EventClient
+export interface BaseEventEmitter extends EventEmitter {
+ addListener<K extends keyof Events>(
+ name: K,
+ fn: HandlerFunction<Events[K]>
+ ): this;
+
+ on<K extends keyof Events>(name: K, fn: HandlerFunction<Events[K]>): this;
+
+ once<K extends keyof Events>(name: K, fn: HandlerFunction<Events[K]>): this;
+
+ off<K extends keyof Events>(name: K, fn: HandlerFunction<Events[K]>): this;
+
+ prependListener<K extends keyof Events>(
+ name: K,
+ fn: HandlerFunction<Events[K]>
+ ): this;
+
+ prependOnceListener<K extends keyof Events>(
+ name: K,
+ fn: HandlerFunction<Events[K]>
+ ): this;
+
+ removeAllListeners(eventName: keyof Events | undefined): this;
+ removeListener(eventName: keyof Events): this;
+
+ emit<T extends keyof Events>(name: T, ...args: Events[T]): boolean;
+ listenerCount(event: keyof Events): number;
+ listeners<T extends keyof Events>(event: T): HandlerFunction<Events[T]>[];
+ rawListeners: this["listeners"];
+}
+
+export class BaseEventEmitter extends EventEmitter implements BaseEventEmitter {
+ constructor() {
+ super();
+ }
+}
diff --git a/src/events/index.ts b/src/events/index.ts
new file mode 100644
index 0000000..5ec7692
--- /dev/null
+++ b/src/events/index.ts
@@ -0,0 +1 @@
+export * from "./client";
diff --git a/src/events/transport.ts b/src/events/transport.ts
new file mode 100644
index 0000000..4f2abd6
--- /dev/null
+++ b/src/events/transport.ts
@@ -0,0 +1,111 @@
+import { connect, ConnectionOptions, NatsConnection } from "nats";
+import { EventClient, Events } from ".";
+import globRegex from "glob-regex";
+
+export type TransportOptions = {
+ additionalEvents?: (keyof Events)[];
+ nats?: ConnectionOptions;
+ queue: string;
+};
+export class Transport {
+ private nats: NatsConnection | null = null;
+ private subscription: Map<string, Function> = new Map();
+ private queue?: string;
+ private events: Set<string> = new Set();
+
+ constructor(private emitter: EventClient) {}
+
+ public async start(options: TransportOptions) {
+ this.nats = await connect(options?.nats);
+ this.queue = options.queue;
+ if (options.additionalEvents) {
+ options.additionalEvents.forEach((a) => this.events.add(a));
+ }
+
+ let initial_events = [...this.events];
+
+ for (let subscription of initial_events) {
+ await this.subscribe(subscription);
+ }
+ }
+
+ public async subscribe(event: string) {
+ if (!this.nats) {
+ console.log("Requesting event " + event);
+ this.events.add(event);
+ return;
+ }
+ let dashed = event.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase());
+ event = `nova.cache.dispatch.${dashed.toUpperCase()}`;
+
+ let isAlreadyPresent = [...this.subscription.keys()].reduce(
+ (previous, current) => {
+ if (previous) return previous;
+ let regex = globRegex(current);
+
+ return regex.test(event);
+ },
+ false
+ );
+
+ if (isAlreadyPresent) {
+ console.warn("nats subscription already covered.");
+ return;
+ }
+
+ let regex = globRegex(event);
+ [...this.subscription.keys()].map((key) => {
+ if (regex.test(key)) {
+ let v = this.subscription.get(key);
+ if (!v) {
+ return;
+ }
+
+ console.log(`unsubscribing from ${key}`);
+ v();
+ }
+ });
+
+ this._subTask(event, this.queue || "default_queue");
+ }
+
+ private async _subTask(event: string, queue: string) {
+ if (!this.nats) {
+ throw new Error("nats transporter is not started.");
+ }
+
+ console.log(`subscribing to ${event}`);
+ let resolve: Function = () => {};
+ let task = new Promise((r) => {
+ resolve = r;
+ });
+ let sub = this.nats.subscribe(event, { queue: "" });
+
+ const fn = async () => {
+ for await (let data of sub) {
+ let string = Buffer.from(data.data).toString("utf-8");
+ let d = JSON.parse(string);
+ let respond: Function | null = null;
+
+ if (data.reply) {
+ console.log("expecting reply.");
+ respond = (d: object) => {
+ data.respond(Buffer.from(JSON.stringify(d), "utf-8"));
+ };
+ }
+ const camelCased = d.t.toLowerCase().replace(/_([a-z])/g, function (g) {
+ return g[1].toUpperCase();
+ });
+ console.log("envoi de ", camelCased);
+ this.emitter.emit(camelCased, d.d, respond);
+ }
+ };
+ this.subscription.set(event, resolve);
+
+ await Promise.race([task, fn()]);
+
+ console.log(`finished task for ${event}`);
+ sub.unsubscribe();
+ this.subscription.delete(event);
+ }
+}
diff --git a/src/handler/builder.ts b/src/handler/builder.ts
new file mode 100644
index 0000000..148542a
--- /dev/null
+++ b/src/handler/builder.ts
@@ -0,0 +1,19 @@
+import { SlashCommandBuilder } from "@discordjs/builders";
+import { Command, HandlerFn } from ".";
+
+export class CommandBuilder extends SlashCommandBuilder {
+ private _handler: HandlerFn;
+
+ constructor() {
+ super();
+ }
+
+ handler(handler: HandlerFn): this {
+ this._handler = handler;
+ return this;
+ }
+
+ build(): Command {
+ return { json: this.toJSON(), handler: this._handler };
+ }
+}
diff --git a/src/handler/index.ts b/src/handler/index.ts
new file mode 100644
index 0000000..8126717
--- /dev/null
+++ b/src/handler/index.ts
@@ -0,0 +1,57 @@
+import { REST } from "@discordjs/rest";
+import {
+ APIApplicationCommandInteraction,
+ APIInteraction,
+ APIInteractionResponse,
+ InteractionType,
+ RESTPostAPIApplicationCommandsJSONBody,
+ RESTPostAPIChatInputApplicationCommandsJSONBody,
+ RESTPostAPIWebhookWithTokenJSONBody,
+ Routes,
+} from "discord-api-types/v10";
+import { rest } from "../rest";
+
+export * from "./builder";
+
+export type HandlerFn = (
+ data: APIApplicationCommandInteraction
+) => PromiseLike<APIInteractionResponse>;
+export type PromiseLike<T> = T | Promise<T>;
+export type Command = {
+ json: RESTPostAPIChatInputApplicationCommandsJSONBody;
+ handler: HandlerFn;
+};
+
+export const registerCommands = async (
+ commands: Iterable<Command>,
+ rest: REST,
+ applicationId: string
+) => {
+ for (const command of commands) {
+ await rest.post(Routes.applicationCommands(applicationId), {
+ body: command.json as RESTPostAPIApplicationCommandsJSONBody,
+ });
+ }
+};
+
+export const buildHandler = (commands: Iterable<Command>) => {
+ let internal: Map<String, Command> = new Map();
+ for (const command of commands) {
+ internal.set(command.json.name, command);
+ }
+
+ return async (event: APIInteraction, reply?: (data: object) => void) => {
+ console.log("executing: ", event.data);
+ if (event.type === InteractionType.ApplicationCommand) {
+ console.log("executing: ", event.data);
+ let command = internal.get(event.data.name);
+
+ if (command) {
+ let data = await command.handler(event);
+ console.log("sending reply", data);
+
+ reply(data);
+ }
+ }
+ };
+};
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..eb8707f
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,52 @@
+import { EventClient } from "./events/index";
+import {
+ RESTPostAPIChannelMessageResult,
+ Routes,
+} from "discord-api-types/v10";
+import { rest } from "./rest";
+import { buildHandler } from "./handler";
+import { commands } from "./commands";
+
+const emitter = new EventClient();
+emitter.on("interactionCreate", buildHandler(commands));
+
+emitter.on("messageCreate", async (message) => {
+ console.log(message);
+ if (message.content.toLowerCase() == "salut") {
+ await rest.post(Routes.channelMessages(message.channel_id), {
+ body: {
+ content: `Salut <@${message.author.id}> :wink:`,
+ },
+ });
+ } else if (message.content.toLocaleLowerCase() == "~ping") {
+ let t1 = new Date().getTime();
+ let sentMessage = <RESTPostAPIChannelMessageResult>await rest.post(
+ Routes.channelMessages(message.channel_id),
+ {
+ body: {
+ content: `Calcul du ping...`,
+ },
+ }
+ );
+ let time = new Date().getTime() - t1;
+
+ await rest.patch(
+ Routes.channelMessage(message.channel_id, sentMessage.id),
+ {
+ body: {
+ content: `Le ping de <@${sentMessage.author.id}> est de \`${time}ms\``,
+ },
+ }
+ );
+ }
+});
+
+emitter
+ .start({
+ additionalEvents: [],
+ nats: {
+ servers: ["localhost:4222"],
+ },
+ queue: "nova-worker-common",
+ })
+ .catch(console.log);
diff --git a/src/register.ts b/src/register.ts
new file mode 100644
index 0000000..4672585
--- /dev/null
+++ b/src/register.ts
@@ -0,0 +1,5 @@
+import { commands } from "./commands";
+import { registerCommands } from "./handler";
+import { rest } from "./rest";
+
+registerCommands(commands, rest, "807188335717384212"); \ No newline at end of file
diff --git a/src/rest.ts b/src/rest.ts
new file mode 100644
index 0000000..9bee0cb
--- /dev/null
+++ b/src/rest.ts
@@ -0,0 +1,8 @@
+require('source-map-support').install();
+import { REST } from "@discordjs/rest";
+
+export const rest = new REST({
+ version: "10",
+ headers: { Authorization: "" },
+ api: "http://localhost:8090/api",
+}).setToken("_");