@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/command-sync.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModuleDefinition } from "@helyx/sdk";
|
|
2
|
+
import type { GlobalCommandRegistrationMode } from "./command-registration-policy.js";
|
|
2
3
|
export interface CommandHashState {
|
|
3
4
|
value: unknown;
|
|
4
5
|
version: bigint;
|
|
@@ -8,13 +9,14 @@ export interface CommandHashStore {
|
|
|
8
9
|
set(key: string, value: unknown, expectedVersion?: bigint | null): Promise<CommandHashState>;
|
|
9
10
|
}
|
|
10
11
|
export interface DiscordCommandRest {
|
|
12
|
+
get?(route: `/${string}`): Promise<unknown>;
|
|
11
13
|
put(route: `/${string}`, options: {
|
|
12
14
|
body: unknown;
|
|
13
15
|
}): Promise<unknown>;
|
|
14
16
|
}
|
|
15
17
|
export interface CommandSyncResult {
|
|
16
18
|
global: "updated" | "unchanged";
|
|
17
|
-
guild: "updated" | "unchanged" | "not_configured";
|
|
19
|
+
guild: "updated" | "unchanged" | "not_configured" | "delegated";
|
|
18
20
|
globalCount: number;
|
|
19
21
|
guildCount: number;
|
|
20
22
|
globalHash: string;
|
|
@@ -24,7 +26,9 @@ export declare function synchroniseApplicationCommands(input: {
|
|
|
24
26
|
applicationId: string;
|
|
25
27
|
token: string;
|
|
26
28
|
modules: readonly ModuleDefinition[];
|
|
29
|
+
globalCommandMode?: GlobalCommandRegistrationMode;
|
|
27
30
|
internalGuildId?: string;
|
|
31
|
+
synchroniseInternalGuild?: boolean;
|
|
28
32
|
state: CommandHashStore;
|
|
29
33
|
rest?: DiscordCommandRest;
|
|
30
34
|
}): Promise<CommandSyncResult>;
|
package/dist/command-sync.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { REST, Routes } from "discord.js";
|
|
3
3
|
import { buildApplicationCommandDefinitions } from "./interactions.js";
|
|
4
4
|
export async function synchroniseApplicationCommands(input) {
|
|
5
|
-
const definitions = buildApplicationCommandDefinitions(input.modules, input.internalGuildId);
|
|
5
|
+
const definitions = buildApplicationCommandDefinitions(input.modules, input.internalGuildId, input.globalCommandMode);
|
|
6
6
|
definitions.global.sort((left, right) => left.name.localeCompare(right.name));
|
|
7
7
|
definitions.guild.sort((left, right) => left.name.localeCompare(right.name));
|
|
8
8
|
const rest = input.rest ?? new REST({ version: "10" }).setToken(input.token);
|
|
@@ -13,6 +13,7 @@ export async function synchroniseApplicationCommands(input) {
|
|
|
13
13
|
hash: globalHash,
|
|
14
14
|
definitions: definitions.global,
|
|
15
15
|
state: input.state,
|
|
16
|
+
observe: () => rest.get?.(Routes.applicationCommands(input.applicationId)),
|
|
16
17
|
update: () => rest.put(Routes.applicationCommands(input.applicationId), {
|
|
17
18
|
body: definitions.global,
|
|
18
19
|
}),
|
|
@@ -25,6 +26,15 @@ export async function synchroniseApplicationCommands(input) {
|
|
|
25
26
|
guildCount: 0,
|
|
26
27
|
globalHash,
|
|
27
28
|
};
|
|
29
|
+
if (input.synchroniseInternalGuild === false)
|
|
30
|
+
return {
|
|
31
|
+
global,
|
|
32
|
+
guild: "delegated",
|
|
33
|
+
globalCount: definitions.global.length,
|
|
34
|
+
guildCount: definitions.guild.length,
|
|
35
|
+
globalHash,
|
|
36
|
+
guildHash: commandDefinitionsHash(definitions.guild),
|
|
37
|
+
};
|
|
28
38
|
const guildKey = `commands:${input.applicationId}:guild:${input.internalGuildId}`;
|
|
29
39
|
const guildHash = commandDefinitionsHash(definitions.guild);
|
|
30
40
|
const guild = await synchroniseCollection({
|
|
@@ -32,6 +42,7 @@ export async function synchroniseApplicationCommands(input) {
|
|
|
32
42
|
hash: guildHash,
|
|
33
43
|
definitions: definitions.guild,
|
|
34
44
|
state: input.state,
|
|
45
|
+
observe: () => rest.get?.(Routes.applicationGuildCommands(input.applicationId, input.internalGuildId)),
|
|
35
46
|
update: () => rest.put(Routes.applicationGuildCommands(input.applicationId, input.internalGuildId), { body: definitions.guild }),
|
|
36
47
|
});
|
|
37
48
|
return {
|
|
@@ -48,8 +59,13 @@ export function commandDefinitionsHash(definitions) {
|
|
|
48
59
|
}
|
|
49
60
|
async function synchroniseCollection(input) {
|
|
50
61
|
const current = await input.state.get(input.key);
|
|
51
|
-
if (isCommandHashState(current?.value, input.hash))
|
|
52
|
-
|
|
62
|
+
if (isCommandHashState(current?.value, input.hash)) {
|
|
63
|
+
const observed = input.observe();
|
|
64
|
+
if (observed &&
|
|
65
|
+
commandDefinitionsHash(normalizeObservedCommands(await observed)) ===
|
|
66
|
+
commandDefinitionsHash(normalizeDesiredCommands(input.definitions)))
|
|
67
|
+
return "unchanged";
|
|
68
|
+
}
|
|
53
69
|
await input.update();
|
|
54
70
|
await input.state.set(input.key, {
|
|
55
71
|
hash: input.hash,
|
|
@@ -58,6 +74,54 @@ async function synchroniseCollection(input) {
|
|
|
58
74
|
}, current?.version ?? null);
|
|
59
75
|
return "updated";
|
|
60
76
|
}
|
|
77
|
+
function normalizeObservedCommands(value) {
|
|
78
|
+
if (!Array.isArray(value))
|
|
79
|
+
return [];
|
|
80
|
+
return value
|
|
81
|
+
.map((command) => normalizeDiscordCommand(command))
|
|
82
|
+
.sort((left, right) => commandName(left).localeCompare(commandName(right)));
|
|
83
|
+
}
|
|
84
|
+
function normalizeDesiredCommands(value) {
|
|
85
|
+
return value
|
|
86
|
+
.map((command) => normalizeDiscordCommand(command))
|
|
87
|
+
.sort((left, right) => commandName(left).localeCompare(commandName(right)));
|
|
88
|
+
}
|
|
89
|
+
function normalizeDiscordCommand(value, root = true) {
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return value.map((item) => normalizeDiscordCommand(item, false));
|
|
92
|
+
if (!value || typeof value !== "object")
|
|
93
|
+
return value;
|
|
94
|
+
const ignored = new Set(["id", "application_id", "guild_id", "version"]);
|
|
95
|
+
const result = Object.fromEntries(Object.entries(value)
|
|
96
|
+
.filter(([key, nested]) => {
|
|
97
|
+
if (ignored.has(key) || nested === undefined)
|
|
98
|
+
return false;
|
|
99
|
+
if (key === "nsfw" && nested === false)
|
|
100
|
+
return false;
|
|
101
|
+
if (key === "default_permission" && nested === true)
|
|
102
|
+
return false;
|
|
103
|
+
if (key === "default_member_permissions" && nested === null)
|
|
104
|
+
return false;
|
|
105
|
+
if (key === "required" && nested === false)
|
|
106
|
+
return false;
|
|
107
|
+
if (key.endsWith("_localizations") && nested === null)
|
|
108
|
+
return false;
|
|
109
|
+
if (key === "options" && Array.isArray(nested) && nested.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
return true;
|
|
112
|
+
})
|
|
113
|
+
.map(([key, nested]) => [key, normalizeDiscordCommand(nested, false)]));
|
|
114
|
+
if (root) {
|
|
115
|
+
result.contexts ??= [0];
|
|
116
|
+
result.integration_types ??= [0];
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function commandName(value) {
|
|
121
|
+
return value && typeof value === "object" && "name" in value
|
|
122
|
+
? String(value.name)
|
|
123
|
+
: "";
|
|
124
|
+
}
|
|
61
125
|
function isCommandHashState(value, expectedHash) {
|
|
62
126
|
return (typeof value === "object" &&
|
|
63
127
|
value !== null &&
|
package/dist/control-server.d.ts
CHANGED
|
@@ -3,10 +3,13 @@ import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourc
|
|
|
3
3
|
import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
|
|
4
4
|
import { type Client } from "discord.js";
|
|
5
5
|
import { type FastifyInstance } from "fastify";
|
|
6
|
+
import { type BotModuleStatusMutationResult } from "./module-status-control.js";
|
|
7
|
+
import { type FirstTimeCommandSetupHandler } from "./first-time-command-setup.js";
|
|
6
8
|
import { type ShardHealthSnapshot } from "./shard-contracts.js";
|
|
7
9
|
import { type DashboardPermissionState } from "./control-permissions.js";
|
|
8
10
|
import { type DashboardCommandDefinition } from "./dashboard-commands.js";
|
|
9
11
|
import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
|
|
12
|
+
import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
|
|
10
13
|
interface DashboardLogEventDefinition {
|
|
11
14
|
id: string;
|
|
12
15
|
name: string;
|
|
@@ -139,7 +142,7 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
|
|
|
139
142
|
actorHighestRolePosition: number | null;
|
|
140
143
|
updatedAt: string | null;
|
|
141
144
|
}
|
|
142
|
-
export interface BotControlHandler {
|
|
145
|
+
export interface BotControlHandler extends FirstTimeCommandSetupHandler {
|
|
143
146
|
cleanupOAuthConsumer(input: {
|
|
144
147
|
consumerBindingId: string;
|
|
145
148
|
moduleId: string;
|
|
@@ -193,7 +196,7 @@ export interface BotControlHandler {
|
|
|
193
196
|
enabled: boolean;
|
|
194
197
|
actorUserId: string;
|
|
195
198
|
correlationId: string;
|
|
196
|
-
}): Promise<
|
|
199
|
+
}): Promise<BotModuleStatusMutationResult>;
|
|
197
200
|
updateConfiguration(input: {
|
|
198
201
|
guildId: string;
|
|
199
202
|
moduleId: string;
|
|
@@ -422,7 +425,15 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
422
425
|
enabled: boolean;
|
|
423
426
|
actorUserId: string;
|
|
424
427
|
correlationId: string;
|
|
425
|
-
}): Promise<
|
|
428
|
+
}): Promise<BotModuleStatusMutationResult>;
|
|
429
|
+
completeFirstTimeCommandSetup(input: {
|
|
430
|
+
guildId: string;
|
|
431
|
+
moduleIds: string[];
|
|
432
|
+
expectedRevision: bigint;
|
|
433
|
+
idempotencyKey: string;
|
|
434
|
+
actorUserId: string;
|
|
435
|
+
correlationId: string;
|
|
436
|
+
}): Promise<BotModuleStatusMutationResult>;
|
|
426
437
|
updateConfiguration(input: {
|
|
427
438
|
guildId: string;
|
|
428
439
|
moduleId: string;
|
|
@@ -548,6 +559,7 @@ export declare function createBotControlServer(input: {
|
|
|
548
559
|
level: string;
|
|
549
560
|
};
|
|
550
561
|
shardHealth?: () => Promise<ShardHealthSnapshot>;
|
|
562
|
+
guildCommands?: Parameters<typeof registerGuildCommandControlRoutes>[1];
|
|
551
563
|
}): Promise<FastifyInstance>;
|
|
552
564
|
export declare function mapBotControlError(error: unknown): {
|
|
553
565
|
status: 400 | 403 | 404 | 409;
|
package/dist/control-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuditRepository, GuildAccessPolicyRepository, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
|
|
1
|
+
import { AuditRepository, GuildAccessPolicyRepository, GuildCommandActiveLeaseError, GuildCommandRevisionConflictError, GuildCommandSetupConflictError, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
|
|
2
2
|
import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
|
|
3
3
|
import { actionReceiptSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
|
|
4
4
|
import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, permissionMutationDenialMessage, resolveHelyxAuthority, } from "@helyx/core";
|
|
@@ -7,12 +7,14 @@ import Fastify, {} from "fastify";
|
|
|
7
7
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import { z, ZodError } from "zod";
|
|
9
9
|
import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteChannelMessage as deleteDiscordChannelMessage, editChannelMessage as editDiscordChannelMessage, normalizeDiscordPermission, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
|
|
10
|
-
import {
|
|
10
|
+
import { mutateDashboardModuleStatus, } from "./module-status-control.js";
|
|
11
|
+
import { completeFirstTimeCommandSetup, } from "./first-time-command-setup.js";
|
|
11
12
|
import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
|
|
12
13
|
import { dashboardCategoryLabel } from "./dashboard-category.js";
|
|
13
14
|
import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
|
|
14
15
|
import { dashboardCommands, } from "./dashboard-commands.js";
|
|
15
16
|
import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, } from "./module-availability-control.js";
|
|
17
|
+
import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
|
|
16
18
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
17
19
|
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
18
20
|
const MAX_DISCORD_SETTING_SELECTIONS = 25;
|
|
@@ -628,53 +630,21 @@ export class BotControlService {
|
|
|
628
630
|
}
|
|
629
631
|
async setModuleStatus(input) {
|
|
630
632
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
.map((
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
})
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
moduleId: input.moduleId,
|
|
646
|
-
enabled: input.enabled,
|
|
647
|
-
context: {
|
|
648
|
-
actorUserId: input.actorUserId,
|
|
649
|
-
correlationId: input.correlationId,
|
|
650
|
-
source: "dashboard",
|
|
651
|
-
},
|
|
633
|
+
return mutateDashboardModuleStatus({
|
|
634
|
+
change: input,
|
|
635
|
+
module,
|
|
636
|
+
dashboardModuleIds: this.#dashboardModules.map(({ id }) => id),
|
|
637
|
+
installations: this.#installations,
|
|
638
|
+
services: this.#services,
|
|
639
|
+
requireModuleAvailable: () => this.#requireModuleAvailable(input.moduleId),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
async completeFirstTimeCommandSetup(input) {
|
|
643
|
+
return completeFirstTimeCommandSetup({
|
|
644
|
+
installations: this.#installations,
|
|
645
|
+
dashboardModuleIds: this.#dashboardModules.map(({ id }) => id),
|
|
646
|
+
change: input,
|
|
652
647
|
});
|
|
653
|
-
if (!writeBehind) {
|
|
654
|
-
await setEnabled();
|
|
655
|
-
}
|
|
656
|
-
else if (input.enabled) {
|
|
657
|
-
for (const scope of scopes) {
|
|
658
|
-
const suppression = await writeBehind.suppressGuild(scope);
|
|
659
|
-
await writeBehind.releaseGuild({
|
|
660
|
-
...scope,
|
|
661
|
-
suppressionId: suppression.suppressionId,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
await setEnabled();
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
await runNestedGuildBarriers({
|
|
668
|
-
scopes,
|
|
669
|
-
writeBehind,
|
|
670
|
-
operation: setEnabled,
|
|
671
|
-
});
|
|
672
|
-
for (const scope of scopes)
|
|
673
|
-
await writeBehind.flushGuild(scope);
|
|
674
|
-
for (const scope of scopes)
|
|
675
|
-
await writeBehind.suppressGuild(scope);
|
|
676
|
-
}
|
|
677
|
-
return this.getGuildState(input.guildId);
|
|
678
648
|
}
|
|
679
649
|
async updateConfiguration(input) {
|
|
680
650
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
@@ -1253,6 +1223,7 @@ export async function createBotControlServer(input) {
|
|
|
1253
1223
|
}
|
|
1254
1224
|
server.get("/health", (_request, reply) => readiness(reply));
|
|
1255
1225
|
server.get("/v1/health", (_request, reply) => readiness(reply));
|
|
1226
|
+
registerGuildCommandControlRoutes(server, input.guildCommands, input.service);
|
|
1256
1227
|
server.post("/v1/verification/results", async (request) => input.service.acceptVerificationResult(signedAssessmentEnvelopeSchema.parse(request.body)));
|
|
1257
1228
|
server.post("/v1/oauth/consumers/cleanup", async (request) => {
|
|
1258
1229
|
const body = z
|
|
@@ -1747,6 +1718,21 @@ export function mapBotControlError(error) {
|
|
|
1747
1718
|
status: 409,
|
|
1748
1719
|
message: "Configuration changed in another session. Reload and try again.",
|
|
1749
1720
|
};
|
|
1721
|
+
if (error instanceof GuildCommandRevisionConflictError)
|
|
1722
|
+
return {
|
|
1723
|
+
status: 409,
|
|
1724
|
+
message: "Command state changed in another session. Refresh and try again.",
|
|
1725
|
+
};
|
|
1726
|
+
if (error instanceof GuildCommandActiveLeaseError)
|
|
1727
|
+
return {
|
|
1728
|
+
status: 409,
|
|
1729
|
+
message: "A command build is already running. Wait for it to finish.",
|
|
1730
|
+
};
|
|
1731
|
+
if (error instanceof GuildCommandSetupConflictError)
|
|
1732
|
+
return {
|
|
1733
|
+
status: 409,
|
|
1734
|
+
message: "Command setup changed. Reload the page and try again.",
|
|
1735
|
+
};
|
|
1750
1736
|
return undefined;
|
|
1751
1737
|
}
|
|
1752
1738
|
class BotControlValidationError extends Error {
|
|
@@ -27,7 +27,11 @@ export function dashboardCommands(module) {
|
|
|
27
27
|
}),
|
|
28
28
|
];
|
|
29
29
|
});
|
|
30
|
-
commands.push(...(module.
|
|
30
|
+
commands.push(...(module.sharedCommands ?? []).map((command) => dashboardCommand(module, {
|
|
31
|
+
id: command.permissionId ?? `${command.family}.${command.path}`,
|
|
32
|
+
name: `/${command.family} ${command.path.replace(".", " ")}`,
|
|
33
|
+
description: command.description,
|
|
34
|
+
})), ...(module.nexusCommands ?? []).map((command) => dashboardCommand(module, {
|
|
31
35
|
id: `nexus.${command.path}`,
|
|
32
36
|
name: `/nexus ${command.path.replace(".", " ")}`,
|
|
33
37
|
description: command.description,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ModuleDefinition } from "@helyx/sdk";
|
|
2
2
|
import { type ShardHealthSnapshot } from "./shard-contracts.js";
|
|
3
|
+
import type { TriviaDataProvider } from "./trivia-data-provider.js";
|
|
3
4
|
export interface ShardHealthProvider {
|
|
4
5
|
getHealth(): Promise<ShardHealthSnapshot>;
|
|
5
6
|
}
|
|
@@ -17,6 +18,7 @@ export interface DeveloperOperationsInput {
|
|
|
17
18
|
modules: readonly DeveloperModuleSummary[];
|
|
18
19
|
processLabel: string;
|
|
19
20
|
health: ShardHealthProvider;
|
|
21
|
+
triviaData: TriviaDataProvider;
|
|
20
22
|
}
|
|
21
23
|
export declare function createDeveloperOperationsModule(input: DeveloperOperationsInput): ModuleDefinition;
|
|
22
24
|
export declare class WorkerShardHealthProvider implements ShardHealthProvider {
|
|
@@ -35,6 +35,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
35
35
|
name: "shards",
|
|
36
36
|
description: "Show bounded read-only shard health",
|
|
37
37
|
acknowledgement: "deferred-ephemeral",
|
|
38
|
+
authorization: "self-authorized",
|
|
38
39
|
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
39
40
|
async execute(context) {
|
|
40
41
|
if (!(await authorizeDeveloperCommand(context)))
|
|
@@ -83,8 +84,8 @@ export function createDeveloperOperationsModule(input) {
|
|
|
83
84
|
{
|
|
84
85
|
name: "bot",
|
|
85
86
|
description: "Show the bot build and installed module versions",
|
|
86
|
-
acknowledgement: "
|
|
87
|
-
|
|
87
|
+
acknowledgement: "immediate-response",
|
|
88
|
+
authorization: "self-authorized",
|
|
88
89
|
async execute(context) {
|
|
89
90
|
if (!(await authorizeDeveloperCommand(context)))
|
|
90
91
|
return;
|
|
@@ -137,8 +138,8 @@ export function createDeveloperOperationsModule(input) {
|
|
|
137
138
|
{
|
|
138
139
|
name: "proc",
|
|
139
140
|
description: "Show safe process resource and uptime information",
|
|
140
|
-
acknowledgement: "
|
|
141
|
-
|
|
141
|
+
acknowledgement: "immediate-response",
|
|
142
|
+
authorization: "self-authorized",
|
|
142
143
|
async execute(context) {
|
|
143
144
|
if (!(await authorizeDeveloperCommand(context)))
|
|
144
145
|
return;
|
|
@@ -168,6 +169,57 @@ export function createDeveloperOperationsModule(input) {
|
|
|
168
169
|
});
|
|
169
170
|
},
|
|
170
171
|
},
|
|
172
|
+
{
|
|
173
|
+
name: "trivia-data",
|
|
174
|
+
description: "Show question counts from the Trivia database",
|
|
175
|
+
acknowledgement: "deferred-ephemeral",
|
|
176
|
+
authorization: "self-authorized",
|
|
177
|
+
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
178
|
+
async execute(context) {
|
|
179
|
+
if (!(await authorizeDeveloperCommand(context)))
|
|
180
|
+
return;
|
|
181
|
+
let summary;
|
|
182
|
+
try {
|
|
183
|
+
summary = await input.triviaData.getSummary();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
await context.reply({
|
|
187
|
+
ephemeral: true,
|
|
188
|
+
componentsV2: {
|
|
189
|
+
accentColor: 0xdc2626,
|
|
190
|
+
text: [
|
|
191
|
+
{
|
|
192
|
+
content: "Trivia data is unavailable. Check the bot database connection and Trivia migrations.",
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const categoryLines = summary.categories.map((entry) => {
|
|
200
|
+
const total = entry.easy + entry.medium + entry.hard;
|
|
201
|
+
return `**${categoryLabel(entry.category)}**: Easy ${entry.easy.toLocaleString("en-GB")} | Medium ${entry.medium.toLocaleString("en-GB")} | Hard ${entry.hard.toLocaleString("en-GB")} | Total ${total.toLocaleString("en-GB")}`;
|
|
202
|
+
});
|
|
203
|
+
await context.reply({
|
|
204
|
+
ephemeral: true,
|
|
205
|
+
componentsV2: {
|
|
206
|
+
accentColor: 0x2563eb,
|
|
207
|
+
text: [
|
|
208
|
+
{
|
|
209
|
+
content: [
|
|
210
|
+
"## Trivia data",
|
|
211
|
+
`**Unique questions:** ${summary.uniqueQuestions.toLocaleString("en-GB")}`,
|
|
212
|
+
`**Multiple Choice:** ${summary.types.multipleChoice.toLocaleString("en-GB")} | **Guess:** ${summary.types.guess.toLocaleString("en-GB")} | **True or False:** ${summary.types.trueFalse.toLocaleString("en-GB")}`,
|
|
213
|
+
"",
|
|
214
|
+
"### Questions by category",
|
|
215
|
+
...categoryLines,
|
|
216
|
+
].join("\n"),
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
},
|
|
171
223
|
],
|
|
172
224
|
},
|
|
173
225
|
],
|
|
@@ -198,6 +250,15 @@ export function createDeveloperOperationsModule(input) {
|
|
|
198
250
|
function safeInlineCode(value) {
|
|
199
251
|
return `\`${value.replaceAll("`", "'").slice(0, 120)}\``;
|
|
200
252
|
}
|
|
253
|
+
function categoryLabel(category) {
|
|
254
|
+
if (category === "science")
|
|
255
|
+
return "Science and Nature";
|
|
256
|
+
return category
|
|
257
|
+
.split("_")
|
|
258
|
+
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
|
|
259
|
+
.join(" ")
|
|
260
|
+
.replace("And", "and");
|
|
261
|
+
}
|
|
201
262
|
function formatBytes(value) {
|
|
202
263
|
return `${(value / (1024 * 1024)).toFixed(1)} MiB`;
|
|
203
264
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Agent } from "undici";
|
|
2
|
+
const discordRestAgent = new Agent({
|
|
3
|
+
keepAliveTimeout: 60_000,
|
|
4
|
+
keepAliveMaxTimeout: 120_000,
|
|
5
|
+
connections: 10,
|
|
6
|
+
pipelining: 1,
|
|
7
|
+
});
|
|
8
|
+
export const discordRestOptions = { agent: discordRestAgent };
|
|
9
|
+
//# sourceMappingURL=discord-rest.js.map
|
package/dist/events.js
CHANGED
|
@@ -1002,10 +1002,12 @@ export class DiscordMessageDeliveryService {
|
|
|
1002
1002
|
async send(input) {
|
|
1003
1003
|
validateSnowflake(input.guildId, "guild");
|
|
1004
1004
|
validateSnowflake(input.channelId, "channel");
|
|
1005
|
-
const guild =
|
|
1005
|
+
const guild = this.client.guilds.cache.get(input.guildId) ??
|
|
1006
|
+
(await this.client.guilds.fetch(input.guildId));
|
|
1006
1007
|
if (guild.id !== input.guildId)
|
|
1007
1008
|
throw new Error("Discord guild validation failed");
|
|
1008
|
-
const channel =
|
|
1009
|
+
const channel = guild.channels.cache.get(input.channelId) ??
|
|
1010
|
+
(await guild.channels.fetch(input.channelId));
|
|
1009
1011
|
if (!channel ||
|
|
1010
1012
|
channel.guild.id !== guild.id ||
|
|
1011
1013
|
!channel.isSendable() ||
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InstallationService } from "@helyx/sdk";
|
|
2
|
+
import type { BotModuleStatusMutationResult } from "./module-status-control.js";
|
|
3
|
+
export interface FirstTimeCommandSetupHandler {
|
|
4
|
+
completeFirstTimeCommandSetup(input: {
|
|
5
|
+
guildId: string;
|
|
6
|
+
moduleIds: string[];
|
|
7
|
+
expectedRevision: bigint;
|
|
8
|
+
idempotencyKey: string;
|
|
9
|
+
actorUserId: string;
|
|
10
|
+
correlationId: string;
|
|
11
|
+
}): Promise<BotModuleStatusMutationResult>;
|
|
12
|
+
}
|
|
13
|
+
export declare function completeFirstTimeCommandSetup(input: {
|
|
14
|
+
installations: InstallationService;
|
|
15
|
+
dashboardModuleIds: readonly string[];
|
|
16
|
+
change: {
|
|
17
|
+
guildId: string;
|
|
18
|
+
moduleIds: string[];
|
|
19
|
+
expectedRevision: bigint;
|
|
20
|
+
idempotencyKey: string;
|
|
21
|
+
actorUserId: string;
|
|
22
|
+
correlationId: string;
|
|
23
|
+
};
|
|
24
|
+
}): Promise<BotModuleStatusMutationResult>;
|
|
25
|
+
//# sourceMappingURL=first-time-command-setup.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function completeFirstTimeCommandSetup(input) {
|
|
2
|
+
if (!input.installations.completeFirstTimeSetup)
|
|
3
|
+
throw new Error("First-time setup is unavailable");
|
|
4
|
+
const result = await input.installations.completeFirstTimeSetup({
|
|
5
|
+
guildId: input.change.guildId,
|
|
6
|
+
moduleIds: input.change.moduleIds,
|
|
7
|
+
expectedRevision: input.change.expectedRevision,
|
|
8
|
+
idempotencyKey: input.change.idempotencyKey,
|
|
9
|
+
context: {
|
|
10
|
+
actorUserId: input.change.actorUserId,
|
|
11
|
+
correlationId: input.change.correlationId,
|
|
12
|
+
source: "dashboard",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
const dashboardModuleIds = new Set(input.dashboardModuleIds);
|
|
16
|
+
return {
|
|
17
|
+
moduleStatuses: Object.fromEntries(result.transitionedModuleIds
|
|
18
|
+
.filter((moduleId) => dashboardModuleIds.has(moduleId))
|
|
19
|
+
.map((moduleId) => [moduleId, "enabled"])),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=first-time-command-setup.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { GuildCommandReadHandler } from "./guild-command-read-service.js";
|
|
3
|
+
import type { BotControlHandler } from "./control-server.js";
|
|
4
|
+
export declare function registerGuildCommandControlRoutes(server: FastifyInstance, commands: GuildCommandReadHandler | undefined, setup: Pick<BotControlHandler, "completeFirstTimeCommandSetup">): void;
|
|
5
|
+
//# sourceMappingURL=guild-command-control-routes.d.ts.map
|