Skip to content

Commit

Permalink
Move to websockets
Browse files Browse the repository at this point in the history
  • Loading branch information
D4isDAVID committed Sep 9, 2024
1 parent 73b8346 commit 643204c
Show file tree
Hide file tree
Showing 17 changed files with 950 additions and 296 deletions.
824 changes: 733 additions & 91 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"postinstall": "patch-package",
"lint": "prettier --ignore-path .gitignore -c .",
"format": "prettier --ignore-path .gitignore -w .",
"typecheck": "tsc -p server",
Expand All @@ -12,12 +13,13 @@
"@discordjs/core": "1.0.0",
"@discordjs/rest": "2.0.0",
"@discordjs/util": "1.0.0",
"tweetnacl": "^1.0.3"
"@discordjs/ws": "1.0.0"
},
"devDependencies": {
"@citizenfx/server": "^2.0.9780-1",
"@types/node": "16.9.1",
"esbuild": "^0.23.1",
"patch-package": "^8.0.0",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"typescript": "^5.5.4"
Expand Down
14 changes: 14 additions & 0 deletions patches/@discordjs+ws+1.0.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
diff --git a/node_modules/@discordjs/ws/dist/index.mjs b/node_modules/@discordjs/ws/dist/index.mjs
index be08932..4b8fe39 100644
--- a/node_modules/@discordjs/ws/dist/index.mjs
+++ b/node_modules/@discordjs/ws/dist/index.mjs
@@ -220,9 +220,6 @@ var WorkerShardingStrategy = class {
}
resolveWorkerPath() {
const path2 = this.options.workerPath;
- if (!path2) {
- return join(__dirname, "defaultWorker.js");
- }
if (isAbsolute(path2)) {
return path2;
}
7 changes: 7 additions & 0 deletions server/components/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from '../types.js';
import { interactionHandler } from './interaction-handler.js';
import { ready } from './ready.js';

export default {
gatewayEvents: [ready, interactionHandler],
} satisfies Component;
95 changes: 95 additions & 0 deletions server/components/core/interaction-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
ApplicationCommandType,
GatewayDispatchEvents,
InteractionType,
} from '@discordjs/core';
import { interactions, statefuls } from '../loader.js';
import { GatewayEvent } from '../types.js';

function findStateful(id: string, list: string[]): string | undefined {
return list
.filter((s) => id.startsWith(s))
.sort((a, b) => b.length - a.length)[0];
}

export const interactionHandler = {
name: GatewayDispatchEvents.InteractionCreate,
type: 'on',
async execute(props) {
const { data: interaction } = props;

switch (interaction.type) {
case InteractionType.ApplicationCommand:
case InteractionType.ApplicationCommandAutocomplete:
const command = interactions.commands.get(
interaction.data.name,
);

if (!command)
throw new Error(
`Command not defined for ${interaction.data.name}.`,
);

if (
interaction.type === InteractionType.ApplicationCommand &&
(command.data.type ?? ApplicationCommandType.ChatInput) ===
interaction.data.type
)
//@ts-ignore
await command.execute(props);
else if (command.autocomplete)
//@ts-ignore
await command.autocomplete(props);
break;

case InteractionType.MessageComponent:
const componentId = interaction.data.custom_id;

let component = interactions.messageComponents.get(componentId);

if (!component) {
const staticId = findStateful(
componentId,
statefuls.messageComponents,
);

if (staticId)
component =
interactions.messageComponents.get(staticId);
}

if (!component)
throw new Error(
`Message component not defined for ${interaction.data.custom_id}.`,
);

if (component.data.type === interaction.data.component_type)
//@ts-ignore
await component.execute(props);
break;

case InteractionType.ModalSubmit:
const modalId = interaction.data.custom_id;

let modal = interactions.modals.get(modalId);

if (!modal) {
const staticId = findStateful(modalId, statefuls.modals);

if (staticId) modal = interactions.modals.get(staticId);
}

if (!modal)
throw new Error(
`Modal not defined for ${interaction.data.custom_id}.`,
);

//@ts-ignore
await modal.execute(props);
break;

default:
break;
}
},
} satisfies GatewayEvent<GatewayDispatchEvents.InteractionCreate>;
11 changes: 11 additions & 0 deletions server/components/core/ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GatewayDispatchEvents } from '@discordjs/core';
import { GatewayEvent } from '../types.js';

