@helyx/bot 0.4.37 → 0.4.39
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/CHANGELOG.md +20 -0
- package/README.md +22 -2
- package/dist/cloud-management-module.js +275 -206
- package/dist/command-registration-policy.d.ts +8 -0
- package/dist/command-registration-policy.js +39 -0
- package/dist/command-sync.d.ts +5 -1
- package/dist/command-sync.js +67 -3
- package/dist/control-server.d.ts +15 -3
- package/dist/control-server.js +34 -48
- package/dist/dashboard-commands.js +5 -1
- package/dist/developer-operations-module.d.ts +2 -0
- package/dist/developer-operations-module.js +65 -4
- package/dist/discord-rest.d.ts +5 -0
- package/dist/discord-rest.js +9 -0
- package/dist/events.js +4 -2
- package/dist/first-time-command-setup.d.ts +25 -0
- package/dist/first-time-command-setup.js +22 -0
- package/dist/guild-command-control-routes.d.ts +5 -0
- package/dist/guild-command-control-routes.js +191 -0
- package/dist/guild-command-discord-client.d.ts +21 -0
- package/dist/guild-command-discord-client.js +50 -0
- package/dist/guild-command-projection.d.ts +46 -0
- package/dist/guild-command-projection.js +254 -0
- package/dist/guild-command-read-service.d.ts +201 -0
- package/dist/guild-command-read-service.js +272 -0
- package/dist/guild-command-release.d.ts +19 -0
- package/dist/guild-command-release.js +47 -0
- package/dist/guild-command-worker.d.ts +49 -0
- package/dist/guild-command-worker.js +443 -0
- package/dist/guilds.d.ts +1 -1
- package/dist/guilds.js +41 -26
- package/dist/index.js +75 -4
- package/dist/interactions.d.ts +15 -4
- package/dist/interactions.js +285 -70
- package/dist/management-module.js +117 -41
- package/dist/module-logging.d.ts +1 -0
- package/dist/module-logging.js +5 -5
- package/dist/module-status-control.d.ts +19 -0
- package/dist/module-status-control.js +69 -0
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +21 -2
- package/dist/nexus-commands.d.ts +1 -12
- package/dist/nexus-commands.js +1 -99
- package/dist/oauth-module.js +134 -88
- package/dist/permissions-module.js +112 -39
- package/dist/privacy-module.d.ts +2 -0
- package/dist/privacy-module.js +60 -0
- package/dist/scheduled-task-runner.js +36 -1
- package/dist/shard-contracts.d.ts +24 -0
- package/dist/shard-contracts.js +20 -0
- package/dist/shard-router.d.ts +1 -0
- package/dist/shard-router.js +7 -0
- package/dist/shard-supervisor.d.ts +2 -1
- package/dist/shard-supervisor.js +94 -16
- package/dist/shard-worker.d.ts +5 -0
- package/dist/shard-worker.js +9 -1
- package/dist/shared-commands.d.ts +13 -0
- package/dist/shared-commands.js +163 -0
- package/dist/support-module.js +51 -51
- package/dist/trivia-data-provider.d.ts +26 -0
- package/dist/trivia-data-provider.js +51 -0
- package/documentation/README.md +32 -0
- package/documentation/examples/.env.example +117 -0
- package/documentation/guides/getting-started.md +51 -0
- package/documentation/guides/self-hosting.md +40 -0
- package/documentation/modules/building-a-module.md +37 -0
- package/documentation/modules/publishing-a-module.md +27 -0
- package/documentation/modules/testing-a-module.md +46 -0
- package/documentation/reference/environment.md +42 -0
- package/documentation/runbooks/backup-and-recovery.md +41 -0
- package/documentation/runbooks/installation-and-upgrades.md +48 -0
- package/package.json +7 -4
- package/dist/oauth-module.test.d.ts +0 -2
- package/dist/oauth-module.test.js +0 -172
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
2
|
import { StatusHeartbeatReporter } from "@helyx/status-client";
|
|
3
3
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
4
|
-
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
4
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
5
5
|
import { createRuntime } from "@helyx/framework";
|
|
6
6
|
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
7
7
|
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
@@ -29,12 +29,19 @@ import { createDeveloperOperationsModule } from "./developer-operations-module.j
|
|
|
29
29
|
import { shardHealthSnapshotSchema } from "./shard-contracts.js";
|
|
30
30
|
import { createSupportModule } from "./support-module.js";
|
|
31
31
|
import { createOAuthModule } from "./oauth-module.js";
|
|
32
|
+
import { createPrivacyModule } from "./privacy-module.js";
|
|
32
33
|
import { HttpOAuthService } from "./oauth-service.js";
|
|
33
34
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
34
35
|
import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
|
|
36
|
+
import { GuildCommandWorker } from "./guild-command-worker.js";
|
|
37
|
+
import { GuildCommandDiscordClient } from "./guild-command-discord-client.js";
|
|
38
|
+
import { GuildCommandReadService } from "./guild-command-read-service.js";
|
|
39
|
+
import { scheduleGuildCommandRelease } from "./guild-command-release.js";
|
|
35
40
|
import { CoreResourceAuthorizationService } from "./resource-authorization.js";
|
|
41
|
+
import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
|
|
36
42
|
import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
37
43
|
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
44
|
+
import { discordRestOptions } from "./discord-rest.js";
|
|
38
45
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
39
46
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
40
47
|
export * from "./control-server.js";
|
|
@@ -128,17 +135,26 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
128
135
|
verifiedMigrationCount: migrationResult.verified.length,
|
|
129
136
|
}, "Core database ready");
|
|
130
137
|
const cleanupExpiredCoreRecords = async () => {
|
|
131
|
-
const [sessions, rateLimits, verificationAudit] = await Promise.all([
|
|
138
|
+
const [sessions, rateLimits, verificationAudit, commandRecords] = await Promise.all([
|
|
132
139
|
new InteractionSessionRepository(database).deleteExpired(),
|
|
133
140
|
new RateLimitRepository(database).deleteExpired(),
|
|
134
141
|
new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
|
|
142
|
+
new GuildCommandRepository(database).deleteExpired(12),
|
|
135
143
|
]);
|
|
136
|
-
|
|
144
|
+
const commandRecordCount = commandRecords.history +
|
|
145
|
+
commandRecords.idempotencyKeys +
|
|
146
|
+
commandRecords.creationLedger +
|
|
147
|
+
commandRecords.requests;
|
|
148
|
+
if (sessions > 0 ||
|
|
149
|
+
rateLimits > 0 ||
|
|
150
|
+
verificationAudit > 0 ||
|
|
151
|
+
commandRecordCount > 0) {
|
|
137
152
|
bootstrapLogger.info({
|
|
138
153
|
event: "database.retention_cleaned",
|
|
139
154
|
interactionSessions: sessions,
|
|
140
155
|
rateLimitWindows: rateLimits,
|
|
141
156
|
verificationAuditEvents: verificationAudit,
|
|
157
|
+
commandRecords: commandRecordCount,
|
|
142
158
|
}, "Expired core records removed");
|
|
143
159
|
}
|
|
144
160
|
};
|
|
@@ -169,6 +185,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
169
185
|
}, "Module databases ready");
|
|
170
186
|
const featureModules = modulePackages.map(({ definition }) => definition);
|
|
171
187
|
const client = new Client({
|
|
188
|
+
rest: discordRestOptions,
|
|
172
189
|
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
|
|
173
190
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
174
191
|
allowedMentions: { parse: [] },
|
|
@@ -192,6 +209,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
192
209
|
})
|
|
193
210
|
: undefined;
|
|
194
211
|
const supportModule = createSupportModule(cloudConnector);
|
|
212
|
+
const privacyModule = createPrivacyModule();
|
|
195
213
|
const oauthModule = environment.HELYX_OAUTH_ENABLED
|
|
196
214
|
? createOAuthModule()
|
|
197
215
|
: undefined;
|
|
@@ -199,11 +217,13 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
199
217
|
managementModule,
|
|
200
218
|
permissionsModule,
|
|
201
219
|
supportModule,
|
|
220
|
+
privacyModule,
|
|
202
221
|
...(oauthModule ? [oauthModule] : []),
|
|
203
222
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
204
223
|
...featureModules,
|
|
205
224
|
];
|
|
206
225
|
let scheduledTasks;
|
|
226
|
+
let guildCommandWorker;
|
|
207
227
|
const unshardedSupervisorId = randomUUID();
|
|
208
228
|
const deployedCommit = deploymentReference(processEnvironment);
|
|
209
229
|
const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
|
|
@@ -216,11 +236,13 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
216
236
|
managementModule,
|
|
217
237
|
permissionsModule,
|
|
218
238
|
supportModule,
|
|
239
|
+
privacyModule,
|
|
219
240
|
...(oauthModule ? [oauthModule] : []),
|
|
220
241
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
221
242
|
].map(({ manifest }) => manifest),
|
|
222
243
|
modules: featureModules.map(({ manifest }) => manifest),
|
|
223
244
|
processLabel: "unsharded bot process",
|
|
245
|
+
triviaData: new PostgresTriviaDataProvider(database),
|
|
224
246
|
health: {
|
|
225
247
|
getHealth: async () => {
|
|
226
248
|
let databaseHealthy = true;
|
|
@@ -333,11 +355,13 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
333
355
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
334
356
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
335
357
|
]);
|
|
336
|
-
const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000);
|
|
358
|
+
const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, undefined, environment.DISCORD_CLIENT_ID);
|
|
337
359
|
const commandsReady = installInteractions(client, modules, {
|
|
360
|
+
globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
|
|
338
361
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
339
362
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
340
363
|
: {}),
|
|
364
|
+
synchroniseInternalGuild: !environment.HELYX_GUILD_COMMAND_WRITES_ENABLED,
|
|
341
365
|
prepare: prepareGuilds,
|
|
342
366
|
selfAuthorizingGuildModuleIds: new Set([
|
|
343
367
|
supportModule.manifest.id,
|
|
@@ -383,6 +407,13 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
383
407
|
process.exitCode = 1;
|
|
384
408
|
bootstrapLogger.error({ event: "scheduler.stop_failed", error: safeError(error) }, "Scheduled task runner shutdown failed");
|
|
385
409
|
});
|
|
410
|
+
await guildCommandWorker?.stop().catch((error) => {
|
|
411
|
+
process.exitCode = 1;
|
|
412
|
+
bootstrapLogger.error({
|
|
413
|
+
event: "guild_commands.worker_stop_failed",
|
|
414
|
+
error: safeError(error),
|
|
415
|
+
}, "Guild command worker shutdown failed");
|
|
416
|
+
});
|
|
386
417
|
bootstrapLogger.info({ event: "bot.shutdown", reason }, "Bot shutdown requested");
|
|
387
418
|
await cloudConnector.stop().catch((error) => {
|
|
388
419
|
process.exitCode = 1;
|
|
@@ -442,12 +473,51 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
442
473
|
workerId: `unsharded:${unshardedSupervisorId}`,
|
|
443
474
|
});
|
|
444
475
|
scheduledTasks.start();
|
|
476
|
+
guildCommandWorker = new GuildCommandWorker({
|
|
477
|
+
database,
|
|
478
|
+
modules,
|
|
479
|
+
logger: bootstrapLogger,
|
|
480
|
+
workerId: `unsharded-command:${unshardedSupervisorId}`,
|
|
481
|
+
fencingToken: 1n,
|
|
482
|
+
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
483
|
+
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
484
|
+
: {}),
|
|
485
|
+
...(environment.HELYX_GUILD_COMMAND_WRITES_ENABLED
|
|
486
|
+
? {
|
|
487
|
+
discord: new GuildCommandDiscordClient({
|
|
488
|
+
applicationId: environment.DISCORD_CLIENT_ID,
|
|
489
|
+
token: environment.DISCORD_TOKEN,
|
|
490
|
+
writeGuildIds: environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS,
|
|
491
|
+
writeAllGuilds: environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all",
|
|
492
|
+
}),
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
});
|
|
496
|
+
guildCommandWorker.start();
|
|
497
|
+
if (environment.HELYX_GUILD_COMMAND_WRITES_ENABLED &&
|
|
498
|
+
environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all") {
|
|
499
|
+
void scheduleGuildCommandRelease({
|
|
500
|
+
database,
|
|
501
|
+
applicationId: environment.DISCORD_CLIENT_ID,
|
|
502
|
+
releaseId: environment.HELYX_GUILD_COMMAND_RELEASE_ID,
|
|
503
|
+
})
|
|
504
|
+
.then((release) => {
|
|
505
|
+
bootstrapLogger.info({ event: "guild_commands.release_scheduled", ...release }, "Guild command release rollout prepared");
|
|
506
|
+
})
|
|
507
|
+
.catch((error) => {
|
|
508
|
+
bootstrapLogger.error({
|
|
509
|
+
event: "guild_commands.release_schedule_failed",
|
|
510
|
+
error: safeError(error),
|
|
511
|
+
}, "Guild command release rollout could not be prepared");
|
|
512
|
+
});
|
|
513
|
+
}
|
|
445
514
|
await cloudConnector.start();
|
|
446
515
|
if (environment.HELYX_BOT_CONTROL_SECRET) {
|
|
447
516
|
controlServer = await createBotControlServer({
|
|
448
517
|
service: controlService,
|
|
449
518
|
secret: environment.HELYX_BOT_CONTROL_SECRET,
|
|
450
519
|
logger: { level: environment.LOG_LEVEL },
|
|
520
|
+
guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", modules),
|
|
451
521
|
});
|
|
452
522
|
await controlServer.listen({
|
|
453
523
|
host: "::",
|
|
@@ -464,6 +534,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
464
534
|
}
|
|
465
535
|
catch (error) {
|
|
466
536
|
await scheduledTasks?.stop().catch(() => undefined);
|
|
537
|
+
await guildCommandWorker?.stop().catch(() => undefined);
|
|
467
538
|
await cloudConnector.stop().catch(() => undefined);
|
|
468
539
|
if (controlServer)
|
|
469
540
|
await controlServer.close().catch(() => undefined);
|
package/dist/interactions.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { CommandContribution, MessageResponse, ModalDefinition, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
|
|
2
|
-
import {
|
|
1
|
+
import type { CommandAutocompleteContext, CommandAuthorization, CommandContribution, MessageResponse, ModalDefinition, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import { type GlobalCommandRegistrationMode } from "./command-registration-policy.js";
|
|
3
|
+
import { ModalBuilder, type AutocompleteInteraction, type ChatInputCommandInteraction, type Client, type InteractionReplyOptions, type InteractionUpdateOptions } from "discord.js";
|
|
3
4
|
export declare const GLOBAL_MODULE_UNAVAILABLE_MESSAGE = "This module is temporarily unavailable for all hosted servers.";
|
|
5
|
+
export declare const AUTOCOMPLETE_RESPONSE_DEADLINE_MS = 2000;
|
|
6
|
+
export declare const DISCORD_DNS_REFRESH_INTERVAL_MS = 30000;
|
|
4
7
|
export interface InteractionLogger {
|
|
5
8
|
info(fields: Record<string, unknown>, message: string): void;
|
|
6
9
|
warn(fields: Record<string, unknown>, message: string): void;
|
|
@@ -10,10 +13,14 @@ export interface CommandRegistration {
|
|
|
10
13
|
internalGuildId?: string;
|
|
11
14
|
prepare?(client: Client<true>): Promise<void>;
|
|
12
15
|
synchronise?: boolean;
|
|
16
|
+
synchroniseInternalGuild?: boolean;
|
|
17
|
+
globalCommandMode?: GlobalCommandRegistrationMode;
|
|
13
18
|
selfAuthorizingGuildModuleIds?: ReadonlySet<string>;
|
|
14
19
|
}
|
|
15
20
|
export declare function installInteractions(client: Client, modules: readonly ModuleDefinition[], registration: CommandRegistration, logger: InteractionLogger, services: ServiceAccess): Promise<void>;
|
|
16
|
-
export declare function
|
|
21
|
+
export declare function warmDiscordRestConnection(client: Client<true>, logger: InteractionLogger): Promise<void>;
|
|
22
|
+
export declare function startDiscordDnsRefresh(logger: InteractionLogger, lookup?: (hostname: string) => Promise<unknown>, intervalMilliseconds?: number): ReturnType<typeof setInterval>;
|
|
23
|
+
export declare function buildApplicationCommandDefinitions(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: GlobalCommandRegistrationMode): {
|
|
17
24
|
global: ReturnType<typeof toCommandDefinition>[];
|
|
18
25
|
guild: ReturnType<typeof toCommandDefinition>[];
|
|
19
26
|
};
|
|
@@ -22,13 +29,17 @@ export declare function toCommandDefinition(registered: {
|
|
|
22
29
|
contribution: CommandContribution;
|
|
23
30
|
}): import("discord.js").RESTPostAPIChatInputApplicationCommandsJSONBody;
|
|
24
31
|
export declare function uniqueMap<T>(items: readonly T[], keyOf: (item: T) => string, label: string): Map<string, T>;
|
|
32
|
+
export declare function createAutocompleteResponseController(interaction: AutocompleteInteraction, logger: InteractionLogger): {
|
|
33
|
+
respond: CommandAutocompleteContext["respond"];
|
|
34
|
+
ensureResponded(): Promise<void>;
|
|
35
|
+
};
|
|
25
36
|
export declare function sendToChannel(client: Client, channelId: string, response: MessageResponse, deliveryId?: string): Promise<{
|
|
26
37
|
messageId: string;
|
|
27
38
|
}>;
|
|
28
39
|
export declare function deleteChannelMessage(client: Client, channelId: string, messageId: string): Promise<void>;
|
|
29
40
|
export declare function editChannelMessage(client: Client, channelId: string, messageId: string, response: MessageResponse): Promise<void>;
|
|
30
41
|
export declare function addChannelMessageReaction(client: Client, channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
|
|
31
|
-
export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean, requiredModuleName?: string): Promise<boolean>;
|
|
42
|
+
export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean, requiredModuleName?: string, installationIndependentCommand?: boolean, authorization?: CommandAuthorization): Promise<boolean>;
|
|
32
43
|
export declare function normalizeDiscordPermission(permission: string): string;
|
|
33
44
|
export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
|
|
34
45
|
export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;
|