@helyx/bot 0.4.25 → 0.4.27
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 +12 -0
- package/README.md +3 -5
- package/dist/cloud-connector.js +1 -0
- package/dist/cloud-control-dispatcher.js +13 -0
- package/dist/control-server.d.ts +34 -1
- package/dist/control-server.js +65 -0
- package/dist/index.js +11 -3
- package/dist/shard-contracts.d.ts +21 -0
- package/dist/shard-contracts.js +20 -0
- package/dist/shard-coordinator.js +16 -2
- package/dist/shard-router.d.ts +1 -0
- package/dist/shard-router.js +3 -0
- package/dist/shard-supervisor.js +12 -3
- package/dist/shard-worker.d.ts +0 -1
- package/dist/shard-worker.js +0 -9
- package/package.json +2 -2
- package/dist/trivia-provider.d.ts +0 -15
- package/dist/trivia-provider.js +0 -123
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.27
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Allow a strictly newer coordinated deployment to atomically replace an incompatible module-catalogue generation while preserving application and environment namespace isolation.
|
|
8
|
+
|
|
9
|
+
## 0.4.26
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Require the database runner that installs packaged module migration seeds before runtime readiness.
|
|
14
|
+
|
|
3
15
|
## 0.4.25
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -23,11 +23,9 @@ resume secure weighted-selection checkpoints without exposing the selection
|
|
|
23
23
|
key to a module. Keep it stable across deployments; do not rotate it while a
|
|
24
24
|
giveaway is drawing.
|
|
25
25
|
|
|
26
|
-
Trivia uses
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Complete subscription. Local development may exercise Multiple Choice without
|
|
30
|
-
a key; production never enables the provider adapter without one.
|
|
26
|
+
Trivia uses questions already imported into PostgreSQL. Multiple Choice and
|
|
27
|
+
Guess both run entirely from the saved question catalogue, and the bot runtime
|
|
28
|
+
does not call an external trivia provider.
|
|
31
29
|
|
|
32
30
|
The always-available guild-only `/bug` command can be used by any guild member.
|
|
33
31
|
Hosted deployments submit through the dedicated `HELYX_SUPPORT_SERVICE_SECRET`;
|
package/dist/cloud-connector.js
CHANGED
|
@@ -719,6 +719,7 @@ function connectorCapabilities() {
|
|
|
719
719
|
return [
|
|
720
720
|
"installation.health.read.v1",
|
|
721
721
|
"guild.state.read.v1",
|
|
722
|
+
"guild.module.public.read.v1",
|
|
722
723
|
"guild.module.status.write.v1",
|
|
723
724
|
"guild.module.configuration.write.v1",
|
|
724
725
|
"guild.module.permissions.write.v1",
|
|
@@ -25,6 +25,19 @@ export class CloudControlDispatcher {
|
|
|
25
25
|
result = await this.control.getGuildState(input.guildId);
|
|
26
26
|
break;
|
|
27
27
|
}
|
|
28
|
+
case "guild.module.public.read.v1": {
|
|
29
|
+
const input = parseOperationInput(request.operation, request.input);
|
|
30
|
+
if (request.actor.kind !== "public")
|
|
31
|
+
throw new CloudControlDeniedError("public_actor_required");
|
|
32
|
+
result = await this.control.readPublicModuleProjection({
|
|
33
|
+
guildId: input.guildId,
|
|
34
|
+
moduleId: input.moduleId,
|
|
35
|
+
...(input.memberUserId ? { subjectUserId: input.memberUserId } : {}),
|
|
36
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
37
|
+
limit: input.limit,
|
|
38
|
+
});
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
28
41
|
case "guild.module.status.write.v1": {
|
|
29
42
|
const input = parseOperationInput(request.operation, request.input);
|
|
30
43
|
result = await this.control.setModuleStatus({
|
package/dist/control-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
|
|
2
|
-
import { type DashboardActionResult, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import { type DashboardActionResult, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
|
|
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";
|
|
@@ -43,6 +43,7 @@ interface DashboardModuleDefinition {
|
|
|
43
43
|
commands: DashboardCommandDefinition[];
|
|
44
44
|
logEvents: DashboardLogEventDefinition[];
|
|
45
45
|
actions: DashboardActionDefinition[];
|
|
46
|
+
achievementTriggers: NonNullable<ModuleDefinition["manifest"]["contributions"]["achievementTriggers"]>;
|
|
46
47
|
resources: readonly DashboardResourceDefinition[];
|
|
47
48
|
}
|
|
48
49
|
interface DashboardLoggingPolicy {
|
|
@@ -72,6 +73,14 @@ export interface BotControlGuildResources {
|
|
|
72
73
|
colour: string;
|
|
73
74
|
}>;
|
|
74
75
|
}
|
|
76
|
+
export interface BotPublicModuleProjection {
|
|
77
|
+
server: {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
memberCount: number;
|
|
81
|
+
};
|
|
82
|
+
projection: PublicAchievementProjectionPage;
|
|
83
|
+
}
|
|
75
84
|
export type BotGuildPresenceState = "installed" | "not_installed" | "unknown" | "unavailable";
|
|
76
85
|
export interface BotGuildPresenceObservation {
|
|
77
86
|
guildId: string;
|
|
@@ -81,6 +90,11 @@ export interface BotGuildPresenceObservation {
|
|
|
81
90
|
export interface BotControlGuildResourceDirectory {
|
|
82
91
|
listGuildPresence(guildIds: readonly string[]): Promise<BotGuildPresenceObservation[]>;
|
|
83
92
|
getGuildResources(guildId: string): Promise<BotControlGuildResources>;
|
|
93
|
+
getGuildPublicSummary(guildId: string): Promise<{
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
memberCount: number;
|
|
97
|
+
}>;
|
|
84
98
|
sendToChannel(channelId: string, response: MessageResponse): Promise<{
|
|
85
99
|
messageId: string;
|
|
86
100
|
}>;
|
|
@@ -95,6 +109,11 @@ export declare class DiscordGuildResourceDirectory implements BotControlGuildRes
|
|
|
95
109
|
constructor(client: Client);
|
|
96
110
|
listGuildPresence(guildIds: readonly string[]): Promise<BotGuildPresenceObservation[]>;
|
|
97
111
|
getGuildResources(guildId: string): Promise<BotControlGuildResources>;
|
|
112
|
+
getGuildPublicSummary(guildId: string): Promise<{
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
memberCount: number;
|
|
116
|
+
}>;
|
|
98
117
|
sendToChannel(channelId: string, response: MessageResponse): Promise<{
|
|
99
118
|
messageId: string;
|
|
100
119
|
}>;
|
|
@@ -134,6 +153,13 @@ export interface BotControlHandler {
|
|
|
134
153
|
correlationId: string;
|
|
135
154
|
}): Promise<BotGuildPresenceObservation[]>;
|
|
136
155
|
getGuildState(guildId: string): Promise<BotControlState>;
|
|
156
|
+
readPublicModuleProjection(input: {
|
|
157
|
+
guildId: string;
|
|
158
|
+
moduleId: string;
|
|
159
|
+
subjectUserId?: string;
|
|
160
|
+
cursor?: string;
|
|
161
|
+
limit: number;
|
|
162
|
+
}): Promise<BotPublicModuleProjection>;
|
|
137
163
|
canAccessManagedResource(input: {
|
|
138
164
|
guildId: string;
|
|
139
165
|
moduleId: string;
|
|
@@ -328,6 +354,13 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
328
354
|
cursor?: string;
|
|
329
355
|
}): Promise<PrivacyErasePage>;
|
|
330
356
|
getGuildState(guildId: string): Promise<BotControlState>;
|
|
357
|
+
readPublicModuleProjection(input: {
|
|
358
|
+
guildId: string;
|
|
359
|
+
moduleId: string;
|
|
360
|
+
subjectUserId?: string;
|
|
361
|
+
cursor?: string;
|
|
362
|
+
limit: number;
|
|
363
|
+
}): Promise<BotPublicModuleProjection>;
|
|
331
364
|
canAccessManagedResource(input: {
|
|
332
365
|
guildId: string;
|
|
333
366
|
moduleId: string;
|
package/dist/control-server.js
CHANGED
|
@@ -155,6 +155,17 @@ export class DiscordGuildResourceDirectory {
|
|
|
155
155
|
throw error;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
async getGuildPublicSummary(guildId) {
|
|
159
|
+
try {
|
|
160
|
+
const guild = await this.client.guilds.fetch(guildId);
|
|
161
|
+
return { id: guild.id, name: guild.name, memberCount: guild.memberCount };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (isUnknownGuildError(error))
|
|
165
|
+
throw new BotControlNotFoundError();
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
158
169
|
sendToChannel(channelId, response) {
|
|
159
170
|
return sendDiscordMessageToChannel(this.client, channelId, response);
|
|
160
171
|
}
|
|
@@ -414,6 +425,39 @@ export class BotControlService {
|
|
|
414
425
|
logging,
|
|
415
426
|
};
|
|
416
427
|
}
|
|
428
|
+
async readPublicModuleProjection(input) {
|
|
429
|
+
const module = this.#requireModule(input.moduleId);
|
|
430
|
+
if (!module.publicProjection)
|
|
431
|
+
throw new BotControlNotFoundError("Public module projection was not found");
|
|
432
|
+
if (!(await new InstallationRepository(this.database).isModuleEnabled(input.guildId, input.moduleId)))
|
|
433
|
+
throw new BotControlValidationError("This module is not enabled");
|
|
434
|
+
if (input.subjectUserId) {
|
|
435
|
+
if (!this.#services.has(HELYX_SERVICE_NAMES.discordResources))
|
|
436
|
+
throw new BotControlTransportError(503, "Discord membership confirmation is unavailable");
|
|
437
|
+
const [member] = await this.#services
|
|
438
|
+
.get(HELYX_SERVICE_NAMES.discordResources)
|
|
439
|
+
.resolveMemberSnapshots({
|
|
440
|
+
guildId: input.guildId,
|
|
441
|
+
userIds: [input.subjectUserId],
|
|
442
|
+
});
|
|
443
|
+
if (!member?.exists)
|
|
444
|
+
throw new BotControlForbiddenError("Server membership is required");
|
|
445
|
+
}
|
|
446
|
+
const [server, projection] = await Promise.all([
|
|
447
|
+
this.resources.getGuildPublicSummary(input.guildId),
|
|
448
|
+
module.publicProjection.read({
|
|
449
|
+
guildId: input.guildId,
|
|
450
|
+
...(input.subjectUserId
|
|
451
|
+
? { subjectUserId: input.subjectUserId }
|
|
452
|
+
: {}),
|
|
453
|
+
services: this.#services,
|
|
454
|
+
}, {
|
|
455
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
456
|
+
limit: Math.min(10, Math.max(1, input.limit)),
|
|
457
|
+
}),
|
|
458
|
+
]);
|
|
459
|
+
return { server, projection };
|
|
460
|
+
}
|
|
417
461
|
async canAccessManagedResource(input) {
|
|
418
462
|
try {
|
|
419
463
|
const { module } = await this.#prepareManagedResource(input);
|
|
@@ -1095,6 +1139,26 @@ export async function createBotControlServer(input) {
|
|
|
1095
1139
|
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1096
1140
|
return input.service.getGuildState(guildId);
|
|
1097
1141
|
});
|
|
1142
|
+
server.get("/v1/guilds/:guildId/modules/:moduleId/public-projection", async (request) => {
|
|
1143
|
+
const params = z
|
|
1144
|
+
.object({ guildId: snowflake, moduleId })
|
|
1145
|
+
.strict()
|
|
1146
|
+
.parse(request.params);
|
|
1147
|
+
const query = z
|
|
1148
|
+
.object({
|
|
1149
|
+
subjectUserId: snowflake.optional(),
|
|
1150
|
+
cursor: z.string().min(1).max(2_048).optional(),
|
|
1151
|
+
limit: z.coerce.number().int().min(1).max(10).default(10),
|
|
1152
|
+
})
|
|
1153
|
+
.strict()
|
|
1154
|
+
.parse(request.query);
|
|
1155
|
+
return input.service.readPublicModuleProjection({
|
|
1156
|
+
...params,
|
|
1157
|
+
...(query.subjectUserId ? { subjectUserId: query.subjectUserId } : {}),
|
|
1158
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
1159
|
+
limit: query.limit,
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1098
1162
|
server.post("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType/access", async (request) => {
|
|
1099
1163
|
const params = z
|
|
1100
1164
|
.object({
|
|
@@ -1602,6 +1666,7 @@ function mapDashboardModule(module) {
|
|
|
1602
1666
|
commands: dashboardCommands(module),
|
|
1603
1667
|
logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
|
|
1604
1668
|
actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
|
|
1669
|
+
achievementTriggers: (module.manifest.contributions?.achievementTriggers ?? []).map((trigger) => structuredClone(trigger)),
|
|
1605
1670
|
resources: (dashboard.resources ?? []).map((resource) => structuredClone(resource)),
|
|
1606
1671
|
};
|
|
1607
1672
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { parseBotEnvironment } from "@helyx/config";
|
|
|
2
2
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
3
3
|
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
4
4
|
import { createRuntime } from "@helyx/framework";
|
|
5
|
-
import { createPlatformServices, gamingStatisticsRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
5
|
+
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
6
6
|
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
7
7
|
import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
8
8
|
import pino from "pino";
|
|
@@ -292,11 +292,19 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
292
292
|
]),
|
|
293
293
|
}, bootstrapLogger, serviceAccess);
|
|
294
294
|
const moduleEvents = installModuleEvents(client, publicModules, bootstrapLogger, serviceAccess);
|
|
295
|
-
const controlService = new BotControlService(database, [
|
|
295
|
+
const controlService = new BotControlService(database, [
|
|
296
|
+
...publicModules,
|
|
297
|
+
gamingStatisticsRecordOwner,
|
|
298
|
+
achievementContributionRecordOwner,
|
|
299
|
+
], guildResources, serviceAccess, privacyDatabase
|
|
296
300
|
? new PostgresPrivacyProviderExecutor({
|
|
297
301
|
readPool: database,
|
|
298
302
|
executorPool: privacyDatabase,
|
|
299
|
-
modules: [
|
|
303
|
+
modules: [
|
|
304
|
+
...featureModules,
|
|
305
|
+
gamingStatisticsRecordOwner,
|
|
306
|
+
achievementContributionRecordOwner,
|
|
307
|
+
],
|
|
300
308
|
})
|
|
301
309
|
: undefined, environment.DISCORD_INTERNAL_GUILD_ID &&
|
|
302
310
|
environment.HELYX_SUPPORT_STAFF_CHANNEL_ID
|
|
@@ -169,6 +169,13 @@ export type ShardHealthSnapshot = z.infer<typeof shardHealthSnapshotSchema>;
|
|
|
169
169
|
export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
170
170
|
kind: z.ZodLiteral<"guild.state.get">;
|
|
171
171
|
guildId: z.ZodString;
|
|
172
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
173
|
+
kind: z.ZodLiteral<"guild.module.public.read">;
|
|
174
|
+
guildId: z.ZodString;
|
|
175
|
+
moduleId: z.ZodString;
|
|
176
|
+
subjectUserId: z.ZodOptional<z.ZodString>;
|
|
177
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
178
|
+
limit: z.ZodNumber;
|
|
172
179
|
}, z.core.$strict>, z.ZodObject<{
|
|
173
180
|
kind: z.ZodLiteral<"guild.presence.list">;
|
|
174
181
|
guildIds: z.ZodArray<z.ZodString>;
|
|
@@ -478,6 +485,13 @@ export declare const shardControlRequestSchema: z.ZodObject<{
|
|
|
478
485
|
operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
479
486
|
kind: z.ZodLiteral<"guild.state.get">;
|
|
480
487
|
guildId: z.ZodString;
|
|
488
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
489
|
+
kind: z.ZodLiteral<"guild.module.public.read">;
|
|
490
|
+
guildId: z.ZodString;
|
|
491
|
+
moduleId: z.ZodString;
|
|
492
|
+
subjectUserId: z.ZodOptional<z.ZodString>;
|
|
493
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
494
|
+
limit: z.ZodNumber;
|
|
481
495
|
}, z.core.$strict>, z.ZodObject<{
|
|
482
496
|
kind: z.ZodLiteral<"guild.presence.list">;
|
|
483
497
|
guildIds: z.ZodArray<z.ZodString>;
|
|
@@ -1292,6 +1306,13 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
|
|
|
1292
1306
|
operation: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1293
1307
|
kind: z.ZodLiteral<"guild.state.get">;
|
|
1294
1308
|
guildId: z.ZodString;
|
|
1309
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
1310
|
+
kind: z.ZodLiteral<"guild.module.public.read">;
|
|
1311
|
+
guildId: z.ZodString;
|
|
1312
|
+
moduleId: z.ZodString;
|
|
1313
|
+
subjectUserId: z.ZodOptional<z.ZodString>;
|
|
1314
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
1315
|
+
limit: z.ZodNumber;
|
|
1295
1316
|
}, z.core.$strict>, z.ZodObject<{
|
|
1296
1317
|
kind: z.ZodLiteral<"guild.presence.list">;
|
|
1297
1318
|
guildIds: z.ZodArray<z.ZodString>;
|
package/dist/shard-contracts.js
CHANGED
|
@@ -222,6 +222,16 @@ export const shardHealthSnapshotSchema = z
|
|
|
222
222
|
});
|
|
223
223
|
export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
224
224
|
z.object({ kind: z.literal("guild.state.get"), guildId: snowflake }).strict(),
|
|
225
|
+
z
|
|
226
|
+
.object({
|
|
227
|
+
kind: z.literal("guild.module.public.read"),
|
|
228
|
+
guildId: snowflake,
|
|
229
|
+
moduleId,
|
|
230
|
+
subjectUserId: snowflake.optional(),
|
|
231
|
+
cursor: resourceCursor.optional(),
|
|
232
|
+
limit: z.number().int().min(1).max(10),
|
|
233
|
+
})
|
|
234
|
+
.strict(),
|
|
225
235
|
z
|
|
226
236
|
.object({
|
|
227
237
|
kind: z.literal("guild.presence.list"),
|
|
@@ -901,6 +911,16 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
901
911
|
});
|
|
902
912
|
case "guild.state.get":
|
|
903
913
|
return handler.getGuildState(operation.guildId);
|
|
914
|
+
case "guild.module.public.read":
|
|
915
|
+
return handler.readPublicModuleProjection({
|
|
916
|
+
guildId: operation.guildId,
|
|
917
|
+
moduleId: operation.moduleId,
|
|
918
|
+
...(operation.subjectUserId
|
|
919
|
+
? { subjectUserId: operation.subjectUserId }
|
|
920
|
+
: {}),
|
|
921
|
+
...(operation.cursor ? { cursor: operation.cursor } : {}),
|
|
922
|
+
limit: operation.limit,
|
|
923
|
+
});
|
|
904
924
|
case "guild.module.resource.access":
|
|
905
925
|
return handler.canAccessManagedResource({
|
|
906
926
|
guildId: operation.guildId,
|
|
@@ -137,8 +137,22 @@ export class RedisShardCoordinator {
|
|
|
137
137
|
const parsed = activeGenerationSchema.safeParse(parseJson(current ?? ""));
|
|
138
138
|
if (parsed.success) {
|
|
139
139
|
if (JSON.stringify(withoutDeploymentOrder(parsed.data)) !==
|
|
140
|
-
JSON.stringify(accepted))
|
|
141
|
-
|
|
140
|
+
JSON.stringify(accepted)) {
|
|
141
|
+
if (parsed.data.applicationId !== accepted.applicationId ||
|
|
142
|
+
parsed.data.environment !== accepted.environment ||
|
|
143
|
+
deploymentOrder <= parsed.data.deploymentOrder ||
|
|
144
|
+
!current)
|
|
145
|
+
throw new Error("Shard coordination namespace is owned by an incompatible generation");
|
|
146
|
+
const replaced = await compareAndReplace(this.#command, this.#generationKey(), current, serialized, GENERATION_TTL_MS);
|
|
147
|
+
if (replaced)
|
|
148
|
+
return deploymentOrder;
|
|
149
|
+
const winner = activeGenerationSchema.safeParse(parseJson((await this.#command.get(this.#generationKey())) ?? ""));
|
|
150
|
+
if (winner.success &&
|
|
151
|
+
JSON.stringify(withoutDeploymentOrder(winner.data)) ===
|
|
152
|
+
JSON.stringify(accepted))
|
|
153
|
+
return winner.data.deploymentOrder;
|
|
154
|
+
throw new Error("Shard coordination generation changed while it was being validated");
|
|
155
|
+
}
|
|
142
156
|
if (!current ||
|
|
143
157
|
!(await compareAndExpire(this.#command, this.#generationKey(), current, GENERATION_TTL_MS)))
|
|
144
158
|
throw new Error("Shard coordination generation changed while it was being validated");
|
package/dist/shard-router.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
16
16
|
notificationGuildId?: string;
|
|
17
17
|
});
|
|
18
18
|
getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
|
|
19
|
+
readPublicModuleProjection(input: Parameters<BotControlHandler["readPublicModuleProjection"]>[0]): ReturnType<BotControlHandler["readPublicModuleProjection"]>;
|
|
19
20
|
canAccessManagedResource(input: Parameters<BotControlHandler["canAccessManagedResource"]>[0]): ReturnType<BotControlHandler["canAccessManagedResource"]>;
|
|
20
21
|
listGuildPresence(input: Parameters<BotControlHandler["listGuildPresence"]>[0]): ReturnType<BotControlHandler["listGuildPresence"]>;
|
|
21
22
|
setModuleStatus(input: Parameters<BotControlHandler["setModuleStatus"]>[0]): ReturnType<BotControlHandler["setModuleStatus"]>;
|
package/dist/shard-router.js
CHANGED
|
@@ -10,6 +10,9 @@ export class ShardControlRouter {
|
|
|
10
10
|
getGuildState(guildId) {
|
|
11
11
|
return this.#route({ kind: "guild.state.get", guildId });
|
|
12
12
|
}
|
|
13
|
+
readPublicModuleProjection(input) {
|
|
14
|
+
return this.#route({ kind: "guild.module.public.read", ...input });
|
|
15
|
+
}
|
|
13
16
|
canAccessManagedResource(input) {
|
|
14
17
|
return this.#route({
|
|
15
18
|
kind: "guild.module.resource.access",
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
2
2
|
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
3
|
-
import { createPlatformServices, gamingStatisticsRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
3
|
+
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
4
4
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
5
5
|
import { Client, ShardingManager } from "discord.js";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -169,6 +169,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
169
169
|
observedAt: new Date().toISOString(),
|
|
170
170
|
}))),
|
|
171
171
|
getGuildResources: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
172
|
+
getGuildPublicSummary: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
172
173
|
sendToChannel: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
173
174
|
deleteChannelMessage: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
174
175
|
editChannelMessage: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
@@ -230,11 +231,19 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
230
231
|
const services = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
|
|
231
232
|
if (numericWriteBehind)
|
|
232
233
|
services.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
|
|
233
|
-
privacyService = new BotControlService(database, [
|
|
234
|
+
privacyService = new BotControlService(database, [
|
|
235
|
+
...publicModules,
|
|
236
|
+
gamingStatisticsRecordOwner,
|
|
237
|
+
achievementContributionRecordOwner,
|
|
238
|
+
], unavailableResources, new ServiceRegistry(services), privacyDatabase
|
|
234
239
|
? new PostgresPrivacyProviderExecutor({
|
|
235
240
|
readPool: database,
|
|
236
241
|
executorPool: privacyDatabase,
|
|
237
|
-
modules: [
|
|
242
|
+
modules: [
|
|
243
|
+
...featureModules,
|
|
244
|
+
gamingStatisticsRecordOwner,
|
|
245
|
+
achievementContributionRecordOwner,
|
|
246
|
+
],
|
|
238
247
|
})
|
|
239
248
|
: undefined);
|
|
240
249
|
cloudConnector?.setControlHandler(new ShardControlRouter({
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -41,7 +41,6 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
41
41
|
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: number;
|
|
42
42
|
HELYX_PRIVACY_ENABLED: boolean;
|
|
43
43
|
HELYX_MESSAGE_CONTENT_ENABLED: boolean;
|
|
44
|
-
HELYX_TRIVIA_API_KEY: string | undefined;
|
|
45
44
|
HELYX_CLOUD_CONNECT_ENABLED: boolean;
|
|
46
45
|
HELYX_CLOUD_ENROLMENT_TOKEN: string | undefined;
|
|
47
46
|
HELYX_CLOUD_ENROLMENT_FILE: string;
|
package/dist/shard-worker.js
CHANGED
|
@@ -10,7 +10,6 @@ import pino from "pino";
|
|
|
10
10
|
import { createCloudManagementModule } from "./cloud-management-module.js";
|
|
11
11
|
import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
|
|
12
12
|
import { HttpVerificationService } from "./verification-service.js";
|
|
13
|
-
import { HttpTriviaQuestionProvider } from "./trivia-provider.js";
|
|
14
13
|
import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
15
14
|
import { installGuildTracking, } from "./guilds.js";
|
|
16
15
|
import { installInteractions, } from "./interactions.js";
|
|
@@ -183,14 +182,6 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
183
182
|
}));
|
|
184
183
|
platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
|
|
185
184
|
}
|
|
186
|
-
if (environment.HELYX_TRIVIA_API_KEY ||
|
|
187
|
-
environment.NODE_ENV !== "production") {
|
|
188
|
-
platformServices.set(HELYX_SERVICE_NAMES.triviaQuestions, new HttpTriviaQuestionProvider({
|
|
189
|
-
...(environment.HELYX_TRIVIA_API_KEY
|
|
190
|
-
? { apiKey: environment.HELYX_TRIVIA_API_KEY }
|
|
191
|
-
: {}),
|
|
192
|
-
}));
|
|
193
|
-
}
|
|
194
185
|
numericWriteBehind =
|
|
195
186
|
identity.scope === "coordinated"
|
|
196
187
|
? new WorkerNumericWriteBehindProxy(identity)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"description": "Production Discord runtime for the modular Helyx platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@helyx/config": ">=0.5.0 <0.6.0",
|
|
52
52
|
"@helyx/control-protocol": ">=0.3.0 <0.4.0",
|
|
53
53
|
"@helyx/core": ">=0.1.0 <0.2.0",
|
|
54
|
-
"@helyx/database": ">=0.7.
|
|
54
|
+
"@helyx/database": ">=0.7.10 <0.8.0",
|
|
55
55
|
"@helyx/framework": ">=0.1.0 <0.2.0",
|
|
56
56
|
"@helyx/module-manifest": ">=0.6.0 <0.7.0",
|
|
57
57
|
"@helyx/platform": ">=0.4.0 <0.5.0",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type TriviaProviderFailureCode, type TriviaQuestion, type TriviaQuestionProvider } from "@helyx/sdk";
|
|
2
|
-
export interface HttpTriviaQuestionProviderOptions {
|
|
3
|
-
apiKey?: string;
|
|
4
|
-
fetchImplementation?: typeof fetch;
|
|
5
|
-
}
|
|
6
|
-
export declare class HttpTriviaQuestionProvider implements TriviaQuestionProvider {
|
|
7
|
-
#private;
|
|
8
|
-
constructor(options: HttpTriviaQuestionProviderOptions);
|
|
9
|
-
getQuestions(input: Parameters<TriviaQuestionProvider["getQuestions"]>[0]): Promise<readonly TriviaQuestion[]>;
|
|
10
|
-
health(): Promise<{
|
|
11
|
-
status: "healthy" | "degraded" | "unavailable";
|
|
12
|
-
safeCode?: TriviaProviderFailureCode;
|
|
13
|
-
}>;
|
|
14
|
-
}
|
|
15
|
-
//# sourceMappingURL=trivia-provider.d.ts.map
|
package/dist/trivia-provider.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { TRIVIA_CATEGORIES, TriviaProviderError, } from "@helyx/sdk";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
const BASE_URL = "https://the-trivia-api.com/v2";
|
|
4
|
-
const MAX_RESPONSE_BYTES = 256 * 1_024;
|
|
5
|
-
const REQUEST_TIMEOUT_MS = 8_000;
|
|
6
|
-
const questionSchema = z.object({
|
|
7
|
-
category: z.enum(TRIVIA_CATEGORIES),
|
|
8
|
-
id: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/u),
|
|
9
|
-
tags: z.array(z.string().trim().min(1).max(100)).max(100),
|
|
10
|
-
difficulty: z.enum(["easy", "medium", "hard"]),
|
|
11
|
-
regions: z.array(z.string().trim().min(1).max(20)).max(100),
|
|
12
|
-
isNiche: z.boolean(),
|
|
13
|
-
question: z.object({ text: z.string().trim().min(1).max(2_000) }),
|
|
14
|
-
correctAnswer: z.string().trim().min(1).max(500),
|
|
15
|
-
incorrectAnswers: z.array(z.string().trim().min(1).max(500)).max(10),
|
|
16
|
-
type: z.literal("text_choice"),
|
|
17
|
-
});
|
|
18
|
-
export class HttpTriviaQuestionProvider {
|
|
19
|
-
#apiKey;
|
|
20
|
-
#fetch;
|
|
21
|
-
#lastFailure;
|
|
22
|
-
constructor(options) {
|
|
23
|
-
this.#apiKey = options.apiKey;
|
|
24
|
-
this.#fetch = options.fetchImplementation ?? fetch;
|
|
25
|
-
}
|
|
26
|
-
async getQuestions(input) {
|
|
27
|
-
if (!Number.isSafeInteger(input.limit) ||
|
|
28
|
-
input.limit < 1 ||
|
|
29
|
-
input.limit > 50)
|
|
30
|
-
this.#fail("invalid_response");
|
|
31
|
-
const query = new URLSearchParams({
|
|
32
|
-
limit: input.isNiche === undefined ? String(input.limit) : "50",
|
|
33
|
-
types: input.type,
|
|
34
|
-
contentFilter: input.familyContentOnly ? "family" : "all",
|
|
35
|
-
});
|
|
36
|
-
if (input.category)
|
|
37
|
-
query.set("categories", input.category);
|
|
38
|
-
if (input.difficulty)
|
|
39
|
-
query.set("difficulties", input.difficulty);
|
|
40
|
-
const raw = await this.#request(`/questions?${query.toString()}`, "GET");
|
|
41
|
-
const parsed = z.array(questionSchema).max(50).safeParse(raw);
|
|
42
|
-
if (!parsed.success) {
|
|
43
|
-
this.#fail("invalid_response");
|
|
44
|
-
}
|
|
45
|
-
const matching = parsed.data.filter((question) => question.type === input.type &&
|
|
46
|
-
(input.isNiche === undefined || question.isNiche === input.isNiche));
|
|
47
|
-
this.#lastFailure = undefined;
|
|
48
|
-
return matching.slice(0, input.limit);
|
|
49
|
-
}
|
|
50
|
-
health() {
|
|
51
|
-
return Promise.resolve(this.#lastFailure
|
|
52
|
-
? { status: "degraded", safeCode: this.#lastFailure }
|
|
53
|
-
: { status: "healthy" });
|
|
54
|
-
}
|
|
55
|
-
async #request(path, method) {
|
|
56
|
-
let response;
|
|
57
|
-
try {
|
|
58
|
-
response = await this.#fetch(`${BASE_URL}${path}`, {
|
|
59
|
-
method,
|
|
60
|
-
headers: {
|
|
61
|
-
accept: "application/json",
|
|
62
|
-
...(this.#apiKey ? { "x-api-key": this.#apiKey } : {}),
|
|
63
|
-
},
|
|
64
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
this.#fail(error instanceof DOMException && error.name === "TimeoutError"
|
|
69
|
-
? "timeout"
|
|
70
|
-
: "unavailable");
|
|
71
|
-
}
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
if (response.status === 429)
|
|
74
|
-
this.#fail("rate_limited");
|
|
75
|
-
if (response.status === 401)
|
|
76
|
-
this.#fail("authentication");
|
|
77
|
-
if (response.status === 403)
|
|
78
|
-
this.#fail("subscription");
|
|
79
|
-
this.#fail(response.status >= 500 ? "unavailable" : "invalid_response");
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(await readBoundedResponse(response));
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
if (error instanceof TriviaProviderError)
|
|
86
|
-
throw error;
|
|
87
|
-
this.#fail("invalid_response");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
#fail(code) {
|
|
91
|
-
this.#lastFailure = code;
|
|
92
|
-
throw new TriviaProviderError(code);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
async function readBoundedResponse(response) {
|
|
96
|
-
const declared = Number(response.headers.get("content-length"));
|
|
97
|
-
if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES)
|
|
98
|
-
throw new TriviaProviderError("invalid_response");
|
|
99
|
-
if (!response.body)
|
|
100
|
-
return "";
|
|
101
|
-
const reader = response.body.getReader();
|
|
102
|
-
const chunks = [];
|
|
103
|
-
let total = 0;
|
|
104
|
-
try {
|
|
105
|
-
for (;;) {
|
|
106
|
-
const chunk = await reader.read();
|
|
107
|
-
if (chunk.done)
|
|
108
|
-
break;
|
|
109
|
-
const boundedValue = chunk.value;
|
|
110
|
-
if (!(boundedValue instanceof Uint8Array))
|
|
111
|
-
throw new TriviaProviderError("invalid_response");
|
|
112
|
-
total += boundedValue.byteLength;
|
|
113
|
-
if (total > MAX_RESPONSE_BYTES)
|
|
114
|
-
throw new TriviaProviderError("invalid_response");
|
|
115
|
-
chunks.push(boundedValue);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
reader.releaseLock();
|
|
120
|
-
}
|
|
121
|
-
return Buffer.concat(chunks, total).toString("utf8");
|
|
122
|
-
}
|
|
123
|
-
//# sourceMappingURL=trivia-provider.js.map
|