export const ready = {
name: GatewayDispatchEvents.Ready,
type: 'once',
async execute({ data }) {
const { username, discriminator } = data.user;
console.log(`Ready as ${username}#${discriminator}!`);
},
} satisfies GatewayEvent<GatewayDispatchEvents.Ready>;
10 changes: 8 additions & 2 deletions server/components/loader.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Collection } from '@discordjs/collection';
import { RESTPutAPIApplicationCommandsJSONBody } from '@discordjs/core/http-only';
import { RESTPutAPIApplicationCommandsJSONBody } from '@discordjs/core';
import EventEmitter from 'node:events';
import { inspect } from 'node:util';
import { rest } from '../utils/env.js';
import { client, gateway, rest } from '../utils/env.js';
import { isStatefulInteraction } from '../utils/stateful.js';
import core from './core/index.js';
import ping from './ping/index.js';
import {
ApplicationCommand,
Expand Down Expand Up @@ -45,11 +46,15 @@ function registerEvents(emitter: EventEmitter, events: EventsMap[EventName][]) {

function loadComponent({
restEvents,
wsEvents,
gatewayEvents,
commands: componentCommands,
messageComponents,
modals,
}: Component) {
restEvents && registerEvents(rest, restEvents);
wsEvents && registerEvents(gateway, wsEvents);
gatewayEvents && registerEvents(client, gatewayEvents);

componentCommands?.map((command) => {
interactions.commands.set(command.data.name, command);
Expand All @@ -71,5 +76,6 @@ function loadComponent({
}

export function loadComponents() {
loadComponent(core);
loadComponent(ping);
}
30 changes: 20 additions & 10 deletions server/components/ping/command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { WebSocketShardEvents } from '@discordjs/ws';
import { gateway } from '../../utils/env.js';
import { ChatInputCommand } from '../types.js';
import { getPing } from './heartbeat.js';

function pingMessage(p: string) {
return `🏓 Pong! \`${p}\``;
Expand All @@ -9,20 +12,27 @@ export const command = {
name: 'ping',
description: 'Ping command',
},
async execute({ api, data: interaction, cb }) {
cb();
async execute({ api, data: interaction }) {
let replied = false;

const first = Date.now();
await api.interactions.reply(interaction.id, interaction.token, {
content: pingMessage('fetching...'),
});
if (getPing() < 0) {
const heartbeatPromise = new Promise<unknown>((resolve) => {
gateway.once(WebSocketShardEvents.HeartbeatComplete, resolve);
});

const ping = Math.ceil((Date.now() - first) / 2);
await api.interactions.editReply(
interaction.application_id,
await api.interactions.reply(interaction.id, interaction.token, {
content: pingMessage('fetching...'),
});

await heartbeatPromise;
replied = true;
}

await api.interactions[replied ? 'editReply' : 'reply'](
replied ? interaction.application_id : interaction.id,
interaction.token,
{
content: pingMessage(`${ping}ms`),
content: pingMessage(`${getPing()}ms`),
},
);
},
Expand Down
15 changes: 15 additions & 0 deletions server/components/ping/heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { WebSocketShardEvents } from '@discordjs/ws';
import { WebSocketEvent } from '../types.js';

let ping = -1;
export function getPing() {
return ping;
}

export const heartbeat = {
name: WebSocketShardEvents.HeartbeatComplete,
type: 'on',
async execute({ latency }) {
ping = latency;
},
} satisfies WebSocketEvent<WebSocketShardEvents.HeartbeatComplete>;
2 changes: 2 additions & 0 deletions server/components/ping/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Component } from '../types.js';
import { command } from './command.js';
import { heartbeat } from './heartbeat.js';

export default {
wsEvents: [heartbeat],
commands: [command],
} satisfies Component;
37 changes: 26 additions & 11 deletions server/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import {
API,
APIApplicationCommandAutocompleteInteraction,
APIApplicationCommandInteraction,
APIButtonComponentWithCustomId,
APIChannelSelectComponent,
APIChatInputApplicationCommandInteraction,
APIContextMenuInteraction,
APIInteraction,
APIInteractionResponse,
APIMentionableSelectComponent,
APIMessageApplicationCommandInteraction,
APIMessageComponentButtonInteraction,
Expand All @@ -21,17 +19,27 @@ import {
APIUserSelectComponent,
ApplicationCommandType,
ComponentType,
MappedEvents,
RESTPostAPIChatInputApplicationCommandsJSONBody,
RESTPostAPIContextMenuApplicationCommandsJSONBody,
} from '@discordjs/core/http-only';
WithIntrinsicProps,
} from '@discordjs/core';
import { RestEvents } from '@discordjs/rest';
import { Awaitable } from '@discordjs/util';
import { ManagerShardEventsMap } from '@discordjs/ws';

export type EventName = keyof RestEvents;
export type EventName =
| keyof RestEvents
| keyof ManagerShardEventsMap
| keyof MappedEvents;

export type EventExecuteArgs<T extends EventName> = T extends keyof RestEvents
? RestEvents[T]
: never;
: T extends keyof ManagerShardEventsMap
? ManagerShardEventsMap[T]
: T extends keyof MappedEvents
? MappedEvents[T]
: never;

export interface IEvent<T extends EventName> {
readonly type: 'on' | 'once';
Expand Down Expand Up @@ -72,11 +80,8 @@ export type InteractionData<T extends APIInteraction> =
? APIModalInteractionResponseCallbackData
: never;

export type InteractionExecuteArgs<T extends APIInteraction> = {
api: API;
data: T;
cb: (response?: APIInteractionResponse) => void;
};
export type InteractionExecuteArgs<T extends APIInteraction> =
WithIntrinsicProps<T>;

export interface IInteraction<T extends APIInteraction> {
readonly data: InteractionData<T>;
Expand All @@ -92,11 +97,19 @@ export type SelectMenuInteractionWithType<T extends ComponentType> =
APIMessageComponentSelectMenuInteraction & { data: { component_type: T } };

export type RestEvent<T extends keyof RestEvents> = IEvent<T>;
export type WebSocketEvent<T extends keyof ManagerShardEventsMap> = IEvent<T>;
export type GatewayEvent<T extends keyof MappedEvents> = IEvent<T>;

export type RestEventsMap = {
[T in keyof RestEvents]: RestEvent<T>;
};
export type EventsMap = RestEventsMap;
export type WebSocketEventsMap = {
[T in keyof ManagerShardEventsMap]: WebSocketEvent<T>;
};
export type GatewayEventsMap = {
[T in keyof MappedEvents]: GatewayEvent<T>;
};
export type EventsMap = RestEventsMap & WebSocketEventsMap & GatewayEventsMap;

export type ChatInputCommand =
IInteraction<APIChatInputApplicationCommandInteraction>;
Expand Down Expand Up @@ -134,6 +147,8 @@ export type Modal = IInteraction<APIModalSubmitInteraction>;

export interface Component {
readonly restEvents?: RestEventsMap[keyof RestEvents][];
readonly wsEvents?: WebSocketEventsMap[keyof ManagerShardEventsMap][];
readonly gatewayEvents?: GatewayEventsMap[keyof MappedEvents][];
readonly commands?: ApplicationCommand[];
readonly messageComponents?: MessageComponent[];
readonly modals?: Modal[];
Expand Down
Loading

0 comments on commit 643204c

Please sign in to comment.