nubis 0.0.0 → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/dist/api.d.ts +12 -0
- package/dist/bot.d.ts +45 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1017 -0
- package/dist/commands.d.ts +24 -0
- package/dist/config-loader.d.ts +10 -0
- package/dist/config.d.ts +51 -0
- package/dist/env.d.ts +15 -0
- package/dist/http.d.ts +41 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +295 -0
- package/dist/intents.d.ts +11 -0
- package/dist/loader.d.ts +33 -0
- package/dist/node-listener.d.ts +8 -0
- package/dist/node.d.ts +19 -0
- package/dist/node.js +592 -0
- package/dist/rest-transport.d.ts +16 -0
- package/dist/route.d.ts +17 -0
- package/dist/run-cli.d.ts +10 -0
- package/dist/sync.d.ts +26 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/package.json +56 -7
package/dist/node.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
var __esm = (fn, res, err) => () => {
|
|
2
|
+
if (fn)
|
|
3
|
+
try {
|
|
4
|
+
res = fn(fn = 0);
|
|
5
|
+
} catch (e) {
|
|
6
|
+
err = [e];
|
|
7
|
+
}
|
|
8
|
+
if (err)
|
|
9
|
+
throw err[0];
|
|
10
|
+
return res;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/loader.ts
|
|
14
|
+
import { readdir, stat } from "node:fs/promises";
|
|
15
|
+
import { extname, join, relative, sep } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import {
|
|
18
|
+
buildCommandTree,
|
|
19
|
+
createComponentRegistry,
|
|
20
|
+
createEventRegistry,
|
|
21
|
+
NubisError
|
|
22
|
+
} from "@nubisjs/core";
|
|
23
|
+
async function listModules(dir, extensions) {
|
|
24
|
+
let names;
|
|
25
|
+
try {
|
|
26
|
+
names = await readdir(dir);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === "ENOENT")
|
|
29
|
+
return [];
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const name of names.sort()) {
|
|
34
|
+
const path = join(dir, name);
|
|
35
|
+
if ((await stat(path)).isDirectory()) {
|
|
36
|
+
files.push(...await listModules(path, extensions));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const base = name.slice(0, name.length - extname(name).length);
|
|
40
|
+
if (!extensions.includes(extname(name)))
|
|
41
|
+
continue;
|
|
42
|
+
if (name.endsWith(".d.ts") || /\.(test|spec)$/.test(base))
|
|
43
|
+
continue;
|
|
44
|
+
if (base.startsWith("_") && base !== FOLDER_FILE)
|
|
45
|
+
continue;
|
|
46
|
+
files.push(path);
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
}
|
|
50
|
+
async function importModule(file, root, version) {
|
|
51
|
+
const url = pathToFileURL(file);
|
|
52
|
+
if (version !== undefined)
|
|
53
|
+
url.searchParams.set("v", String(version));
|
|
54
|
+
try {
|
|
55
|
+
return await import(url.href);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const code = error.code;
|
|
58
|
+
if (code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
59
|
+
throw new NubisError(`${relative(root, file)}: this runtime can't import ${extname(file)} files. Run the bot with Bun or Node 22.18+, or build it to JavaScript first.`);
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function loadProject(root, options = {}) {
|
|
65
|
+
const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
|
|
66
|
+
const load = (file) => importModule(file, root, options.version);
|
|
67
|
+
const where = (file) => relative(root, file);
|
|
68
|
+
const files = [];
|
|
69
|
+
const commandsDir = join(root, "commands");
|
|
70
|
+
const commandFiles = [];
|
|
71
|
+
const folders = [];
|
|
72
|
+
for (const file of await listModules(commandsDir, extensions)) {
|
|
73
|
+
files.push(where(file));
|
|
74
|
+
const module = await load(file);
|
|
75
|
+
const segments = segmentsOf(file, commandsDir);
|
|
76
|
+
if (segments.at(-1) === FOLDER_FILE) {
|
|
77
|
+
const settings = module.default;
|
|
78
|
+
if (!isObject(settings)) {
|
|
79
|
+
throw new NubisError(`${where(file)}: export the folder's settings as default, e.g. export default folder({ description: "..." }).`);
|
|
80
|
+
}
|
|
81
|
+
folders.push({ ...settings, path: segments.slice(0, -1) });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!isCommand(module.default)) {
|
|
85
|
+
throw new NubisError(`${where(file)}: export a command as default, e.g. export default command({ ... }). Helpers that aren't commands can live in files starting with "_".`);
|
|
86
|
+
}
|
|
87
|
+
commandFiles.push({ path: segments, definition: module.default });
|
|
88
|
+
}
|
|
89
|
+
const components = [];
|
|
90
|
+
for (const file of await listModules(join(root, "components"), extensions)) {
|
|
91
|
+
files.push(where(file));
|
|
92
|
+
const found = Object.values(await load(file)).filter(isComponent);
|
|
93
|
+
if (found.length === 0) {
|
|
94
|
+
throw new NubisError(`${where(file)}: no buttons, selects or modals are exported here.`);
|
|
95
|
+
}
|
|
96
|
+
for (const component of found) {
|
|
97
|
+
if (!components.includes(component))
|
|
98
|
+
components.push(component);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const menus = [];
|
|
102
|
+
for (const file of await listModules(join(root, "menus"), extensions)) {
|
|
103
|
+
files.push(where(file));
|
|
104
|
+
const menu = (await load(file)).default;
|
|
105
|
+
if (!isMenu(menu)) {
|
|
106
|
+
throw new NubisError(`${where(file)}: export a context menu command as default, e.g. export default userCommand({ ... }).`);
|
|
107
|
+
}
|
|
108
|
+
menus.push(menu);
|
|
109
|
+
}
|
|
110
|
+
const events = [];
|
|
111
|
+
for (const file of await listModules(join(root, "events"), extensions)) {
|
|
112
|
+
files.push(where(file));
|
|
113
|
+
const exported = (await load(file)).default;
|
|
114
|
+
const list = Array.isArray(exported) ? exported : [exported];
|
|
115
|
+
if (list.length === 0 || !list.every(isEvent)) {
|
|
116
|
+
throw new NubisError(`${where(file)}: export an event as default, e.g. export default event("guildMemberAdd", ...), or an array of them.`);
|
|
117
|
+
}
|
|
118
|
+
events.push(...list);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
root,
|
|
122
|
+
tree: buildCommandTree(commandFiles, folders),
|
|
123
|
+
components: createComponentRegistry(components),
|
|
124
|
+
menus,
|
|
125
|
+
events: createEventRegistry(events),
|
|
126
|
+
files
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
var DEFAULT_EXTENSIONS, FOLDER_FILE = "_folder", isObject = (value) => (typeof value === "object" || typeof value === "function") && value !== null, isCommand = (value) => isObject(value) && value.kind === "command", isComponent = (value) => isObject(value) && ("~nubis" in value), isMenu = (value) => isObject(value) && (value.kind === "user" || value.kind === "message") && ("config" in value), isEvent = (value) => isObject(value) && value.kind === "event", segmentsOf = (file, dir) => {
|
|
130
|
+
const path = relative(dir, file);
|
|
131
|
+
return path.slice(0, path.length - extname(path).length).split(sep);
|
|
132
|
+
};
|
|
133
|
+
var init_loader = __esm(() => {
|
|
134
|
+
DEFAULT_EXTENSIONS = [".ts", ".mts", ".js", ".mjs"];
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/node-listener.ts
|
|
138
|
+
function toNodeListener(handler) {
|
|
139
|
+
return (incoming, outgoing) => {
|
|
140
|
+
(async () => {
|
|
141
|
+
try {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
for await (const chunk of incoming)
|
|
144
|
+
chunks.push(chunk);
|
|
145
|
+
const headers = new Headers;
|
|
146
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
147
|
+
if (typeof value === "string")
|
|
148
|
+
headers.set(name, value);
|
|
149
|
+
else if (Array.isArray(value))
|
|
150
|
+
headers.set(name, value.join(", "));
|
|
151
|
+
}
|
|
152
|
+
const method = incoming.method ?? "GET";
|
|
153
|
+
const request = new Request(`http://${incoming.headers.host ?? "localhost"}${incoming.url ?? "/"}`, {
|
|
154
|
+
method,
|
|
155
|
+
headers,
|
|
156
|
+
body: method === "GET" || method === "HEAD" ? undefined : Buffer.concat(chunks)
|
|
157
|
+
});
|
|
158
|
+
const response = await handler(request);
|
|
159
|
+
outgoing.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
160
|
+
outgoing.end(Buffer.from(await response.arrayBuffer()));
|
|
161
|
+
} catch (error) {
|
|
162
|
+
outgoing.writeHead(500, { "content-type": "text/plain" });
|
|
163
|
+
outgoing.end("Internal error");
|
|
164
|
+
console.error(error);
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/intents.ts
|
|
171
|
+
import { GatewayIntentBits as I } from "discord-api-types/v10";
|
|
172
|
+
function intentsFor(events, extra = 0) {
|
|
173
|
+
let intents = I.Guilds | extra;
|
|
174
|
+
for (const name of events.keys())
|
|
175
|
+
intents |= EVENT_INTENTS[name] ?? 0;
|
|
176
|
+
return intents;
|
|
177
|
+
}
|
|
178
|
+
function intentNames(intents) {
|
|
179
|
+
return Object.entries(I).filter(([name, bit]) => typeof bit === "number" && (intents & bit) === bit && name !== "GuildBans" && name !== "GuildEmojisAndStickers").map(([name]) => name);
|
|
180
|
+
}
|
|
181
|
+
var EVENT_INTENTS, PRIVILEGED_INTENTS;
|
|
182
|
+
var init_intents = __esm(() => {
|
|
183
|
+
EVENT_INTENTS = {
|
|
184
|
+
guildMemberAdd: I.GuildMembers,
|
|
185
|
+
guildMemberUpdate: I.GuildMembers,
|
|
186
|
+
guildMemberRemove: I.GuildMembers,
|
|
187
|
+
guildBanAdd: I.GuildModeration,
|
|
188
|
+
guildBanRemove: I.GuildModeration,
|
|
189
|
+
guildAuditLogEntryCreate: I.GuildModeration,
|
|
190
|
+
guildEmojisUpdate: I.GuildExpressions,
|
|
191
|
+
guildStickersUpdate: I.GuildExpressions,
|
|
192
|
+
guildSoundboardSoundCreate: I.GuildExpressions,
|
|
193
|
+
guildSoundboardSoundUpdate: I.GuildExpressions,
|
|
194
|
+
guildSoundboardSoundDelete: I.GuildExpressions,
|
|
195
|
+
guildSoundboardSoundsUpdate: I.GuildExpressions,
|
|
196
|
+
guildIntegrationsUpdate: I.GuildIntegrations,
|
|
197
|
+
integrationCreate: I.GuildIntegrations,
|
|
198
|
+
integrationUpdate: I.GuildIntegrations,
|
|
199
|
+
integrationDelete: I.GuildIntegrations,
|
|
200
|
+
webhooksUpdate: I.GuildWebhooks,
|
|
201
|
+
inviteCreate: I.GuildInvites,
|
|
202
|
+
inviteDelete: I.GuildInvites,
|
|
203
|
+
voiceStateUpdate: I.GuildVoiceStates,
|
|
204
|
+
voiceChannelEffectSend: I.GuildVoiceStates,
|
|
205
|
+
presenceUpdate: I.GuildPresences,
|
|
206
|
+
messageCreate: I.GuildMessages | I.DirectMessages,
|
|
207
|
+
messageUpdate: I.GuildMessages | I.DirectMessages,
|
|
208
|
+
messageDelete: I.GuildMessages | I.DirectMessages,
|
|
209
|
+
messageDeleteBulk: I.GuildMessages,
|
|
210
|
+
messageReactionAdd: I.GuildMessageReactions | I.DirectMessageReactions,
|
|
211
|
+
messageReactionRemove: I.GuildMessageReactions | I.DirectMessageReactions,
|
|
212
|
+
messageReactionRemoveAll: I.GuildMessageReactions | I.DirectMessageReactions,
|
|
213
|
+
messageReactionRemoveEmoji: I.GuildMessageReactions | I.DirectMessageReactions,
|
|
214
|
+
typingStart: I.GuildMessageTyping | I.DirectMessageTyping,
|
|
215
|
+
guildScheduledEventCreate: I.GuildScheduledEvents,
|
|
216
|
+
guildScheduledEventUpdate: I.GuildScheduledEvents,
|
|
217
|
+
guildScheduledEventDelete: I.GuildScheduledEvents,
|
|
218
|
+
guildScheduledEventUserAdd: I.GuildScheduledEvents,
|
|
219
|
+
guildScheduledEventUserRemove: I.GuildScheduledEvents,
|
|
220
|
+
autoModerationRuleCreate: I.AutoModerationConfiguration,
|
|
221
|
+
autoModerationRuleUpdate: I.AutoModerationConfiguration,
|
|
222
|
+
autoModerationRuleDelete: I.AutoModerationConfiguration,
|
|
223
|
+
autoModerationActionExecution: I.AutoModerationExecution,
|
|
224
|
+
messagePollVoteAdd: I.GuildMessagePolls | I.DirectMessagePolls,
|
|
225
|
+
messagePollVoteRemove: I.GuildMessagePolls | I.DirectMessagePolls
|
|
226
|
+
};
|
|
227
|
+
PRIVILEGED_INTENTS = I.GuildMembers | I.GuildPresences | I.MessageContent;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// src/rest-transport.ts
|
|
231
|
+
import { NubisError as NubisError2 } from "@nubisjs/core";
|
|
232
|
+
import { Routes } from "discord-api-types/v10";
|
|
233
|
+
async function toRawFiles(files) {
|
|
234
|
+
if (!files?.length)
|
|
235
|
+
return;
|
|
236
|
+
return Promise.all(files.map(async (file, index) => ({
|
|
237
|
+
key: `files[${index}]`,
|
|
238
|
+
name: file.name,
|
|
239
|
+
contentType: file.contentType,
|
|
240
|
+
data: typeof file.data === "string" || file.data instanceof Uint8Array ? file.data : file.data instanceof ArrayBuffer ? new Uint8Array(file.data) : new Uint8Array(await file.data.arrayBuffer())
|
|
241
|
+
})));
|
|
242
|
+
}
|
|
243
|
+
function explain(error, what) {
|
|
244
|
+
const code = error.code;
|
|
245
|
+
if (typeof code === "number" && EXPIRED.has(code)) {
|
|
246
|
+
return new NubisError2(`${what} failed: the interaction token has expired. Discord allows follow-ups and edits for 15 minutes after an interaction.`);
|
|
247
|
+
}
|
|
248
|
+
return error;
|
|
249
|
+
}
|
|
250
|
+
function restTransport(rest, interaction) {
|
|
251
|
+
const { id, token, application_id: applicationId } = interaction;
|
|
252
|
+
const call = async (what, send) => {
|
|
253
|
+
try {
|
|
254
|
+
await send();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw explain(error, what);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
respond: async (response, files) => call("Responding", async () => rest.post(Routes.interactionCallback(id, token), {
|
|
261
|
+
auth: false,
|
|
262
|
+
body: response,
|
|
263
|
+
files: await toRawFiles(files)
|
|
264
|
+
})),
|
|
265
|
+
editOriginal: async (body, files) => call("Editing the reply", async () => rest.patch(Routes.webhookMessage(applicationId, token, "@original"), {
|
|
266
|
+
auth: false,
|
|
267
|
+
body,
|
|
268
|
+
files: await toRawFiles(files)
|
|
269
|
+
})),
|
|
270
|
+
deleteOriginal: () => call("Deleting the reply", () => rest.delete(Routes.webhookMessage(applicationId, token, "@original"), {
|
|
271
|
+
auth: false
|
|
272
|
+
})),
|
|
273
|
+
followUp: async (body, files) => call("Sending a follow-up", async () => rest.post(Routes.webhook(applicationId, token), {
|
|
274
|
+
auth: false,
|
|
275
|
+
body,
|
|
276
|
+
files: await toRawFiles(files)
|
|
277
|
+
}))
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
var EXPIRED;
|
|
281
|
+
var init_rest_transport = __esm(() => {
|
|
282
|
+
EXPIRED = new Set([10015, 10062, 50027]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// src/route.ts
|
|
286
|
+
import {
|
|
287
|
+
dispatch,
|
|
288
|
+
dispatchAutocomplete,
|
|
289
|
+
dispatchComponent,
|
|
290
|
+
dispatchMenu,
|
|
291
|
+
dispatchModal
|
|
292
|
+
} from "@nubisjs/core";
|
|
293
|
+
import {
|
|
294
|
+
ApplicationCommandType,
|
|
295
|
+
InteractionType
|
|
296
|
+
} from "discord-api-types/v10";
|
|
297
|
+
function routeInteraction(project, interaction, transport, options) {
|
|
298
|
+
switch (interaction.type) {
|
|
299
|
+
case InteractionType.ApplicationCommand:
|
|
300
|
+
return interaction.data.type === ApplicationCommandType.ChatInput ? dispatch(project.tree, interaction, transport, options) : dispatchMenu(project.menus, interaction, transport, options);
|
|
301
|
+
case InteractionType.MessageComponent:
|
|
302
|
+
return dispatchComponent(project.components, interaction, transport, options);
|
|
303
|
+
case InteractionType.ApplicationCommandAutocomplete:
|
|
304
|
+
return dispatchAutocomplete(project.tree, interaction, transport, options);
|
|
305
|
+
case InteractionType.ModalSubmit:
|
|
306
|
+
return dispatchModal(project.components, interaction, transport, options);
|
|
307
|
+
default:
|
|
308
|
+
return Promise.resolve({ status: "unsupported" });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var init_route = () => {};
|
|
312
|
+
|
|
313
|
+
// src/bot.ts
|
|
314
|
+
import { API } from "@discordjs/core/http-only";
|
|
315
|
+
import { dispatchEvent } from "@nubisjs/core";
|
|
316
|
+
import { REST } from "@discordjs/rest";
|
|
317
|
+
import { WebSocketManager, WebSocketShardEvents } from "@discordjs/ws";
|
|
318
|
+
import {
|
|
319
|
+
GatewayDispatchEvents
|
|
320
|
+
} from "discord-api-types/v10";
|
|
321
|
+
function createBot(options) {
|
|
322
|
+
const rest = options.rest ?? new REST({ version: "10" }).setToken(options.token);
|
|
323
|
+
const intents = intentsFor(options.project.events, options.intents);
|
|
324
|
+
const presence = options.presence ?? null;
|
|
325
|
+
const gateway = options.gateway ? options.gateway({ intents, rest, presence }) : new WebSocketManager({
|
|
326
|
+
token: options.token,
|
|
327
|
+
intents,
|
|
328
|
+
rest,
|
|
329
|
+
initialPresence: presence
|
|
330
|
+
});
|
|
331
|
+
const api = new API(rest);
|
|
332
|
+
const routeOptions = {
|
|
333
|
+
...options.dispatch,
|
|
334
|
+
api,
|
|
335
|
+
onError: options.onError ?? options.dispatch?.onError
|
|
336
|
+
};
|
|
337
|
+
gateway.on(WebSocketShardEvents.Dispatch, (payload, shardId) => {
|
|
338
|
+
if (payload.t === GatewayDispatchEvents.InteractionCreate) {
|
|
339
|
+
const interaction = payload.d;
|
|
340
|
+
routeInteraction(options.project, interaction, restTransport(rest, interaction), routeOptions);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
dispatchEvent(options.project.events, payload.t, payload.d, {
|
|
344
|
+
shardId,
|
|
345
|
+
onError: routeOptions.onError,
|
|
346
|
+
api
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
rest,
|
|
351
|
+
api,
|
|
352
|
+
intents,
|
|
353
|
+
start: () => gateway.connect(),
|
|
354
|
+
stop: async () => {
|
|
355
|
+
await gateway.destroy();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
var init_bot = __esm(() => {
|
|
360
|
+
init_intents();
|
|
361
|
+
init_rest_transport();
|
|
362
|
+
init_route();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// src/config.ts
|
|
366
|
+
import {
|
|
367
|
+
ActivityType,
|
|
368
|
+
GatewayIntentBits,
|
|
369
|
+
PresenceUpdateStatus
|
|
370
|
+
} from "discord-api-types/v10";
|
|
371
|
+
function defineConfig(config) {
|
|
372
|
+
return config;
|
|
373
|
+
}
|
|
374
|
+
function validateConfig(config, file) {
|
|
375
|
+
const where = (detail) => new Error(`${file}: ${detail}`);
|
|
376
|
+
if (typeof config !== "object" || config === null) {
|
|
377
|
+
throw where("export the config as default: export default defineConfig({ ... }).");
|
|
378
|
+
}
|
|
379
|
+
const value = config;
|
|
380
|
+
for (const key of Object.keys(value)) {
|
|
381
|
+
if (!KEYS.has(key)) {
|
|
382
|
+
throw where(`"${key}" isn't a config setting. The settings are: ${[...KEYS].join(", ")}.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (const name of value.intents ?? []) {
|
|
386
|
+
if (typeof name !== "string" || !(name in GatewayIntentBits) || !Number.isNaN(Number(name))) {
|
|
387
|
+
throw where(`"${String(name)}" isn't an intent. Use names like "MessageContent" or "GuildMembers".`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const presence = value.presence;
|
|
391
|
+
if (presence?.status && !(presence.status in STATUSES)) {
|
|
392
|
+
throw where(`presence.status must be online, idle, dnd or invisible (got "${presence.status}").`);
|
|
393
|
+
}
|
|
394
|
+
if (presence?.activity && !(presence.activity.type in ACTIVITY_TYPES)) {
|
|
395
|
+
throw where(`presence.activity.type must be playing, listening, watching, competing or custom (got "${presence.activity.type}").`);
|
|
396
|
+
}
|
|
397
|
+
if (value.transport !== undefined && value.transport !== "gateway" && value.transport !== "http") {
|
|
398
|
+
throw where(`transport must be "gateway" or "http" (got "${String(value.transport)}").`);
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
function intentBits(names = []) {
|
|
403
|
+
return names.reduce((bits, name) => bits | GatewayIntentBits[name], 0);
|
|
404
|
+
}
|
|
405
|
+
function presenceData(presence) {
|
|
406
|
+
if (!presence)
|
|
407
|
+
return;
|
|
408
|
+
const activity = presence.activity;
|
|
409
|
+
return {
|
|
410
|
+
since: null,
|
|
411
|
+
afk: false,
|
|
412
|
+
status: STATUSES[presence.status ?? "online"] ?? PresenceUpdateStatus.Online,
|
|
413
|
+
activities: activity ? [
|
|
414
|
+
activity.type === "custom" ? {
|
|
415
|
+
type: ActivityType.Custom,
|
|
416
|
+
name: "Custom Status",
|
|
417
|
+
state: activity.name
|
|
418
|
+
} : { type: ACTIVITY_TYPES[activity.type], name: activity.name }
|
|
419
|
+
] : []
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
var KEYS, ACTIVITY_TYPES, STATUSES;
|
|
423
|
+
var init_config = __esm(() => {
|
|
424
|
+
KEYS = new Set([
|
|
425
|
+
"root",
|
|
426
|
+
"intents",
|
|
427
|
+
"presence",
|
|
428
|
+
"guards",
|
|
429
|
+
"middleware",
|
|
430
|
+
"onError",
|
|
431
|
+
"autoDeferMs",
|
|
432
|
+
"transport",
|
|
433
|
+
"http",
|
|
434
|
+
"devGuildId"
|
|
435
|
+
]);
|
|
436
|
+
ACTIVITY_TYPES = {
|
|
437
|
+
playing: ActivityType.Playing,
|
|
438
|
+
listening: ActivityType.Listening,
|
|
439
|
+
watching: ActivityType.Watching,
|
|
440
|
+
competing: ActivityType.Competing,
|
|
441
|
+
custom: ActivityType.Custom
|
|
442
|
+
};
|
|
443
|
+
STATUSES = {
|
|
444
|
+
online: PresenceUpdateStatus.Online,
|
|
445
|
+
idle: PresenceUpdateStatus.Idle,
|
|
446
|
+
dnd: PresenceUpdateStatus.DoNotDisturb,
|
|
447
|
+
invisible: PresenceUpdateStatus.Invisible
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// src/config-loader.ts
|
|
452
|
+
import { access } from "node:fs/promises";
|
|
453
|
+
import { join as join2 } from "node:path";
|
|
454
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
455
|
+
async function loadConfig(cwd) {
|
|
456
|
+
for (const name of NAMES) {
|
|
457
|
+
const file = join2(cwd, name);
|
|
458
|
+
const exists = await access(file).then(() => true, () => false);
|
|
459
|
+
if (!exists)
|
|
460
|
+
continue;
|
|
461
|
+
const url = pathToFileURL2(file);
|
|
462
|
+
url.searchParams.set("v", String(Date.now()));
|
|
463
|
+
const module = await import(url.href);
|
|
464
|
+
const config = validateConfig(module.default, name);
|
|
465
|
+
return { config, file, root: join2(cwd, config.root ?? ".") };
|
|
466
|
+
}
|
|
467
|
+
return { config: {}, file: undefined, root: cwd };
|
|
468
|
+
}
|
|
469
|
+
var NAMES;
|
|
470
|
+
var init_config_loader = __esm(() => {
|
|
471
|
+
init_config();
|
|
472
|
+
NAMES = [
|
|
473
|
+
"nubis.config.ts",
|
|
474
|
+
"nubis.config.mts",
|
|
475
|
+
"nubis.config.js",
|
|
476
|
+
"nubis.config.mjs"
|
|
477
|
+
];
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// src/env.ts
|
|
481
|
+
import { readFile } from "node:fs/promises";
|
|
482
|
+
import { dirname, join as join3 } from "node:path";
|
|
483
|
+
function parseEnv(text) {
|
|
484
|
+
const values = {};
|
|
485
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
486
|
+
const line = raw.trim();
|
|
487
|
+
if (!line || line.startsWith("#"))
|
|
488
|
+
continue;
|
|
489
|
+
const at = line.indexOf("=");
|
|
490
|
+
if (at === -1)
|
|
491
|
+
continue;
|
|
492
|
+
const key = line.slice(0, at).replace(/^export\s+/, "").trim();
|
|
493
|
+
let value = line.slice(at + 1).trim();
|
|
494
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
495
|
+
value = value.slice(1, -1);
|
|
496
|
+
}
|
|
497
|
+
values[key] = value;
|
|
498
|
+
}
|
|
499
|
+
return values;
|
|
500
|
+
}
|
|
501
|
+
async function loadEnv(start, target = process.env) {
|
|
502
|
+
let dir = start;
|
|
503
|
+
for (;; ) {
|
|
504
|
+
const file = join3(dir, ".env");
|
|
505
|
+
const text = await readFile(file, "utf8").catch(() => {
|
|
506
|
+
return;
|
|
507
|
+
});
|
|
508
|
+
if (text !== undefined) {
|
|
509
|
+
for (const [key, value] of Object.entries(parseEnv(text))) {
|
|
510
|
+
target[key] ??= value;
|
|
511
|
+
}
|
|
512
|
+
return file;
|
|
513
|
+
}
|
|
514
|
+
const parent = dirname(dir);
|
|
515
|
+
if (parent === dir)
|
|
516
|
+
return;
|
|
517
|
+
dir = parent;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function discordEnv(env = process.env) {
|
|
521
|
+
const missing = ["DISCORD_TOKEN", "DISCORD_APPLICATION_ID"].filter((key) => !env[key]);
|
|
522
|
+
if (missing.length > 0) {
|
|
523
|
+
throw new Error(`Missing ${missing.join(" and ")}. Put them in a .env file next to your bot (see .env.example).`);
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
token: env.DISCORD_TOKEN,
|
|
527
|
+
applicationId: env.DISCORD_APPLICATION_ID,
|
|
528
|
+
publicKey: env.DISCORD_PUBLIC_KEY || undefined,
|
|
529
|
+
devGuildId: env.DISCORD_DEV_GUILD_ID || undefined
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
var init_env = () => {};
|
|
533
|
+
|
|
534
|
+
// src/sync.ts
|
|
535
|
+
import {
|
|
536
|
+
planSync,
|
|
537
|
+
toCommandPayloads,
|
|
538
|
+
toMenuPayloads
|
|
539
|
+
} from "@nubisjs/core";
|
|
540
|
+
import { Routes as Routes2 } from "discord-api-types/v10";
|
|
541
|
+
async function syncCommands(options) {
|
|
542
|
+
const route = options.guildId ? Routes2.applicationGuildCommands(options.applicationId, options.guildId) : Routes2.applicationCommands(options.applicationId);
|
|
543
|
+
const local = [
|
|
544
|
+
...toCommandPayloads(options.project.tree),
|
|
545
|
+
...toMenuPayloads(options.project.menus)
|
|
546
|
+
];
|
|
547
|
+
const remote = await options.rest.get(route);
|
|
548
|
+
const plan = planSync(local, remote);
|
|
549
|
+
const scope = options.guildId ? "guild" : "global";
|
|
550
|
+
if (options.dryRun || !hasChanges(plan))
|
|
551
|
+
return { plan, applied: false, scope };
|
|
552
|
+
await options.rest.put(route, { body: local });
|
|
553
|
+
return { plan, applied: true, scope };
|
|
554
|
+
}
|
|
555
|
+
function describePlan(plan) {
|
|
556
|
+
return [
|
|
557
|
+
...plan.create.map((command) => `+ ${command.name}`),
|
|
558
|
+
...plan.update.map(({ payload }) => `~ ${payload.name}`),
|
|
559
|
+
...plan.remove.map((command) => `- ${command.name}`)
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
var hasChanges = (plan) => plan.create.length + plan.update.length + plan.remove.length > 0;
|
|
563
|
+
var init_sync = () => {};
|
|
564
|
+
|
|
565
|
+
// src/node.ts
|
|
566
|
+
init_loader();
|
|
567
|
+
init_bot();
|
|
568
|
+
init_config();
|
|
569
|
+
init_config_loader();
|
|
570
|
+
init_env();
|
|
571
|
+
init_intents();
|
|
572
|
+
init_rest_transport();
|
|
573
|
+
init_sync();
|
|
574
|
+
export {
|
|
575
|
+
PRIVILEGED_INTENTS,
|
|
576
|
+
createBot,
|
|
577
|
+
describePlan,
|
|
578
|
+
discordEnv,
|
|
579
|
+
hasChanges,
|
|
580
|
+
intentBits,
|
|
581
|
+
intentNames,
|
|
582
|
+
intentsFor,
|
|
583
|
+
loadConfig,
|
|
584
|
+
loadEnv,
|
|
585
|
+
loadProject,
|
|
586
|
+
parseEnv,
|
|
587
|
+
presenceData,
|
|
588
|
+
restTransport,
|
|
589
|
+
syncCommands,
|
|
590
|
+
toNodeListener,
|
|
591
|
+
validateConfig
|
|
592
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Transport } from "@nubisjs/core";
|
|
2
|
+
import { type APIInteraction } from "discord-api-types/v10";
|
|
3
|
+
/** The slice of @discordjs/rest's REST client Nubis uses. */
|
|
4
|
+
export interface RestLike {
|
|
5
|
+
get(route: `/${string}`, options?: object): Promise<unknown>;
|
|
6
|
+
post(route: `/${string}`, options?: object): Promise<unknown>;
|
|
7
|
+
put(route: `/${string}`, options?: object): Promise<unknown>;
|
|
8
|
+
patch(route: `/${string}`, options?: object): Promise<unknown>;
|
|
9
|
+
delete(route: `/${string}`, options?: object): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Answers an interaction received over the gateway, through Discord's REST
|
|
13
|
+
* API. Interaction callbacks and webhooks use the interaction's own token,
|
|
14
|
+
* so these calls don't send the bot token.
|
|
15
|
+
*/
|
|
16
|
+
export declare function restTransport(rest: RestLike, interaction: APIInteraction): Transport;
|
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CommandTree, type ComponentDispatchOptions, type ComponentRegistry, type DispatchOptions, type MenuDefinition, type Transport } from "@nubisjs/core";
|
|
2
|
+
import { type APIInteraction } from "discord-api-types/v10";
|
|
3
|
+
/** The parts of a project that answer interactions. A loaded Project fits. */
|
|
4
|
+
export interface InteractionProject {
|
|
5
|
+
readonly tree: CommandTree;
|
|
6
|
+
readonly components: ComponentRegistry;
|
|
7
|
+
readonly menus: readonly MenuDefinition[];
|
|
8
|
+
}
|
|
9
|
+
export type RouteOptions = DispatchOptions & ComponentDispatchOptions;
|
|
10
|
+
/**
|
|
11
|
+
* Sends an interaction to the dispatcher for its type. Shared by the HTTP
|
|
12
|
+
* endpoint and the gateway bot, which differ only in their transport.
|
|
13
|
+
*/
|
|
14
|
+
export declare function routeInteraction(project: InteractionProject, interaction: APIInteraction, transport: Transport, options: RouteOptions): Promise<{
|
|
15
|
+
readonly status: string;
|
|
16
|
+
readonly error?: unknown;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CliResult = {
|
|
2
|
+
readonly code: number;
|
|
3
|
+
readonly output: string;
|
|
4
|
+
}
|
|
5
|
+
/** A command that talks to Discord; cli.ts runs it. */
|
|
6
|
+
| {
|
|
7
|
+
readonly run: "dev" | "start" | "sync";
|
|
8
|
+
readonly args: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function runCli(args: readonly string[]): CliResult;
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type SyncPlan } from "@nubisjs/core";
|
|
2
|
+
import type { Project } from "./loader";
|
|
3
|
+
import type { RestLike } from "./rest-transport";
|
|
4
|
+
export interface SyncOptions {
|
|
5
|
+
readonly rest: RestLike;
|
|
6
|
+
readonly applicationId: string;
|
|
7
|
+
readonly project: Pick<Project, "tree" | "menus">;
|
|
8
|
+
/** Register to one server (instant; for development) instead of globally. */
|
|
9
|
+
readonly guildId?: string;
|
|
10
|
+
/** Work out the plan without changing anything. */
|
|
11
|
+
readonly dryRun?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SyncResult {
|
|
14
|
+
readonly plan: SyncPlan;
|
|
15
|
+
/** Whether Discord was updated: false for a dry run or when nothing changed. */
|
|
16
|
+
readonly applied: boolean;
|
|
17
|
+
readonly scope: "guild" | "global";
|
|
18
|
+
}
|
|
19
|
+
export declare const hasChanges: (plan: SyncPlan) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Makes Discord's registered commands match the project's. Compares first,
|
|
22
|
+
* so an unchanged bot makes no write; changes go in one bulk overwrite.
|
|
23
|
+
*/
|
|
24
|
+
export declare function syncCommands(options: SyncOptions): Promise<SyncResult>;
|
|
25
|
+
/** One line per change, for the CLI. */
|
|
26
|
+
export declare function describePlan(plan: SyncPlan): string[];
|
package/dist/testing.js
ADDED