@helyx/bot 0.4.9 → 0.4.13
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 +66 -0
- package/dist/cloud-control-dispatcher.js +2 -1
- package/dist/control-server.d.ts +43 -3
- package/dist/control-server.js +228 -18
- package/dist/events.d.ts +92 -1
- package/dist/events.js +1483 -5
- package/dist/guilds.d.ts +20 -1
- package/dist/guilds.js +149 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +89 -23
- package/dist/interactions.js +101 -22
- package/dist/management-module.d.ts +1 -1
- package/dist/management-module.js +42 -10
- package/dist/message-classification.d.ts +5 -0
- package/dist/message-classification.js +75 -0
- package/dist/modules.js +9 -0
- package/dist/numeric-write-behind-proxy.d.ts +62 -0
- package/dist/numeric-write-behind-proxy.js +313 -0
- package/dist/numeric-write-behind-redis.d.ts +123 -0
- package/dist/numeric-write-behind-redis.js +1281 -0
- package/dist/numeric-write-behind.d.ts +5 -0
- package/dist/numeric-write-behind.js +37 -0
- package/dist/shard-contracts.d.ts +810 -0
- package/dist/shard-contracts.js +292 -3
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +42 -0
- package/dist/shard-supervisor.d.ts +4 -1
- package/dist/shard-supervisor.js +262 -7
- package/dist/shard-worker.d.ts +7 -0
- package/dist/shard-worker.js +64 -10
- package/dist/verification-service.d.ts +50 -0
- package/dist/verification-service.js +166 -0
- package/package.json +10 -9
package/dist/guilds.d.ts
CHANGED
|
@@ -2,5 +2,24 @@ import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
|
|
|
2
2
|
import type { ModuleDefinition } from "@helyx/sdk";
|
|
3
3
|
import { type Client } from "discord.js";
|
|
4
4
|
import type { InteractionLogger } from "./interactions.js";
|
|
5
|
-
export
|
|
5
|
+
export interface GuildReconciliationHealth {
|
|
6
|
+
status: "pending" | "running" | "healthy" | "failed" | "stopped";
|
|
7
|
+
lastStartedAt?: string;
|
|
8
|
+
lastSucceededAt?: string;
|
|
9
|
+
durationMs?: number;
|
|
10
|
+
examinedRows: number;
|
|
11
|
+
refreshedGuilds: number;
|
|
12
|
+
removedGuilds: number;
|
|
13
|
+
rejectedFences: number;
|
|
14
|
+
unavailableGuilds: number;
|
|
15
|
+
failureReasonCode?: string | undefined;
|
|
16
|
+
nextScheduledAt?: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface GuildTrackingPreparation {
|
|
19
|
+
(readyClient: Client<true>): Promise<void>;
|
|
20
|
+
reconcile(reason: "ready" | "resume" | "periodic"): Promise<void>;
|
|
21
|
+
health(): GuildReconciliationHealth;
|
|
22
|
+
stop(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function installGuildTracking(client: Client, database: DatabasePool, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number): GuildTrackingPreparation;
|
|
6
25
|
//# sourceMappingURL=guilds.d.ts.map
|
package/dist/guilds.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { InstallationRepository, } from "@helyx/database";
|
|
2
2
|
import { Events } from "discord.js";
|
|
3
|
-
|
|
3
|
+
const RECONCILIATION_PAGE_SIZE = 250;
|
|
4
|
+
const DEFAULT_RECONCILIATION_INTERVAL_MS = 15 * 60 * 1_000;
|
|
5
|
+
export function installGuildTracking(client, database, modules, logger, enabledByDefaultModuleIds = new Set(), ownership, reconciliationIntervalMs = DEFAULT_RECONCILIATION_INTERVAL_MS, shardCount) {
|
|
6
|
+
if (!Number.isSafeInteger(reconciliationIntervalMs) ||
|
|
7
|
+
reconciliationIntervalMs < 60_000)
|
|
8
|
+
throw new Error("Guild reconciliation interval must be at least one minute");
|
|
4
9
|
const repository = new InstallationRepository(database);
|
|
10
|
+
let prepared = false;
|
|
11
|
+
let stopped = false;
|
|
12
|
+
let reconciliation;
|
|
13
|
+
let timer;
|
|
14
|
+
let health = {
|
|
15
|
+
status: "pending",
|
|
16
|
+
examinedRows: 0,
|
|
17
|
+
refreshedGuilds: 0,
|
|
18
|
+
removedGuilds: 0,
|
|
19
|
+
rejectedFences: 0,
|
|
20
|
+
unavailableGuilds: 0,
|
|
21
|
+
};
|
|
5
22
|
async function syncGuild(guild) {
|
|
6
23
|
const record = await repository.syncGuild({
|
|
7
24
|
guildId: guild.id,
|
|
@@ -39,10 +56,16 @@ export function installGuildTracking(client, database, modules, logger, enabledB
|
|
|
39
56
|
client.on(Events.GuildDelete, (guild) => {
|
|
40
57
|
void repository
|
|
41
58
|
.markGuildRemoved(guild.id, ownership)
|
|
59
|
+
.then((changed) => {
|
|
60
|
+
logger.info({
|
|
61
|
+
event: "guild.removed",
|
|
62
|
+
outcome: changed ? "changed" : "rejected_or_duplicate",
|
|
63
|
+
}, "Discord guild removal observation persisted");
|
|
64
|
+
})
|
|
42
65
|
.catch((error) => {
|
|
43
66
|
logger.error({
|
|
44
67
|
event: "guild.remove_failed",
|
|
45
|
-
|
|
68
|
+
reasonCode: "database_write_failed",
|
|
46
69
|
error: safeError(error),
|
|
47
70
|
}, "Guild removal tracking failed");
|
|
48
71
|
});
|
|
@@ -50,14 +73,131 @@ export function installGuildTracking(client, database, modules, logger, enabledB
|
|
|
50
73
|
client.on(Events.GuildUnavailable, (guild) => {
|
|
51
74
|
logger.warn({ event: "guild.unavailable", guildId: guild.id }, "Discord guild is temporarily unavailable; installation remains active");
|
|
52
75
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
async function runReconciliation(reason) {
|
|
77
|
+
if (stopped || !prepared || !client.isReady())
|
|
78
|
+
return;
|
|
79
|
+
if (reconciliation)
|
|
80
|
+
return reconciliation;
|
|
81
|
+
reconciliation = (async () => {
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
health = {
|
|
84
|
+
...health,
|
|
85
|
+
status: "running",
|
|
86
|
+
lastStartedAt: new Date(startedAt).toISOString(),
|
|
87
|
+
failureReasonCode: undefined,
|
|
88
|
+
nextScheduledAt: undefined,
|
|
89
|
+
};
|
|
90
|
+
let examinedRows = 0;
|
|
91
|
+
let refreshedGuilds = 0;
|
|
92
|
+
let removedGuilds = 0;
|
|
93
|
+
let rejectedFences = 0;
|
|
94
|
+
const observedGuildIds = new Set(client.guilds.cache.keys());
|
|
95
|
+
const unavailableGuilds = [...client.guilds.cache.values()].filter((guild) => !guild.available).length;
|
|
96
|
+
try {
|
|
97
|
+
for (const guild of client.guilds.cache.values()) {
|
|
98
|
+
if (stopped)
|
|
99
|
+
return;
|
|
100
|
+
await syncGuild(guild);
|
|
101
|
+
refreshedGuilds += 1;
|
|
102
|
+
}
|
|
103
|
+
let cursor;
|
|
104
|
+
do {
|
|
105
|
+
if (stopped || !client.isReady())
|
|
106
|
+
return;
|
|
107
|
+
const page = await repository.listActiveGuildIdsForOwnership({
|
|
108
|
+
...(ownership ? { ownership } : {}),
|
|
109
|
+
...(ownership && shardCount !== undefined ? { shardCount } : {}),
|
|
110
|
+
...(cursor ? { cursor } : {}),
|
|
111
|
+
limit: RECONCILIATION_PAGE_SIZE,
|
|
112
|
+
});
|
|
113
|
+
examinedRows += page.guildIds.length;
|
|
114
|
+
if (ownership)
|
|
115
|
+
await repository.adoptGuildsForReconciliation({
|
|
116
|
+
guildIds: page.guildIds,
|
|
117
|
+
ownership,
|
|
118
|
+
shardCount: shardCount,
|
|
119
|
+
});
|
|
120
|
+
const absent = page.guildIds.filter((guildId) => !observedGuildIds.has(guildId));
|
|
121
|
+
const result = await repository.markGuildsRemovedForReconciliation({
|
|
122
|
+
guildIds: absent,
|
|
123
|
+
...(ownership ? { ownership } : {}),
|
|
124
|
+
});
|
|
125
|
+
removedGuilds += result.changed;
|
|
126
|
+
rejectedFences += result.rejectedFence;
|
|
127
|
+
cursor = page.nextCursor;
|
|
128
|
+
} while (cursor);
|
|
129
|
+
const completedAt = Date.now();
|
|
130
|
+
health = {
|
|
131
|
+
status: "healthy",
|
|
132
|
+
lastStartedAt: new Date(startedAt).toISOString(),
|
|
133
|
+
lastSucceededAt: new Date(completedAt).toISOString(),
|
|
134
|
+
durationMs: completedAt - startedAt,
|
|
135
|
+
examinedRows,
|
|
136
|
+
refreshedGuilds,
|
|
137
|
+
removedGuilds,
|
|
138
|
+
rejectedFences,
|
|
139
|
+
unavailableGuilds,
|
|
140
|
+
nextScheduledAt: new Date(completedAt + reconciliationIntervalMs).toISOString(),
|
|
141
|
+
};
|
|
142
|
+
logger.info({
|
|
143
|
+
event: "guild.reconciliation_completed",
|
|
144
|
+
reason,
|
|
145
|
+
durationMs: health.durationMs,
|
|
146
|
+
examinedRows,
|
|
147
|
+
refreshedGuilds,
|
|
148
|
+
removedGuilds,
|
|
149
|
+
rejectedFences,
|
|
150
|
+
unavailableGuilds,
|
|
151
|
+
}, "Discord guild presence reconciliation completed");
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
health = {
|
|
155
|
+
status: "failed",
|
|
156
|
+
lastStartedAt: new Date(startedAt).toISOString(),
|
|
157
|
+
durationMs: Date.now() - startedAt,
|
|
158
|
+
examinedRows,
|
|
159
|
+
refreshedGuilds,
|
|
160
|
+
removedGuilds,
|
|
161
|
+
rejectedFences,
|
|
162
|
+
unavailableGuilds,
|
|
163
|
+
failureReasonCode: "reconciliation_failed",
|
|
164
|
+
nextScheduledAt: new Date(Date.now() + reconciliationIntervalMs).toISOString(),
|
|
165
|
+
};
|
|
166
|
+
logger.error({
|
|
167
|
+
event: "guild.reconciliation_failed",
|
|
168
|
+
reason,
|
|
169
|
+
reasonCode: health.failureReasonCode,
|
|
170
|
+
error: safeError(error),
|
|
171
|
+
}, "Discord guild presence reconciliation failed");
|
|
172
|
+
}
|
|
173
|
+
})().finally(() => {
|
|
174
|
+
reconciliation = undefined;
|
|
175
|
+
});
|
|
176
|
+
return reconciliation;
|
|
177
|
+
}
|
|
178
|
+
const prepare = async (readyClient) => {
|
|
179
|
+
if (stopped || !readyClient.isReady())
|
|
180
|
+
return;
|
|
181
|
+
prepared = true;
|
|
182
|
+
await runReconciliation("ready");
|
|
183
|
+
if (!timer) {
|
|
184
|
+
timer = setInterval(() => void runReconciliation("periodic"), reconciliationIntervalMs);
|
|
185
|
+
timer.unref();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
client.on(Events.ShardResume, () => {
|
|
189
|
+
void runReconciliation("resume");
|
|
190
|
+
});
|
|
191
|
+
prepare.reconcile = runReconciliation;
|
|
192
|
+
prepare.health = () => ({ ...health });
|
|
193
|
+
prepare.stop = () => {
|
|
194
|
+
stopped = true;
|
|
195
|
+
if (timer)
|
|
196
|
+
clearInterval(timer);
|
|
197
|
+
timer = undefined;
|
|
198
|
+
health = { ...health, status: "stopped", nextScheduledAt: undefined };
|
|
60
199
|
};
|
|
200
|
+
return prepare;
|
|
61
201
|
}
|
|
62
202
|
function safeError(error) {
|
|
63
203
|
const value = error instanceof Error ? error : new Error(String(error));
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export interface BotHandle {
|
|
|
9
9
|
export declare function startBot(processEnvironment?: NodeJS.ProcessEnv): Promise<BotHandle>;
|
|
10
10
|
export declare function resolveBotWorkerEntryPoint(entryUrl?: string): string;
|
|
11
11
|
export declare function readBotVersion(): string;
|
|
12
|
-
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[]): GatewayIntentBits[];
|
|
12
|
+
export declare function gatewayIntentsForModules(modules: readonly ModuleDefinition[], messageContentEnabled?: boolean): GatewayIntentBits[];
|
|
13
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
2
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
3
|
-
import { checkDatabaseConnection, createDatabasePool, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
3
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, VerificationSetupRepository, } from "@helyx/database";
|
|
4
4
|
import { createRuntime } from "@helyx/framework";
|
|
5
5
|
import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
6
6
|
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
@@ -18,9 +18,10 @@ import { loadConfiguredModulePackages, platformServiceOptions, } from "./modules
|
|
|
18
18
|
import { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
19
19
|
import { createPermissionsModule } from "./permissions-module.js";
|
|
20
20
|
import { createManagementModule } from "./management-module.js";
|
|
21
|
-
import { DiscordMemberRoleService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
21
|
+
import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordVerificationSetupService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
22
22
|
import { DiscordModuleLoggingService } from "./module-logging.js";
|
|
23
23
|
import { BotControlService, createBotControlServer, DiscordGuildResourceDirectory, } from "./control-server.js";
|
|
24
|
+
import { HttpVerificationService } from "./verification-service.js";
|
|
24
25
|
import { startShardSupervisor } from "./shard-supervisor.js";
|
|
25
26
|
import { parseShardWorkerLaunchContract, startShardWorker, } from "./shard-worker.js";
|
|
26
27
|
import { createDeveloperOperationsModule } from "./developer-operations-module.js";
|
|
@@ -29,6 +30,7 @@ import { createSupportModule } from "./support-module.js";
|
|
|
29
30
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
30
31
|
import { CoreResourceAuthorizationService } from "./resource-authorization.js";
|
|
31
32
|
import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
|
|
33
|
+
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
32
34
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
33
35
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
34
36
|
export * from "./control-server.js";
|
|
@@ -86,15 +88,17 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
86
88
|
verifiedMigrationCount: migrationResult.verified.length,
|
|
87
89
|
}, "Core database ready");
|
|
88
90
|
const cleanupExpiredCoreRecords = async () => {
|
|
89
|
-
const [sessions, rateLimits] = await Promise.all([
|
|
91
|
+
const [sessions, rateLimits, verificationAudit] = await Promise.all([
|
|
90
92
|
new InteractionSessionRepository(database).deleteExpired(),
|
|
91
93
|
new RateLimitRepository(database).deleteExpired(),
|
|
94
|
+
new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
|
|
92
95
|
]);
|
|
93
|
-
if (sessions > 0 || rateLimits > 0) {
|
|
96
|
+
if (sessions > 0 || rateLimits > 0 || verificationAudit > 0) {
|
|
94
97
|
bootstrapLogger.info({
|
|
95
98
|
event: "database.retention_cleaned",
|
|
96
99
|
interactionSessions: sessions,
|
|
97
100
|
rateLimitWindows: rateLimits,
|
|
101
|
+
verificationAuditEvents: verificationAudit,
|
|
98
102
|
}, "Expired core records removed");
|
|
99
103
|
}
|
|
100
104
|
};
|
|
@@ -125,7 +129,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
125
129
|
}, "Module databases ready");
|
|
126
130
|
const featureModules = modulePackages.map(({ definition }) => definition);
|
|
127
131
|
const client = new Client({
|
|
128
|
-
intents: gatewayIntentsForModules(featureModules),
|
|
132
|
+
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED),
|
|
129
133
|
partials: [Partials.Message, Partials.Channel, Partials.Reaction],
|
|
130
134
|
allowedMentions: { parse: [] },
|
|
131
135
|
});
|
|
@@ -234,15 +238,37 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
234
238
|
...publicModules,
|
|
235
239
|
...(operationsModule ? [operationsModule] : []),
|
|
236
240
|
];
|
|
237
|
-
const platformServices = new Map(
|
|
241
|
+
const platformServices = new Map();
|
|
242
|
+
const serviceAccess = new ServiceRegistry(platformServices);
|
|
243
|
+
for (const [name, service] of createPlatformServices(database, publicModules, {
|
|
244
|
+
...platformServiceOptions(environment, publicModules),
|
|
245
|
+
activationServices: serviceAccess,
|
|
246
|
+
})) {
|
|
247
|
+
platformServices.set(name, service);
|
|
248
|
+
}
|
|
249
|
+
if (environment.HELYX_VERIFICATION_ENABLED &&
|
|
250
|
+
environment.HELYX_VERIFICATION_URL &&
|
|
251
|
+
environment.HELYX_VERIFICATION_INTERNAL_SECRET) {
|
|
252
|
+
platformServices.set(HELYX_SERVICE_NAMES.verification, new HttpVerificationService({
|
|
253
|
+
baseUrl: environment.HELYX_VERIFICATION_URL,
|
|
254
|
+
internalSecret: environment.HELYX_VERIFICATION_INTERNAL_SECRET,
|
|
255
|
+
installationId: environment.HELYX_VERIFICATION_INSTALLATION_ID,
|
|
256
|
+
publicSigningKeys: environment.HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS,
|
|
257
|
+
}));
|
|
258
|
+
platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
|
|
259
|
+
platformServices.set(HELYX_SERVICE_NAMES.verificationSetup, new DiscordVerificationSetupService(client, new VerificationSetupRepository(database)));
|
|
260
|
+
}
|
|
261
|
+
const numericWriteBehind = createLocalNumericWriteBehind(platformServices, publicModules);
|
|
262
|
+
platformServices.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
|
|
238
263
|
platformServices.set(HELYX_SERVICE_NAMES.resourceAuthorization, new CoreResourceAuthorizationService(platformServices.get(HELYX_SERVICE_NAMES.permissions)));
|
|
264
|
+
platformServices.set(HELYX_SERVICE_NAMES.discordMessages, new DiscordMessageDeliveryService(client));
|
|
239
265
|
platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
|
|
266
|
+
platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordMemberEnforcementService(client));
|
|
240
267
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, new DiscordMemberRoleService(client));
|
|
241
268
|
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
242
269
|
if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging")) {
|
|
243
270
|
platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, publicModules, bootstrapLogger));
|
|
244
271
|
}
|
|
245
|
-
const serviceAccess = new ServiceRegistry(platformServices);
|
|
246
272
|
const runtime = createRuntime({
|
|
247
273
|
modules,
|
|
248
274
|
logger: bootstrapLogger,
|
|
@@ -255,7 +281,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
255
281
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
256
282
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
257
283
|
]);
|
|
258
|
-
const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds);
|
|
284
|
+
const prepareGuilds = installGuildTracking(client, database, publicModules, bootstrapLogger, enabledCoreModuleIds, undefined, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000);
|
|
259
285
|
const commandsReady = installInteractions(client, modules, {
|
|
260
286
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
261
287
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
@@ -287,6 +313,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
287
313
|
if (shuttingDown)
|
|
288
314
|
return;
|
|
289
315
|
shuttingDown = true;
|
|
316
|
+
prepareGuilds.stop();
|
|
290
317
|
process.off("SIGINT", onSigint);
|
|
291
318
|
process.off("SIGTERM", onSigterm);
|
|
292
319
|
if (retentionCleanupTimer)
|
|
@@ -305,14 +332,29 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
305
332
|
process.exitCode = 1;
|
|
306
333
|
bootstrapLogger.error({ event: "control.stop_failed", error: safeError(error) }, "Private bot control shutdown failed");
|
|
307
334
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
335
|
+
await moduleEvents.stop().catch((error) => {
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
bootstrapLogger.error({ event: "module_events.stop_failed", error: safeError(error) }, "Module event shutdown failed");
|
|
338
|
+
});
|
|
339
|
+
await runtime.stop().catch((error) => {
|
|
313
340
|
process.exitCode = 1;
|
|
314
341
|
bootstrapLogger.error({ event: "runtime.stop_failed", error: safeError(error) }, "Runtime shutdown failed");
|
|
315
|
-
}
|
|
342
|
+
});
|
|
343
|
+
await numericWriteBehind
|
|
344
|
+
.stop()
|
|
345
|
+
.then(({ drained }) => {
|
|
346
|
+
if (drained)
|
|
347
|
+
return;
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
bootstrapLogger.error({ event: "numeric_write_behind.shutdown_not_drained" }, "Numeric write-behind exceeded its bounded shutdown drain");
|
|
350
|
+
})
|
|
351
|
+
.catch((error) => {
|
|
352
|
+
process.exitCode = 1;
|
|
353
|
+
bootstrapLogger.error({
|
|
354
|
+
event: "numeric_write_behind.shutdown_failed",
|
|
355
|
+
error: safeError(error),
|
|
356
|
+
}, "Numeric write-behind shutdown failed");
|
|
357
|
+
});
|
|
316
358
|
await client.destroy();
|
|
317
359
|
await closeDatabase();
|
|
318
360
|
}
|
|
@@ -364,13 +406,26 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
364
406
|
await cloudConnector.stop().catch(() => undefined);
|
|
365
407
|
if (controlServer)
|
|
366
408
|
await controlServer.close().catch(() => undefined);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
409
|
+
await moduleEvents.stop().catch((stopError) => {
|
|
410
|
+
bootstrapLogger.error({
|
|
411
|
+
event: "module_events.cleanup_failed",
|
|
412
|
+
error: safeError(stopError),
|
|
413
|
+
}, "Module event cleanup failed");
|
|
414
|
+
});
|
|
415
|
+
await runtime.stop().catch((stopError) => {
|
|
372
416
|
bootstrapLogger.error({ event: "runtime.cleanup_failed", error: safeError(stopError) }, "Runtime cleanup failed");
|
|
373
|
-
}
|
|
417
|
+
});
|
|
418
|
+
const numericCleanup = await numericWriteBehind
|
|
419
|
+
.stop()
|
|
420
|
+
.catch((stopError) => {
|
|
421
|
+
bootstrapLogger.error({
|
|
422
|
+
event: "numeric_write_behind.cleanup_failed",
|
|
423
|
+
error: safeError(stopError),
|
|
424
|
+
}, "Numeric write-behind cleanup failed");
|
|
425
|
+
return { drained: false };
|
|
426
|
+
});
|
|
427
|
+
if (!numericCleanup.drained)
|
|
428
|
+
bootstrapLogger.error({ event: "numeric_write_behind.cleanup_not_drained" }, "Numeric write-behind cleanup did not drain");
|
|
374
429
|
await client.destroy();
|
|
375
430
|
throw error;
|
|
376
431
|
}
|
|
@@ -407,16 +462,27 @@ function deploymentReference(processEnvironment) {
|
|
|
407
462
|
processEnvironment.GIT_COMMIT_SHA;
|
|
408
463
|
return value?.trim().slice(0, 12) || undefined;
|
|
409
464
|
}
|
|
410
|
-
export function gatewayIntentsForModules(modules) {
|
|
465
|
+
export function gatewayIntentsForModules(modules, messageContentEnabled = false) {
|
|
411
466
|
const intents = [GatewayIntentBits.Guilds];
|
|
412
467
|
if (modules.some((module) => module.events?.messageReactionAdd?.length ||
|
|
413
468
|
module.events?.messageReactionRemove?.length)) {
|
|
414
469
|
intents.push(GatewayIntentBits.GuildMessageReactions);
|
|
415
470
|
}
|
|
416
|
-
|
|
471
|
+
const hasPrivilegedMessageCreate = modules.some((module) => module.events?.privilegedMessageCreate?.length);
|
|
472
|
+
const requiresMessageContent = modules.some((module) => module.requiredDiscordCapabilities?.includes("message-content"));
|
|
473
|
+
if (((hasPrivilegedMessageCreate || requiresMessageContent) &&
|
|
474
|
+
messageContentEnabled) ||
|
|
475
|
+
modules.some((module) => module.events?.guildMessageCreate?.length ||
|
|
476
|
+
module.events?.messageDelete?.length)) {
|
|
417
477
|
intents.push(GatewayIntentBits.GuildMessages);
|
|
418
478
|
}
|
|
419
|
-
if (
|
|
479
|
+
if ((hasPrivilegedMessageCreate || requiresMessageContent) &&
|
|
480
|
+
messageContentEnabled) {
|
|
481
|
+
intents.push(GatewayIntentBits.MessageContent);
|
|
482
|
+
}
|
|
483
|
+
if (modules.some((module) => module.requiredDiscordCapabilities?.includes("guild-members") ||
|
|
484
|
+
module.events?.guildMemberAdd?.length ||
|
|
485
|
+
module.events?.guildMemberRemove?.length)) {
|
|
420
486
|
intents.push(GatewayIntentBits.GuildMembers);
|
|
421
487
|
}
|
|
422
488
|
if (modules.some((module) => module.events?.guildEmojiDelete?.length ||
|