summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMatthieuCoder <matthieu@matthieu-dev.xyz>2023-01-17 18:23:51 +0400
committerMatthieuCoder <matthieu@matthieu-dev.xyz>2023-01-17 18:23:51 +0400
commitb4bc9e35871ffefaddb8803ea02735cd4c465e45 (patch)
tree12758366ea5bd4a1cab323f206a73a356317498e /src
parentb9251019bf2ad92542fde24b5d06059068455a1e (diff)
edit
Diffstat (limited to 'src')
-rw-r--r--src/commands/index.ts4
-rw-r--r--src/commands/loveCalculator.ts134
-rw-r--r--src/commands/ping.ts95
-rw-r--r--src/index.ts40
-rw-r--r--src/register.ts18
-rw-r--r--src/sys/events/client.ts149
-rw-r--r--src/sys/events/index.ts1
-rw-r--r--src/sys/events/transport.ts185
-rw-r--r--src/sys/handler/builder.ts18
-rw-r--r--src/sys/handler/index.ts73
10 files changed, 0 insertions, 717 deletions
diff --git a/src/commands/index.ts b/src/commands/index.ts
deleted file mode 100644
index 79ea03a..0000000
--- a/src/commands/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { loveCalculator } from './loveCalculator';
-import {ping} from './ping';
-
-export const commands = [ping, loveCalculator];
diff --git a/src/commands/loveCalculator.ts b/src/commands/loveCalculator.ts
deleted file mode 100644
index 3e42dab..0000000
--- a/src/commands/loveCalculator.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import {
- type APIApplicationCommandInteraction,
- type APIInteractionResponse,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- InteractionResponseType,
- APIUser,
- MessageFlags,
-} from "discord-api-types/v10";
-import { CommandBuilder, type HandlerFn } from "../sys/handler";
-
-type Messages = {
- low: [];
- high: [];
- average: [];
- loveMessage: [APIUser, APIUser, number];
- sameUser: [];
-};
-
-type Locales = {
- [K in keyof Messages]: Partial<
- Record<
- APIApplicationCommandInteraction["locale"] | "_",
- (...args: Messages[K]) => string
- >
- >;
-};
-
-const locales: Locales = {
- low: {
- fr: () => "déteste",
- _: () => "hates",
- },
- average: {
- fr: () => "est compatible avec",
- _: () => "is compatible with",
- },
- high: {
- fr: () => "est fou/folle de",
- _: () => "is in love with",
- },
-
- sameUser: {
- fr: () => "Vous devez choisir deux utilisateurs distincts",
- _: () => "You can't calculate love between the same user!",
- },
-
- loveMessage: {
- fr: (user1: APIUser, user2: APIUser, percentage: number) =>
- `**${percentage}%**! ${user1.username}#${user1.discriminator} ${resolve(
- percentage > 80 ? "high" : percentage <= 35 ? "low" : "average",
- "fr"
- )} ${user2.username}#${user2.discriminator}`,
- _: (user1: APIUser, user2: APIUser, percentage: number) =>
- `**${percentage}%**! ${user1.username}#${user1.discriminator} ${resolve(
- percentage > 80 ? "high" : percentage <= 35 ? "low" : "average",
- "_"
- )} ${user2.username}#${user2.discriminator}`,
- },
-};
-
-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,
- locale,
-}: APIApplicationCommandInteraction): Promise<APIInteractionResponse> => {
- if (
- data.type === ApplicationCommandType.ChatInput &&
- data.options[0].name === "user1" &&
- data.options[0].type === ApplicationCommandOptionType.User &&
- data.options[1].name === "user2" &&
- data.options[1].type === ApplicationCommandOptionType.User
- ) {
- let user1 = data.resolved.users[data.options[0].value];
- let user2 = data.resolved.users[data.options[1].value];
-
- if (user1.id === user2.id) {
- return {
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- flags: MessageFlags.Ephemeral,
- content: resolve("sameUser", locale),
- },
- };
- }
-
- let percentage = Math.round(Math.random() * 100);
- return {
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- content: resolve("loveMessage", locale, user1, user2, percentage),
- },
- };
- }
-};
-
-export const loveCalculator = new CommandBuilder()
- .setName("calculate-love")
- .handler(handler)
- .setDescription("Calcule the love compatibility between two users")
- .addUserOption((option) =>
- option
- .setName("user1")
- .setDescription("The first user of the couple")
- .setDescriptionLocalization("fr", "Le premier membre du couple")
- .setNameLocalization("fr", "premier_utilisateur")
- .setNameLocalization("en-US", "first_user")
- .setRequired(true)
- )
- .addUserOption((option) =>
- option
- .setName("user2")
- .setDescription("The second user of the couple")
- .setDescriptionLocalization("fr", "Le deuxième membre du couple")
- .setNameLocalization("fr", "second_utilisateur")
- .setNameLocalization("en-US", "second_user")
- .setRequired(true)
- )
- .setDescriptionLocalizations({
- fr: "Calcule l'amour entre deux membres",
- })
- .setNameLocalization("fr", "calculer-compatibilite")
- .setDMPermission(false)
- .build();
diff --git a/src/commands/ping.ts b/src/commands/ping.ts
deleted file mode 100644
index f92485b..0000000
--- a/src/commands/ping.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import {
- type APIApplicationCommandInteraction,
- type APIInteractionResponse,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- InteractionResponseType,
-} from 'discord-api-types/v10';
-import {promise} from 'ping';
-import {CommandBuilder, type HandlerFn} from '../sys/handler';
-
-type Messages = {
- latencyMessage: [number];
- failureMessage: [string];
-};
-
-type Locales = {
- [K in keyof Messages]: Partial<
- Record<
- APIApplicationCommandInteraction['locale'] | '_',
- (...args: Messages[K]) => string
- >
- >;
-};
-
-const locales: Locales = {
- 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 = <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,
-): Promise<APIInteractionResponse> => {
- 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(
- '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();
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index af786e1..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-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',
- },
-});
-
-// We register our slash command handler.
-emitter.on('interactionCreate', buildHandler(commands));
-
-// Simple message handler
-emitter.onMessageCreate(async (message) => {
- if (message.content === '~ping') {
- await message.client.channels.createMessage(message.channel_id, {
- content: `Bonjour! <@${message.author.id}>`,
- });
- } else if (message.content === '~pid') {
-
- await message.client.channels.createMessage(message.channel_id, {
- content: `Mon pid est ${process.pid}`,
- });
- }
-});
-
-// We connect ourselves to the nova nats broker.
-(async () => emitter.start())();
diff --git a/src/register.ts b/src/register.ts
deleted file mode 100644
index 1758957..0000000
--- a/src/register.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import "source-map-support";
-import { REST } from "@discordjs/rest";
-import { commands } from "./commands";
-import { registerCommands } from "./sys/handler";
-import { RESTGetAPIUserResult, Routes } from "discord-api-types/v10";
-
-const rest = new REST({
- version: "10",
- api: "http://localhost:8090/api",
-}).setToken("_");
-
-/**
- * We register the commands with discord
- */
-(async () => {
- let self = (await rest.get(Routes.user("@me"))) as RESTGetAPIUserResult;
- registerCommands(commands, rest, self.id);
-})();
diff --git a/src/sys/events/client.ts b/src/sys/events/client.ts
deleted file mode 100644
index bad8203..0000000
--- a/src/sys/events/client.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import {EventEmitter} from 'node:stream';
-import {type CamelCase, type PascalCase} from 'type-fest';
-import {REST, type RESTOptions} from '@discordjs/rest';
-import {
- type APIInteractionResponse,
- type GatewayDispatchPayload,
- type GatewayInteractionCreateDispatchData,
-} from 'discord-api-types/v10';
-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 all the union members that have a matching O['t']
- * Example:
- * type Variant1 = { t: 'type1', myProperty: 1 };
- * type Variant2 = { t: 'type2', anotherProperty: 2 };
- * type ExampleUnion = Variant1 | Variant2;
- *
- * let variant1: ExtractVariant<ExampleUnion, 'type1'>; // Type of variant1 is Variant1
- * 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}
->;
-
-/**
- * Add intrisics properties to the event, such as `client` and `rest`
- */
-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
-) => PromiseLike<void>;
-
-export type EventTypes = {
- [P in GatewayDispatchPayload['t']]: WithIntrisics<
- ExtractVariant<GatewayDispatchPayload, P>['d']
- >;
-};
-
-/**
- * Maps all events from GatewayDispatchPayload['t'] (GatewayDispatchEvents) and maps them to a camelcase event name
- * Also reteives the type of the event using ExtractEvent
- */
-export type EventsHandlerArguments = {
- [P in keyof EventTypes as `${CamelCase<P>}`]: HandlerFunction<
- [EventTypes[P]]
- >;
-} & {
- interactionCreate: HandlerFunction<
- [
- WithIntrisics<GatewayInteractionCreateDispatchData>,
- (interactionCreate: APIInteractionResponse) => void,
- ]
- >;
-};
-
-/**
- * Defines all the 'on...' functions on the client
- * 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;
-};
-
-/**
- * Defines all the methods known to be implemented
- */
-type ClientFunctions = Record<string, unknown> &
- EventsFunctions &
- TypedEmitter<EventsHandlerArguments> &
- API;
-
-/**
- * The real extended class is an EventEmitter.
- */
-// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
-const undefinedClient: new () => ClientFunctions = EventEmitter as any;
-
-/**
- * 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 {
- 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).setToken('_');
- this.api = new API(this.rest);
-
- // Using a proxy to provide the 'on...' functionality
- let self = 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];
- },
- });
-
- this.transport = new Transport(self, options.transport);
-
- // 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);
- });
-
- return self;
- }
-
- public async start() {
- return this.transport.start();
- }
-}
diff --git a/src/sys/events/index.ts b/src/sys/events/index.ts
deleted file mode 100644
index 4f1cce4..0000000
--- a/src/sys/events/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './client';
diff --git a/src/sys/events/transport.ts b/src/sys/events/transport.ts
deleted file mode 100644
index c3c9f7a..0000000
--- a/src/sys/events/transport.ts
+++ /dev/null
@@ -1,185 +0,0 @@
-import { Buffer } from "node:buffer";
-import {
- connect,
- type ConnectionOptions,
- type NatsConnection,
- type Subscription,
-} from "nats";
-import globRegex from "glob-regex";
-import {
- type APIInteractionResponse,
- InteractionResponseType,
- type APIInteractionResponseCallbackData,
- type GatewayDispatchPayload,
- Routes,
-} from "discord-api-types/v10";
-import { type CamelCase } from "type-fest";
-import { type Client, type EventName, type EventsHandlerArguments } from ".";
-
-/**
- * Options for the nats transport layer
- */
-export type TransportOptions = {
- 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 | 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 || "nova_consumer",
- });
- 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
deleted file mode 100644
index 71732a8..0000000
--- a/src/sys/handler/builder.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {SlashCommandBuilder} from '@discordjs/builders';
-import {type Command, type HandlerFn} from '.';
-
-/**
- * Simple wrapper around the SlashCommandBuilder provided by Discord.js
- */
-export class CommandBuilder extends SlashCommandBuilder {
- private _handler: HandlerFn;
-
- handler(handler: HandlerFn): this {
- this._handler = handler;
- return this;
- }
-
- build(): Command {
- return {json: this.toJSON(), handler: this._handler};
- }
-}
diff --git a/src/sys/handler/index.ts b/src/sys/handler/index.ts
deleted file mode 100644
index 1512ab7..0000000
--- a/src/sys/handler/index.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import {type REST} from '@discordjs/rest';
-import {
- type APIApplicationCommandInteraction,
- type APIInteraction,
- InteractionType,
- type RESTPostAPIApplicationCommandsJSONBody,
- type RESTPostAPIChatInputApplicationCommandsJSONBody,
- Routes,
- type APIInteractionResponse,
-} from 'discord-api-types/v10';
-
-export * from './builder';
-
-export type PromiseLike<T> = T | Promise<T>;
-/**
- * A simple function that executes a slash command.
- */
-export type HandlerFn = (
- data: APIApplicationCommandInteraction,
-) => PromiseLike<APIInteractionResponse>;
-
-export type Command = {
- json: RESTPostAPIChatInputApplicationCommandsJSONBody;
- handler: HandlerFn;
-};
-
-/**
- * Register all the commands to discord
- * @param commands List of commands to register
- * @param rest Rest api instance
- * @param applicationId Current application id
- */
-export const registerCommands = async (
- commands: Iterable<Command>,
- rest: REST,
- appId: string,
-) => {
- await rest.put(Routes.applicationCommands(appId), {
- body: [...commands].map(
- (x) => x.json,
- ) as RESTPostAPIApplicationCommandsJSONBody[],
- });
-};
-
-/**
- * Creates a new handler to handle the slash commands.
- * @param commands List of commands to handle
- * @returns Handler function
- */
-export const buildHandler = (commands: Iterable<Command>) => {
- const internal = new Map<string, Command>();
- for (const command of commands) {
- internal.set(command.json.name, command);
- }
-
- 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) {
- const data = await command.handler(event);
- console.log('sending reply', data);
-
- reply(data);
- }
- }
- };
-};