@helyx/bot 0.4.24 → 0.4.26
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 +4 -0
- package/dist/cloud-connector.d.ts +3 -1
- package/dist/cloud-connector.js +18 -2
- 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/events.js +68 -0
- package/dist/index.js +14 -3
- package/dist/interactions.js +15 -0
- package/dist/modules.js +1 -0
- package/dist/numeric-write-behind-proxy.js +3 -1
- package/dist/shard-contracts.d.ts +21 -0
- package/dist/shard-contracts.js +20 -0
- package/dist/shard-router.d.ts +1 -0
- package/dist/shard-router.js +3 -0
- package/dist/shard-supervisor.js +12 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.26
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Require the database runner that installs packaged module migration seeds before runtime readiness.
|
|
8
|
+
|
|
9
|
+
## 0.4.25
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Launch the shared gaming statistics foundation and the Achievements, Counting and Trivia module integrations, while removing retired Access Control and Counting storage through guarded migrations.
|
|
14
|
+
|
|
3
15
|
## 0.4.24
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ 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 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.
|
|
29
|
+
|
|
26
30
|
The always-available guild-only `/bug` command can be used by any guild member.
|
|
27
31
|
Hosted deployments submit through the dedicated `HELYX_SUPPORT_SERVICE_SECRET`;
|
|
28
32
|
paired self-hosted deployments use their authenticated Cloud Connect session
|
|
@@ -4,7 +4,7 @@ import { type CloudConnectorConsentRecord, type DatabasePool } from "@helyx/data
|
|
|
4
4
|
import type { Client } from "discord.js";
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import type { ConfiguredModulePackage } from "./modules.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type BotControlHandler } from "./control-server.js";
|
|
8
8
|
type ConnectorState = "disabled" | "unpaired" | "connecting" | "online" | "offline" | "recovery_required" | "revoked" | "stopped";
|
|
9
9
|
export interface CloudConnectorStatus {
|
|
10
10
|
state: ConnectorState;
|
|
@@ -80,5 +80,7 @@ export declare class CloudConnectorController implements CloudConnector {
|
|
|
80
80
|
export declare class CloudConnectorUserError extends Error {
|
|
81
81
|
constructor(message: string);
|
|
82
82
|
}
|
|
83
|
+
export declare function mapControlError(error: unknown): "not_found" | "conflict" | "bad_request" | "forbidden" | "temporarily_unavailable" | "internal_error";
|
|
84
|
+
export declare function safeControlMessage(error: unknown): string;
|
|
83
85
|
export {};
|
|
84
86
|
//# sourceMappingURL=cloud-connector.d.ts.map
|
package/dist/cloud-connector.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolve } from "node:path";
|
|
|
6
6
|
import WebSocket from "ws";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { CloudControlDeniedError, CloudControlDispatcher, } from "./cloud-control-dispatcher.js";
|
|
9
|
+
import { BotControlTransportError, } from "./control-server.js";
|
|
9
10
|
const enrolmentFileSchema = z
|
|
10
11
|
.object({
|
|
11
12
|
schemaVersion: z.literal(1),
|
|
@@ -718,6 +719,7 @@ function connectorCapabilities() {
|
|
|
718
719
|
return [
|
|
719
720
|
"installation.health.read.v1",
|
|
720
721
|
"guild.state.read.v1",
|
|
722
|
+
"guild.module.public.read.v1",
|
|
721
723
|
"guild.module.status.write.v1",
|
|
722
724
|
"guild.module.configuration.write.v1",
|
|
723
725
|
"guild.module.permissions.write.v1",
|
|
@@ -786,7 +788,19 @@ function connectedMessage(input) {
|
|
|
786
788
|
payload: input.payload,
|
|
787
789
|
});
|
|
788
790
|
}
|
|
789
|
-
function mapControlError(error) {
|
|
791
|
+
export function mapControlError(error) {
|
|
792
|
+
if (error instanceof BotControlTransportError) {
|
|
793
|
+
if (error.status === 400)
|
|
794
|
+
return "bad_request";
|
|
795
|
+
if (error.status === 403)
|
|
796
|
+
return "forbidden";
|
|
797
|
+
if (error.status === 404)
|
|
798
|
+
return "not_found";
|
|
799
|
+
if (error.status === 409)
|
|
800
|
+
return "conflict";
|
|
801
|
+
if (error.status >= 502)
|
|
802
|
+
return "temporarily_unavailable";
|
|
803
|
+
}
|
|
790
804
|
const name = error instanceof Error ? error.name : "";
|
|
791
805
|
if (name.includes("NotFound"))
|
|
792
806
|
return "not_found";
|
|
@@ -798,7 +812,7 @@ function mapControlError(error) {
|
|
|
798
812
|
return "temporarily_unavailable";
|
|
799
813
|
return "internal_error";
|
|
800
814
|
}
|
|
801
|
-
function safeControlMessage(error) {
|
|
815
|
+
export function safeControlMessage(error) {
|
|
802
816
|
const code = mapControlError(error);
|
|
803
817
|
if (code === "not_found")
|
|
804
818
|
return "The requested Helyx resource was not found";
|
|
@@ -808,6 +822,8 @@ function safeControlMessage(error) {
|
|
|
808
822
|
return error instanceof Error
|
|
809
823
|
? error.message.slice(0, 300)
|
|
810
824
|
: "The requested change was not valid";
|
|
825
|
+
if (code === "forbidden")
|
|
826
|
+
return "You do not have permission to make this change";
|
|
811
827
|
if (code === "temporarily_unavailable")
|
|
812
828
|
return "The self-hosted bot is temporarily unavailable";
|
|
813
829
|
return "The self-hosted bot could not complete the request";
|
|
@@ -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/events.js
CHANGED
|
@@ -14,6 +14,10 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
14
14
|
false,
|
|
15
15
|
contribution,
|
|
16
16
|
})));
|
|
17
|
+
const guildVoiceSessionContributions = modules.flatMap((module) => (module.events?.guildVoiceSession ?? []).map((contribution) => ({
|
|
18
|
+
moduleId: module.manifest.id,
|
|
19
|
+
contribution,
|
|
20
|
+
})));
|
|
17
21
|
const messageClassificationRequested = modules.some((module) => module.requiredDiscordCapabilities?.includes("message-content"));
|
|
18
22
|
const messageClassificationAvailable = messageClassificationRequested &&
|
|
19
23
|
client.options.intents.has(GatewayIntentBits.MessageContent);
|
|
@@ -63,6 +67,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
63
67
|
})));
|
|
64
68
|
if (privilegedMessageContributions.length === 0 &&
|
|
65
69
|
guildMessageContributions.length === 0 &&
|
|
70
|
+
guildVoiceSessionContributions.length === 0 &&
|
|
66
71
|
reactionContributions.length === 0 &&
|
|
67
72
|
reactionRemoveContributions.length === 0 &&
|
|
68
73
|
messageDeleteContributions.length === 0 &&
|
|
@@ -76,6 +81,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
76
81
|
guildEmojiUpdateContributions.length === 0)
|
|
77
82
|
return { stop: () => Promise.resolve(), queueDepth: () => 0 };
|
|
78
83
|
const queue = new BoundedGuildEventQueue(logger);
|
|
84
|
+
const voiceSessions = new Map();
|
|
79
85
|
const privilegedEligibilityQueue = privilegedMessageContributions.length > 0
|
|
80
86
|
? new BoundedGuildEventQueue(logger)
|
|
81
87
|
: null;
|
|
@@ -129,6 +135,13 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
129
135
|
guildMessageQueue.enqueue(message.guildId, () => handleGuildMessageCreate(message));
|
|
130
136
|
});
|
|
131
137
|
}
|
|
138
|
+
if (guildVoiceSessionContributions.length > 0) {
|
|
139
|
+
client.on(Events.VoiceStateUpdate, (before, after) => {
|
|
140
|
+
if ((after.member ?? before.member)?.user.bot)
|
|
141
|
+
return;
|
|
142
|
+
queue.enqueue(after.guild.id, () => handleVoiceState(before, after));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
132
145
|
if (reactionContributions.length > 0) {
|
|
133
146
|
client.on(Events.MessageReactionAdd, (reaction, user, details) => {
|
|
134
147
|
const guildId = reaction.message.guildId;
|
|
@@ -271,6 +284,8 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
271
284
|
channelId: message.channelId,
|
|
272
285
|
...(parentChannelId ? { parentChannelId } : {}),
|
|
273
286
|
messageId: message.id,
|
|
287
|
+
isReply: message.reference?.messageId !== null &&
|
|
288
|
+
message.reference?.messageId !== undefined,
|
|
274
289
|
authorId: message.author.id,
|
|
275
290
|
authorDisplayName,
|
|
276
291
|
authorRoleIds,
|
|
@@ -514,11 +529,64 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
514
529
|
}
|
|
515
530
|
}
|
|
516
531
|
}
|
|
532
|
+
async function handleVoiceState(before, after) {
|
|
533
|
+
const memberId = after.id;
|
|
534
|
+
const key = `${after.guild.id}:${memberId}`;
|
|
535
|
+
const previous = voiceSessions.get(key);
|
|
536
|
+
const channelId = eligibleVoiceChannel(after);
|
|
537
|
+
if (previous && previous.channelId !== channelId) {
|
|
538
|
+
voiceSessions.delete(key);
|
|
539
|
+
const endedAt = new Date();
|
|
540
|
+
const durationSeconds = Math.max(1, Math.floor((endedAt.getTime() - previous.startedAt.getTime()) / 1_000));
|
|
541
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
542
|
+
for (const { moduleId, contribution } of guildVoiceSessionContributions) {
|
|
543
|
+
try {
|
|
544
|
+
if (!(await installations.isModuleEnabled(previous.guildId, moduleId)))
|
|
545
|
+
continue;
|
|
546
|
+
await contribution.execute({
|
|
547
|
+
serverId: previous.guildId,
|
|
548
|
+
channelId: previous.channelId,
|
|
549
|
+
memberId,
|
|
550
|
+
startedAt: previous.startedAt,
|
|
551
|
+
endedAt,
|
|
552
|
+
durationSeconds,
|
|
553
|
+
services,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
logger.warn({
|
|
558
|
+
event: "module.guild_voice_session_failed",
|
|
559
|
+
moduleId,
|
|
560
|
+
guildId: previous.guildId,
|
|
561
|
+
error: safeError(error),
|
|
562
|
+
}, "Module guild voice session event failed");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (channelId && (!previous || previous.channelId !== channelId)) {
|
|
567
|
+
voiceSessions.set(key, {
|
|
568
|
+
guildId: after.guild.id,
|
|
569
|
+
memberId,
|
|
570
|
+
channelId,
|
|
571
|
+
startedAt: new Date(),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function eligibleVoiceChannel(state) {
|
|
576
|
+
if (!state.channelId || !state.channel)
|
|
577
|
+
return null;
|
|
578
|
+
if (state.channel.type !== ChannelType.GuildVoice)
|
|
579
|
+
return null;
|
|
580
|
+
if (state.guild.afkChannelId === state.channelId)
|
|
581
|
+
return null;
|
|
582
|
+
return state.channelId;
|
|
583
|
+
}
|
|
517
584
|
return {
|
|
518
585
|
stop: async () => {
|
|
519
586
|
await privilegedEligibilityQueue?.stop();
|
|
520
587
|
await guildMessageQueue?.stop();
|
|
521
588
|
await queue.stop();
|
|
589
|
+
voiceSessions.clear();
|
|
522
590
|
},
|
|
523
591
|
queueDepth: () => queue.queueDepth() +
|
|
524
592
|
(privilegedEligibilityQueue?.queueDepth() ?? 0) +
|
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, 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
|
|
@@ -484,6 +492,9 @@ export function gatewayIntentsForModules(modules, messageContentEnabled = false)
|
|
|
484
492
|
module.events?.guildMemberRemove?.length)) {
|
|
485
493
|
intents.push(GatewayIntentBits.GuildMembers);
|
|
486
494
|
}
|
|
495
|
+
if (modules.some((module) => module.events?.guildVoiceSession?.length)) {
|
|
496
|
+
intents.push(GatewayIntentBits.GuildVoiceStates);
|
|
497
|
+
}
|
|
487
498
|
if (modules.some((module) => module.events?.guildEmojiDelete?.length ||
|
|
488
499
|
module.events?.guildEmojiUpdate?.length)) {
|
|
489
500
|
intents.push(GatewayIntentBits.GuildExpressions);
|
package/dist/interactions.js
CHANGED
|
@@ -277,6 +277,20 @@ function addCommandOption(builder, option) {
|
|
|
277
277
|
.setRequired(option.required ?? false));
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
|
+
if (option.type === "integer") {
|
|
281
|
+
builder.addIntegerOption((optionBuilder) => {
|
|
282
|
+
optionBuilder
|
|
283
|
+
.setName(option.name)
|
|
284
|
+
.setDescription(option.description)
|
|
285
|
+
.setRequired(option.required ?? false);
|
|
286
|
+
if (option.minimum !== undefined)
|
|
287
|
+
optionBuilder.setMinValue(option.minimum);
|
|
288
|
+
if (option.maximum !== undefined)
|
|
289
|
+
optionBuilder.setMaxValue(option.maximum);
|
|
290
|
+
return optionBuilder;
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
280
294
|
if (option.type === "user") {
|
|
281
295
|
builder.addUserOption((optionBuilder) => optionBuilder
|
|
282
296
|
.setName(option.name)
|
|
@@ -399,6 +413,7 @@ function createCommandContext(interaction, services) {
|
|
|
399
413
|
...createContext(interaction, services),
|
|
400
414
|
getStringOption: (name) => interaction.options.getString(name),
|
|
401
415
|
getBooleanOption: (name) => interaction.options.getBoolean(name),
|
|
416
|
+
getIntegerOption: (name) => interaction.options.getInteger(name),
|
|
402
417
|
getUserOption: (name) => {
|
|
403
418
|
const user = interaction.options.getUser(name);
|
|
404
419
|
return user
|
package/dist/modules.js
CHANGED
|
@@ -100,6 +100,7 @@ export function validateContributionParity(definition) {
|
|
|
100
100
|
const executableEvents = [
|
|
101
101
|
...(definition.events?.privilegedMessageCreate ?? []),
|
|
102
102
|
...(definition.events?.guildMessageCreate ?? []),
|
|
103
|
+
...(definition.events?.guildVoiceSession ?? []),
|
|
103
104
|
...(definition.events?.messageReactionAdd ?? []),
|
|
104
105
|
...(definition.events?.messageReactionRemove ?? []),
|
|
105
106
|
...(definition.events?.messageDelete ?? []),
|
|
@@ -275,8 +275,10 @@ function encodeRecord(input) {
|
|
|
275
275
|
result[field] = { type: "string", value };
|
|
276
276
|
else if (typeof value === "boolean")
|
|
277
277
|
result[field] = { type: "boolean", value };
|
|
278
|
-
else
|
|
278
|
+
else if (typeof value === "number")
|
|
279
279
|
result[field] = { type: "number", value };
|
|
280
|
+
else
|
|
281
|
+
throw new Error("Numeric write-behind values must be scalar");
|
|
280
282
|
}
|
|
281
283
|
return result;
|
|
282
284
|
}
|
|
@@ -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,
|
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, 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.26",
|
|
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",
|