@helyx/bot 1.3.0 → 1.4.0
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 +92 -28
- package/README.md +65 -11
- package/dist/cloud-connector.d.ts +11 -4
- package/dist/cloud-connector.js +52 -15
- package/dist/cloud-control-dispatcher.d.ts +8 -3
- package/dist/cloud-control-dispatcher.js +62 -4
- package/dist/control-errors.d.ts +4 -0
- package/dist/control-errors.js +8 -0
- package/dist/control-server-schemas.d.ts +84 -0
- package/dist/control-server-schemas.js +101 -0
- package/dist/control-server.d.ts +21 -7
- package/dist/control-server.js +88 -132
- package/dist/core-retention.d.ts +26 -0
- package/dist/core-retention.js +49 -0
- package/dist/creator-guild-join-service.d.ts +25 -0
- package/dist/creator-guild-join-service.js +218 -0
- package/dist/creator-invite-service.d.ts +37 -0
- package/dist/creator-invite-service.js +686 -0
- package/dist/creator-onboarding-control.d.ts +12 -0
- package/dist/creator-onboarding-control.js +65 -0
- package/dist/creator-onboarding-platform.d.ts +20 -0
- package/dist/creator-onboarding-platform.js +58 -0
- package/dist/dashboard-action-control.d.ts +2 -2
- package/dist/dashboard-action-control.js +2 -3
- package/dist/database-bootstrap.d.ts +36 -0
- package/dist/database-bootstrap.js +101 -0
- package/dist/developer-operations-module.d.ts +1 -0
- package/dist/developer-operations-module.js +34 -16
- package/dist/discord-owned-message-service.d.ts +1 -0
- package/dist/discord-owned-message-service.js +3 -0
- package/dist/guild-command-read-service.d.ts +8 -3
- package/dist/guild-command-read-service.js +14 -7
- package/dist/guild-command-release.d.ts +15 -8
- package/dist/guild-command-release.js +10 -3
- package/dist/guild-command-worker.d.ts +17 -4
- package/dist/guild-command-worker.js +17 -6
- package/dist/guilds.d.ts +24 -3
- package/dist/guilds.js +185 -64
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/hosted-server-moderation.js +10 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -81
- package/dist/interactions.js +16 -3
- package/dist/module-configuration.d.ts +1 -1
- package/dist/module-configuration.js +5 -2
- package/dist/module-logging.d.ts +8 -3
- package/dist/module-logging.js +13 -7
- package/dist/modules.d.ts +1 -0
- package/dist/modules.js +8 -1
- package/dist/mysql-runtime-persistence.d.ts +46 -0
- package/dist/mysql-runtime-persistence.js +91 -0
- package/dist/oauth-module.js +2 -2
- package/dist/oauth-service.d.ts +7 -0
- package/dist/oauth-service.js +3 -0
- package/dist/scheduled-task-runner.d.ts +9 -85
- package/dist/scheduled-task-runner.js +1 -1
- package/dist/shard-contracts.d.ts +135 -0
- package/dist/shard-contracts.js +13 -1
- package/dist/shard-router.d.ts +2 -1
- package/dist/shard-router.js +7 -0
- package/dist/shard-supervisor.js +43 -9
- package/dist/shard-worker.d.ts +5 -2
- package/dist/shard-worker.js +46 -5
- package/dist/supervisor-cloud-directory.d.ts +9 -3
- package/dist/supervisor-cloud-directory.js +8 -12
- package/dist/trivia-data-provider.d.ts +11 -0
- package/dist/trivia-data-provider.js +46 -26
- package/dist/verification-service.d.ts +2 -2
- package/dist/verification-service.js +6 -3
- package/documentation/README.md +5 -3
- package/documentation/examples/.env.example +15 -11
- package/documentation/guides/getting-started.md +29 -12
- package/documentation/guides/privacy-and-support.md +6 -5
- package/documentation/guides/self-hosting.md +16 -11
- package/documentation/modules/auto-moderation.md +7 -5
- package/documentation/modules/building-a-module.md +48 -2
- package/documentation/modules/moderation.md +5 -5
- package/documentation/modules/publishing-a-module.md +6 -4
- package/documentation/modules/testing-a-module.md +39 -4
- package/documentation/reference/environment.md +43 -9
- package/documentation/runbooks/backup-and-recovery.md +26 -7
- package/documentation/runbooks/installation-and-upgrades.md +97 -27
- package/package.json +20 -16
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CreatorOnboardingRequest, type CreatorOnboardingResponse, type InstallationService, type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
export declare function registerCreatorOnboardingControlRoute(server: FastifyInstance, service: {
|
|
4
|
+
creatorOnboarding?(input: CreatorOnboardingRequest): Promise<CreatorOnboardingResponse>;
|
|
5
|
+
}): void;
|
|
6
|
+
/** Execute the public creator flow behind the bot's existing module guards. */
|
|
7
|
+
export declare function executeCreatorOnboarding(input: CreatorOnboardingRequest, dependencies: {
|
|
8
|
+
modules: ReadonlyMap<string, ModuleDefinition>;
|
|
9
|
+
installations: InstallationService;
|
|
10
|
+
services: ServiceAccess;
|
|
11
|
+
}): Promise<CreatorOnboardingResponse>;
|
|
12
|
+
//# sourceMappingURL=creator-onboarding-control.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { moduleAvailabilityService } from "./module-availability-control.js";
|
|
3
|
+
import { canAccessModule } from "./premium-access.js";
|
|
4
|
+
import { creatorOnboardingRequestSchema } from "@helyx/control-protocol";
|
|
5
|
+
import { BotControlTransportError } from "./control-errors.js";
|
|
6
|
+
export function registerCreatorOnboardingControlRoute(server, service) {
|
|
7
|
+
server.post("/v1/creator-onboarding", async (request) => {
|
|
8
|
+
if (!service.creatorOnboarding)
|
|
9
|
+
throw new BotControlTransportError(503, "Creator onboarding is unavailable");
|
|
10
|
+
return service.creatorOnboarding(creatorOnboardingRequestSchema.parse(request.body));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** Execute the public creator flow behind the bot's existing module guards. */
|
|
14
|
+
export async function executeCreatorOnboarding(input, dependencies) {
|
|
15
|
+
const { modules, installations, services } = dependencies;
|
|
16
|
+
const module = modules.get("helyx.creator-connections");
|
|
17
|
+
if (!module?.creatorOnboarding ||
|
|
18
|
+
(moduleAvailabilityService(services) &&
|
|
19
|
+
!(await moduleAvailabilityService(services).isAvailable(module.manifest.id))) ||
|
|
20
|
+
!(await installations.isModuleEnabled(input.guildId, module.manifest.id)) ||
|
|
21
|
+
!(await canAccessModule(services, input.guildId, module)))
|
|
22
|
+
return { available: false };
|
|
23
|
+
if (input.action === "inspect") {
|
|
24
|
+
const page = await module.creatorOnboarding.execute({ action: "page", guildId: input.guildId, reference: input.reference }, services);
|
|
25
|
+
if (!page.available ||
|
|
26
|
+
page.page?.configurationRevision !== input.configurationRevision ||
|
|
27
|
+
!services.has(HELYX_SERVICE_NAMES.creatorGuildJoin))
|
|
28
|
+
return { available: false };
|
|
29
|
+
const status = await services
|
|
30
|
+
.get(HELYX_SERVICE_NAMES.creatorGuildJoin)
|
|
31
|
+
.inspect({ guildId: input.guildId, memberUserId: input.memberUserId });
|
|
32
|
+
return {
|
|
33
|
+
available: true,
|
|
34
|
+
memberReadiness: status.memberReadiness,
|
|
35
|
+
joinOutcome: ["member_missing", "unavailable"].includes(status.memberReadiness)
|
|
36
|
+
? "uncertain"
|
|
37
|
+
: "already_member",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (input.action !== "join")
|
|
41
|
+
return module.creatorOnboarding.execute(input, services);
|
|
42
|
+
const page = await module.creatorOnboarding.execute({ action: "page", guildId: input.guildId, reference: input.reference }, services);
|
|
43
|
+
if (!page.available ||
|
|
44
|
+
page.page?.configurationRevision !== input.configurationRevision ||
|
|
45
|
+
!services.has(HELYX_SERVICE_NAMES.creatorGuildJoin))
|
|
46
|
+
return { available: false };
|
|
47
|
+
const joined = await services
|
|
48
|
+
.get(HELYX_SERVICE_NAMES.creatorGuildJoin)
|
|
49
|
+
.join({
|
|
50
|
+
guildId: input.guildId,
|
|
51
|
+
memberUserId: input.memberUserId,
|
|
52
|
+
sessionId: input.sessionId,
|
|
53
|
+
authorisationReference: input.authorisationReference,
|
|
54
|
+
});
|
|
55
|
+
let effects = { available: true };
|
|
56
|
+
if (input.withTwitch && ["joined", "already_member"].includes(joined.outcome))
|
|
57
|
+
effects = await module.creatorOnboarding.execute({ ...input, action: "deliver" }, services);
|
|
58
|
+
return {
|
|
59
|
+
...effects,
|
|
60
|
+
available: true,
|
|
61
|
+
joinOutcome: joined.outcome,
|
|
62
|
+
memberReadiness: joined.memberReadiness,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=creator-onboarding-control.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DatabasePool } from "@helyx/database";
|
|
2
|
+
import { type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
|
|
3
|
+
import type { Client } from "discord.js";
|
|
4
|
+
/** Independent of customer-module start/stop so disabled invites can still be revoked. */
|
|
5
|
+
export declare function installCreatorInvitePlatform(input: {
|
|
6
|
+
client: Client;
|
|
7
|
+
database: DatabasePool;
|
|
8
|
+
services: ServiceAccess;
|
|
9
|
+
modules: readonly ModuleDefinition[];
|
|
10
|
+
encryptionKey: string | undefined;
|
|
11
|
+
oauthBroker?: {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
secret: string;
|
|
14
|
+
};
|
|
15
|
+
register(name: string, service: unknown): void;
|
|
16
|
+
warn(event: string): void;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
} | undefined>;
|
|
20
|
+
//# sourceMappingURL=creator-onboarding-platform.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { CreatorInviteRepository } from "@helyx/database";
|
|
2
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
3
|
+
import { DiscordCreatorInviteService } from "./creator-invite-service.js";
|
|
4
|
+
import { DiscordCreatorGuildJoinService } from "./creator-guild-join-service.js";
|
|
5
|
+
/** Independent of customer-module start/stop so disabled invites can still be revoked. */
|
|
6
|
+
export async function installCreatorInvitePlatform(input) {
|
|
7
|
+
const module = input.modules.find((candidate) => candidate.manifest.id === "helyx.creator-connections");
|
|
8
|
+
const repository = new CreatorInviteRepository(input.database);
|
|
9
|
+
if (!module && !(await repository.isInstalled()))
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!input.encryptionKey)
|
|
12
|
+
throw new Error("Hosted Creator Connections invites require HELYX_DATA_ENCRYPTION_KEY");
|
|
13
|
+
const service = new DiscordCreatorInviteService(input.client, repository, input.services, module, input.encryptionKey);
|
|
14
|
+
input.register(HELYX_SERVICE_NAMES.creatorInvites, service);
|
|
15
|
+
if (input.oauthBroker) {
|
|
16
|
+
const broker = input.oauthBroker;
|
|
17
|
+
input.register(HELYX_SERVICE_NAMES.creatorGuildJoin, new DiscordCreatorGuildJoinService(input.client, input.services, input.modules, repository, async (request) => {
|
|
18
|
+
const response = await fetch(new URL("/v1/internal/oauth/onboarding/credential", broker.baseUrl), {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
authorization: `Bearer ${broker.secret}`,
|
|
22
|
+
"content-type": "application/json",
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(request),
|
|
25
|
+
signal: AbortSignal.timeout(10_000),
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok)
|
|
28
|
+
throw new Error("creator_join_authorisation_unavailable");
|
|
29
|
+
const result = (await response.json());
|
|
30
|
+
if (typeof result.accessToken !== "string" ||
|
|
31
|
+
typeof result.applicationId !== "string")
|
|
32
|
+
throw new Error("creator_join_authorisation_invalid");
|
|
33
|
+
return {
|
|
34
|
+
accessToken: result.accessToken,
|
|
35
|
+
applicationId: result.applicationId,
|
|
36
|
+
};
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
let pending;
|
|
40
|
+
const timer = setInterval(() => {
|
|
41
|
+
if (pending || !input.client.isReady())
|
|
42
|
+
return;
|
|
43
|
+
pending = service
|
|
44
|
+
.cleanupUnavailableInvites()
|
|
45
|
+
.catch(() => input.warn("creator_invites.cleanup_delayed"))
|
|
46
|
+
.finally(() => {
|
|
47
|
+
pending = undefined;
|
|
48
|
+
});
|
|
49
|
+
}, 30_000);
|
|
50
|
+
timer.unref();
|
|
51
|
+
return {
|
|
52
|
+
async stop() {
|
|
53
|
+
clearInterval(timer);
|
|
54
|
+
await pending;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=creator-onboarding-platform.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { InstallationRepository } from "@helyx/database";
|
|
2
2
|
import { type DashboardActionContext, type DashboardActionInspection, type DashboardActionResult, type ModuleDefinition, type PermissionActor, type ServiceAccess } from "@helyx/sdk";
|
|
3
3
|
export interface DashboardActionInput {
|
|
4
4
|
guildId: string;
|
|
@@ -10,7 +10,7 @@ export interface DashboardActionInput {
|
|
|
10
10
|
}
|
|
11
11
|
export type DashboardActionPreviewInput = Omit<DashboardActionInput, "inspectionRevision">;
|
|
12
12
|
export interface DashboardActionControl {
|
|
13
|
-
|
|
13
|
+
installations: Pick<InstallationRepository, "isModuleEnabled">;
|
|
14
14
|
services: ServiceAccess;
|
|
15
15
|
requireDashboardModule(moduleId: string): ModuleDefinition;
|
|
16
16
|
requireModuleAccessible(guildId: string, moduleId: string): Promise<void>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { InstallationRepository } from "@helyx/database";
|
|
2
1
|
import {} from "@helyx/sdk";
|
|
3
2
|
import { dashboardActionPermissionId } from "./control-permissions.js";
|
|
4
3
|
import { requireDashboardFeatures } from "./dashboard-feature-control.js";
|
|
@@ -10,7 +9,7 @@ function prepareAction(input, control) {
|
|
|
10
9
|
}
|
|
11
10
|
async function requireEnabledAndAuthorized(input, control, module, permissionId, disabledMessage) {
|
|
12
11
|
const [enabled] = await Promise.all([
|
|
13
|
-
|
|
12
|
+
control.installations.isModuleEnabled(input.guildId, input.moduleId),
|
|
14
13
|
control.authorizeAction(input.guildId, module, input.actorUserId, permissionId),
|
|
15
14
|
]);
|
|
16
15
|
if (!enabled)
|
|
@@ -25,7 +24,7 @@ function actionContext(input, control, module, permissionId) {
|
|
|
25
24
|
revalidate: async () => {
|
|
26
25
|
try {
|
|
27
26
|
await control.requireModuleAccessible(input.guildId, input.moduleId);
|
|
28
|
-
const enabled = await
|
|
27
|
+
const enabled = await control.installations.isModuleEnabled(input.guildId, input.moduleId);
|
|
29
28
|
if (!enabled)
|
|
30
29
|
return false;
|
|
31
30
|
await control.authorizeAction(input.guildId, module, input.actorUserId, permissionId);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BotEnvironment } from "@helyx/config";
|
|
2
|
+
import { resolveDatabaseAdapter } from "@helyx/database/adapters";
|
|
3
|
+
import type { DatabaseAdapter, DatabaseSession } from "@helyx/database-contracts";
|
|
4
|
+
import { type ConfiguredModulePackage } from "./modules.js";
|
|
5
|
+
type MigrationResult = Awaited<ReturnType<DatabaseSession<unknown>["migrations"]["runCore"]>>;
|
|
6
|
+
type MigrationInspection = Awaited<ReturnType<DatabaseSession<unknown>["migrations"]["inspectCore"]>>;
|
|
7
|
+
export interface BotDatabaseStartupResult {
|
|
8
|
+
readonly core: MigrationResult;
|
|
9
|
+
readonly modules: readonly {
|
|
10
|
+
moduleId: string;
|
|
11
|
+
applied: readonly string[];
|
|
12
|
+
verified: readonly string[];
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export interface BotDatabaseHandle<Repositories = Record<string, never>> {
|
|
16
|
+
readonly session: DatabaseSession<Repositories>;
|
|
17
|
+
readonly migrations: BotDatabaseStartupResult;
|
|
18
|
+
/** A read-only adapter query that also verifies the core migration ledger. */
|
|
19
|
+
healthCheck(): Promise<MigrationInspection>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface BotDatabaseBootstrapInput {
|
|
23
|
+
readonly environment: Pick<BotEnvironment, "DATABASE_ENGINE" | "DATABASE_URL" | "DATABASE_SSL" | "HELYX_RUNTIME_PROFILE">;
|
|
24
|
+
readonly modulePackages: readonly ConfiguredModulePackage[];
|
|
25
|
+
readonly applicationName?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Engine-neutral database startup foundation. Bot repositories and platform
|
|
29
|
+
* services still need an engine-aware runtime before this can replace the
|
|
30
|
+
* PostgreSQL-only startup path.
|
|
31
|
+
*/
|
|
32
|
+
export declare function openBotDatabase(input: BotDatabaseBootstrapInput, resolveAdapter?: typeof resolveDatabaseAdapter): Promise<BotDatabaseHandle>;
|
|
33
|
+
/** Connect with an explicit repository-bearing adapter and the same startup checks. */
|
|
34
|
+
export declare function openBotDatabaseWithAdapter<Repositories>(input: BotDatabaseBootstrapInput, adapter: DatabaseAdapter<Repositories>): Promise<BotDatabaseHandle<Repositories>>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=database-bootstrap.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { resolveDatabaseAdapter } from "@helyx/database/adapters";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { assertConfiguredModuleDatabaseCompatibility, } from "./modules.js";
|
|
5
|
+
/**
|
|
6
|
+
* Engine-neutral database startup foundation. Bot repositories and platform
|
|
7
|
+
* services still need an engine-aware runtime before this can replace the
|
|
8
|
+
* PostgreSQL-only startup path.
|
|
9
|
+
*/
|
|
10
|
+
export async function openBotDatabase(input, resolveAdapter = resolveDatabaseAdapter) {
|
|
11
|
+
const prepared = await prepareBotDatabase(input);
|
|
12
|
+
const adapter = await resolveAdapter(prepared.engine);
|
|
13
|
+
return connectPreparedBotDatabase(input, prepared, adapter);
|
|
14
|
+
}
|
|
15
|
+
/** Connect with an explicit repository-bearing adapter and the same startup checks. */
|
|
16
|
+
export async function openBotDatabaseWithAdapter(input, adapter) {
|
|
17
|
+
const prepared = await prepareBotDatabase(input);
|
|
18
|
+
return connectPreparedBotDatabase(input, prepared, adapter);
|
|
19
|
+
}
|
|
20
|
+
async function prepareBotDatabase(input) {
|
|
21
|
+
const { environment, modulePackages } = input;
|
|
22
|
+
const engine = environment.DATABASE_ENGINE;
|
|
23
|
+
if (engine !== "postgresql" && engine !== "mysql") {
|
|
24
|
+
throw new Error(`Unsupported bot DATABASE_ENGINE=${String(engine)}`);
|
|
25
|
+
}
|
|
26
|
+
if (engine === "mysql" &&
|
|
27
|
+
environment.HELYX_RUNTIME_PROFILE !== "self-hosted") {
|
|
28
|
+
throw new Error("Bot MySQL database startup requires HELYX_RUNTIME_PROFILE=self-hosted");
|
|
29
|
+
}
|
|
30
|
+
assertConfiguredModuleDatabaseCompatibility(modulePackages, engine);
|
|
31
|
+
const migrationSources = await Promise.all(modulePackages.map(async ({ definition, packageRoot }) => {
|
|
32
|
+
const manifest = definition.manifest;
|
|
33
|
+
const migrationsDirectory = join(packageRoot, engine === "mysql" ? "migrations-mysql" : "migrations");
|
|
34
|
+
if (engine === "mysql") {
|
|
35
|
+
const directory = await stat(migrationsDirectory).catch((error) => {
|
|
36
|
+
if (error.code === "ENOENT")
|
|
37
|
+
return undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
const postgresDirectory = await stat(join(packageRoot, "migrations")).catch((error) => {
|
|
41
|
+
if (error.code === "ENOENT")
|
|
42
|
+
return undefined;
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
if ((!directory || !directory.isDirectory()) &&
|
|
46
|
+
postgresDirectory?.isDirectory()) {
|
|
47
|
+
throw new Error(`Module ${manifest.id} is missing its MySQL migration directory: ${migrationsDirectory}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
moduleId: manifest.id,
|
|
52
|
+
moduleVersion: manifest.version,
|
|
53
|
+
migrationsDirectory,
|
|
54
|
+
};
|
|
55
|
+
}));
|
|
56
|
+
return { engine, migrationSources };
|
|
57
|
+
}
|
|
58
|
+
async function connectPreparedBotDatabase(input, prepared, adapter) {
|
|
59
|
+
const { environment } = input;
|
|
60
|
+
const { engine, migrationSources } = prepared;
|
|
61
|
+
if (adapter.engine !== engine) {
|
|
62
|
+
throw new Error(`Database adapter mismatch: requested ${engine}, received ${adapter.engine}`);
|
|
63
|
+
}
|
|
64
|
+
const session = await adapter.connect({
|
|
65
|
+
connectionString: environment.DATABASE_URL,
|
|
66
|
+
sslMode: environment.DATABASE_SSL,
|
|
67
|
+
applicationName: input.applicationName ?? "helyx-bot",
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
if (session.identity.engine !== engine) {
|
|
71
|
+
throw new Error(`Database identity mismatch: requested ${engine}, received ${session.identity.engine}`);
|
|
72
|
+
}
|
|
73
|
+
const core = await session.migrations.runCore();
|
|
74
|
+
const modules = [];
|
|
75
|
+
for (const source of migrationSources) {
|
|
76
|
+
const result = await session.migrations.runModule(source);
|
|
77
|
+
modules.push({ moduleId: source.moduleId, ...result });
|
|
78
|
+
}
|
|
79
|
+
let closed = false;
|
|
80
|
+
return {
|
|
81
|
+
session,
|
|
82
|
+
migrations: { core, modules },
|
|
83
|
+
healthCheck() {
|
|
84
|
+
if (closed)
|
|
85
|
+
return Promise.reject(new Error("Bot database is closed"));
|
|
86
|
+
return session.migrations.inspectCore();
|
|
87
|
+
},
|
|
88
|
+
async close() {
|
|
89
|
+
if (closed)
|
|
90
|
+
return;
|
|
91
|
+
closed = true;
|
|
92
|
+
await session.close();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
await session.close().catch(() => undefined);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=database-bootstrap.js.map
|
|
@@ -15,6 +15,7 @@ export interface DeveloperOperationsInput {
|
|
|
15
15
|
approvedRoleIds: readonly string[];
|
|
16
16
|
botVersion: string;
|
|
17
17
|
deploymentReference?: string;
|
|
18
|
+
databaseEngine: "postgresql" | "mysql";
|
|
18
19
|
coreFunctions: readonly DeveloperModuleSummary[];
|
|
19
20
|
modules: readonly DeveloperModuleSummary[];
|
|
20
21
|
processLabel: string;
|
|
@@ -5,7 +5,6 @@ import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
|
5
5
|
const DEVELOPER_MODULE_ID = "helyx.developer-operations";
|
|
6
6
|
const DEVELOPER_MODULE_NAME = "Helyx Developer Operations";
|
|
7
7
|
const DEVELOPER_MODULE_VERSION = "0.3.0";
|
|
8
|
-
const MAX_MODULE_LINES = 8;
|
|
9
8
|
export function createDeveloperOperationsModule(input) {
|
|
10
9
|
const approvedRoles = new Set(input.approvedRoleIds);
|
|
11
10
|
return defineModule({
|
|
@@ -27,6 +26,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
27
26
|
commands: Object.fromEntries([
|
|
28
27
|
"dev.shards",
|
|
29
28
|
"dev.bot",
|
|
29
|
+
"dev.modules",
|
|
30
30
|
"dev.proc",
|
|
31
31
|
"dev.trivia-data",
|
|
32
32
|
"dev.leave",
|
|
@@ -96,7 +96,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
96
96
|
{
|
|
97
97
|
name: "bot",
|
|
98
98
|
permissionId: "dev.bot",
|
|
99
|
-
description: "Show the bot build and
|
|
99
|
+
description: "Show the bot build and core function versions",
|
|
100
100
|
acknowledgement: "immediate-response",
|
|
101
101
|
authorization: "self-authorized",
|
|
102
102
|
async execute(context) {
|
|
@@ -110,13 +110,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
110
110
|
},
|
|
111
111
|
...input.coreFunctions,
|
|
112
112
|
].sort((left, right) => left.id.localeCompare(right.id));
|
|
113
|
-
const
|
|
114
|
-
const visibleCoreFunctions = coreFunctions.slice(0, MAX_MODULE_LINES);
|
|
115
|
-
const visibleModules = modules.slice(0, Math.max(0, MAX_MODULE_LINES - visibleCoreFunctions.length));
|
|
116
|
-
const coreFunctionLines = visibleCoreFunctions.map((coreFunction) => `- ${safeInlineCode(coreFunction.name)} · ${safeInlineCode(coreFunction.id)} · ${safeInlineCode(coreFunction.version)}`);
|
|
117
|
-
const moduleLines = visibleModules.map((module) => `- ${safeInlineCode(module.name)} · ${safeInlineCode(module.id)} · ${safeInlineCode(module.version)}`);
|
|
118
|
-
const componentCount = coreFunctions.length + modules.length;
|
|
119
|
-
const visibleComponentCount = visibleCoreFunctions.length + visibleModules.length;
|
|
113
|
+
const coreFunctionLines = coreFunctions.map((coreFunction) => `- ${safeInlineCode(coreFunction.name)} · ${safeInlineCode(coreFunction.version)}`);
|
|
120
114
|
await context.reply({
|
|
121
115
|
ephemeral: true,
|
|
122
116
|
componentsV2: {
|
|
@@ -127,19 +121,43 @@ export function createDeveloperOperationsModule(input) {
|
|
|
127
121
|
"## Bot information",
|
|
128
122
|
`Version: ${safeInlineCode(input.botVersion)}`,
|
|
129
123
|
`Build: ${safeInlineCode(input.deploymentReference ?? "not supplied")}`,
|
|
124
|
+
`Database: ${safeInlineCode(input.databaseEngine === "mysql" ? "MySQL" : "PostgreSQL")}`,
|
|
130
125
|
`Runtime: ${input.processLabel}`,
|
|
131
126
|
`Core functions: ${coreFunctions.length}`,
|
|
132
|
-
`Installed modules: ${modules.length}`,
|
|
127
|
+
`Installed modules: ${input.modules.length} — see /dev modules`,
|
|
133
128
|
"",
|
|
134
129
|
"### Core functions",
|
|
135
130
|
...coreFunctionLines,
|
|
131
|
+
].join("\n"),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "modules",
|
|
140
|
+
permissionId: "dev.modules",
|
|
141
|
+
description: "List all installed feature modules and their versions",
|
|
142
|
+
acknowledgement: "immediate-response",
|
|
143
|
+
authorization: "self-authorized",
|
|
144
|
+
async execute(context) {
|
|
145
|
+
if (!(await authorizeDeveloperCommand(context)))
|
|
146
|
+
return;
|
|
147
|
+
const modules = [...input.modules].sort((left, right) => left.id.localeCompare(right.id));
|
|
148
|
+
await context.reply({
|
|
149
|
+
ephemeral: true,
|
|
150
|
+
componentsV2: {
|
|
151
|
+
accentColor: 0x2563eb,
|
|
152
|
+
text: [
|
|
153
|
+
{
|
|
154
|
+
content: [
|
|
155
|
+
"## Installed modules",
|
|
156
|
+
`Total: ${modules.length}`,
|
|
136
157
|
"",
|
|
137
|
-
|
|
138
|
-
...
|
|
139
|
-
|
|
140
|
-
? [
|
|
141
|
-
`… showing ${visibleComponentCount} of ${componentCount} components`,
|
|
142
|
-
]
|
|
158
|
+
...modules.map((module) => `- ${safeInlineCode(module.name)} · ${safeInlineCode(module.version)}`),
|
|
159
|
+
...(modules.length === 0
|
|
160
|
+
? ["No feature modules installed."]
|
|
143
161
|
: []),
|
|
144
162
|
].join("\n"),
|
|
145
163
|
},
|
|
@@ -6,6 +6,9 @@ export class DiscordOwnedMessageDeletionService {
|
|
|
6
6
|
constructor(client) {
|
|
7
7
|
this.client = client;
|
|
8
8
|
}
|
|
9
|
+
isReady() {
|
|
10
|
+
return this.client.isReady();
|
|
11
|
+
}
|
|
9
12
|
async addOwnedMessageReaction(input) {
|
|
10
13
|
const emoji = prepareEmojiIdentifier(input.emojiIdentifier);
|
|
11
14
|
const message = await this.#fetchOwnedMessage(input);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type GuildCommandBudgetClass, type GuildCommandHealth, type GuildCommandHistoryRecord, type GuildCommandRequestRecord, type GuildCommandRequestSource, type GuildCommandStateRecord, type GuildCommandVisibilityProfile } from "@helyx/database";
|
|
1
|
+
import { GuildCommandRepository, InstallationRepository, type GuildCommandBudgetClass, type GuildCommandHealth, type GuildCommandHistoryRecord, type GuildCommandRequestRecord, type GuildCommandRequestSource, type GuildCommandStateRecord, type GuildCommandVisibilityProfile } from "@helyx/database";
|
|
2
|
+
import { type GuildCommandRepository as MysqlGuildCommandRepository, type InstallationRepository as MysqlInstallationRepository } from "@helyx/database-mysql";
|
|
2
3
|
import type { ModuleDefinition } from "@helyx/sdk";
|
|
3
4
|
import type { Pool } from "pg";
|
|
4
5
|
import type { GuildCommandWorkerHealth } from "./guild-command-worker.js";
|
|
@@ -78,9 +79,13 @@ export interface GuildCommandReadHandler {
|
|
|
78
79
|
scheduledGuildCount: number;
|
|
79
80
|
}>;
|
|
80
81
|
}
|
|
82
|
+
type ReadCommandMethod = "getState" | "getActiveRequest" | "getCapacity" | "ensureState" | "schedule" | "recordStatusRefresh" | "changeVisibilityProfile" | "setMigrationHold" | "listHistory" | "getHealth";
|
|
83
|
+
export interface GuildCommandReadRepositories {
|
|
84
|
+
commands: Pick<GuildCommandRepository, ReadCommandMethod> & Pick<MysqlGuildCommandRepository, ReadCommandMethod>;
|
|
85
|
+
installations: Pick<InstallationRepository, "listActiveGuildIdsWithEnabledModule"> & Pick<MysqlInstallationRepository, "listActiveGuildIdsWithEnabledModule">;
|
|
86
|
+
}
|
|
81
87
|
export declare class GuildCommandReadService implements GuildCommandReadHandler {
|
|
82
88
|
#private;
|
|
83
|
-
private readonly database;
|
|
84
89
|
private readonly applicationId;
|
|
85
90
|
private readonly workerHealth?;
|
|
86
91
|
private readonly discordToken?;
|
|
@@ -88,7 +93,7 @@ export declare class GuildCommandReadService implements GuildCommandReadHandler
|
|
|
88
93
|
private readonly writeGuildIds;
|
|
89
94
|
private readonly writeAllGuilds;
|
|
90
95
|
private readonly modules;
|
|
91
|
-
constructor(
|
|
96
|
+
constructor(databaseOrRepositories: Pool | GuildCommandReadRepositories, applicationId: string, workerHealth?: (() => Promise<GuildCommandWorkerHealth> | undefined) | undefined, discordToken?: string | undefined, guildCommandWritesEnabled?: boolean, writeGuildIds?: readonly string[], writeAllGuilds?: boolean, modules?: readonly ModuleDefinition[]);
|
|
92
97
|
previewFirstTimeSetup(moduleIds: string[]): Promise<{
|
|
93
98
|
selectedModuleIds: string[];
|
|
94
99
|
rootCount: number;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { GuildCommandRepository, GuildCommandRevisionConflictError, InstallationRepository, } from "@helyx/database";
|
|
2
|
+
import { GuildCommandRevisionConflictError as MysqlGuildCommandRevisionConflictError, } from "@helyx/database-mysql";
|
|
2
3
|
import { isCustomerSelectableModule } from "@helyx/module-manifest";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
import { REST, Routes } from "discord.js";
|
|
5
6
|
import { guildCommandDefinitionHash, projectGuildCommands, } from "./guild-command-projection.js";
|
|
6
7
|
export class GuildCommandReadService {
|
|
7
|
-
database;
|
|
8
8
|
applicationId;
|
|
9
9
|
workerHealth;
|
|
10
10
|
discordToken;
|
|
@@ -13,8 +13,8 @@ export class GuildCommandReadService {
|
|
|
13
13
|
writeAllGuilds;
|
|
14
14
|
modules;
|
|
15
15
|
#repository;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
#installations;
|
|
17
|
+
constructor(databaseOrRepositories, applicationId, workerHealth, discordToken, guildCommandWritesEnabled = false, writeGuildIds = [], writeAllGuilds = false, modules = []) {
|
|
18
18
|
this.applicationId = applicationId;
|
|
19
19
|
this.workerHealth = workerHealth;
|
|
20
20
|
this.discordToken = discordToken;
|
|
@@ -22,7 +22,14 @@ export class GuildCommandReadService {
|
|
|
22
22
|
this.writeGuildIds = writeGuildIds;
|
|
23
23
|
this.writeAllGuilds = writeAllGuilds;
|
|
24
24
|
this.modules = modules;
|
|
25
|
-
|
|
25
|
+
const repositories = "commands" in databaseOrRepositories
|
|
26
|
+
? databaseOrRepositories
|
|
27
|
+
: {
|
|
28
|
+
commands: new GuildCommandRepository(databaseOrRepositories),
|
|
29
|
+
installations: new InstallationRepository(databaseOrRepositories),
|
|
30
|
+
};
|
|
31
|
+
this.#repository = repositories.commands;
|
|
32
|
+
this.#installations = repositories.installations;
|
|
26
33
|
}
|
|
27
34
|
previewFirstTimeSetup(moduleIds) {
|
|
28
35
|
const requested = new Set(moduleIds);
|
|
@@ -97,11 +104,10 @@ export class GuildCommandReadService {
|
|
|
97
104
|
return this.getStatus(input.guildId);
|
|
98
105
|
}
|
|
99
106
|
async reconcileModuleAvailability(input) {
|
|
100
|
-
const installations = new InstallationRepository(this.database);
|
|
101
107
|
let cursor;
|
|
102
108
|
let scheduledGuildCount = 0;
|
|
103
109
|
do {
|
|
104
|
-
const page = await installations.listActiveGuildIdsWithEnabledModule({
|
|
110
|
+
const page = await this.#installations.listActiveGuildIdsWithEnabledModule({
|
|
105
111
|
moduleId: input.moduleId,
|
|
106
112
|
...(cursor ? { cursor } : {}),
|
|
107
113
|
limit: 500,
|
|
@@ -152,7 +158,8 @@ export class GuildCommandReadService {
|
|
|
152
158
|
});
|
|
153
159
|
}
|
|
154
160
|
catch (error) {
|
|
155
|
-
if (!(error instanceof GuildCommandRevisionConflictError)
|
|
161
|
+
if (!(error instanceof GuildCommandRevisionConflictError) &&
|
|
162
|
+
!(error instanceof MysqlGuildCommandRevisionConflictError))
|
|
156
163
|
throw error;
|
|
157
164
|
}
|
|
158
165
|
return this.getStatus(input.guildId);
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { GuildCommandRepository, InstallationRepository, RuntimeStateRepository, type DatabasePool } from "@helyx/database";
|
|
2
|
-
type
|
|
3
|
-
type
|
|
4
|
-
type
|
|
5
|
-
|
|
2
|
+
import type { GuildCommandRepository as MysqlGuildCommandRepository, InstallationRepository as MysqlInstallationRepository, RuntimeStateRepository as MysqlRuntimeStateRepository } from "@helyx/database-mysql";
|
|
3
|
+
type ReleaseInstallationRepository = Pick<InstallationRepository, "listActiveGuilds"> & Pick<MysqlInstallationRepository, "listActiveGuilds">;
|
|
4
|
+
type ReleaseCommandRepository = Pick<GuildCommandRepository, "getState" | "schedule"> & Pick<MysqlGuildCommandRepository, "getState" | "schedule">;
|
|
5
|
+
type ReleaseRuntimeRepository = Pick<RuntimeStateRepository, "get" | "compareAndSet"> & Pick<MysqlRuntimeStateRepository, "get" | "compareAndSet">;
|
|
6
|
+
type ReleaseRepositories = {
|
|
7
|
+
installations: ReleaseInstallationRepository;
|
|
8
|
+
commands: ReleaseCommandRepository;
|
|
9
|
+
runtime: ReleaseRuntimeRepository;
|
|
10
|
+
};
|
|
11
|
+
type ReleasePersistence = ({
|
|
6
12
|
database: DatabasePool;
|
|
13
|
+
} & Partial<ReleaseRepositories>) | ({
|
|
14
|
+
database?: never;
|
|
15
|
+
} & ReleaseRepositories);
|
|
16
|
+
export declare function scheduleGuildCommandRelease(input: {
|
|
7
17
|
applicationId: string;
|
|
8
18
|
releaseId: string;
|
|
9
19
|
staggerMs?: number;
|
|
10
|
-
|
|
11
|
-
commands?: ReleaseCommandRepository;
|
|
12
|
-
runtime?: ReleaseRuntimeRepository;
|
|
13
|
-
}): Promise<{
|
|
20
|
+
} & ReleasePersistence): Promise<{
|
|
14
21
|
scheduled: number;
|
|
15
22
|
skippedIncomplete: number;
|
|
16
23
|
replay: boolean;
|
|
@@ -6,9 +6,16 @@ export async function scheduleGuildCommandRelease(input) {
|
|
|
6
6
|
if (!RELEASE_ID.test(input.releaseId))
|
|
7
7
|
throw new Error("Guild command release ID is invalid");
|
|
8
8
|
const markerKey = `guild-commands:release:${input.applicationId}:${input.releaseId}`;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const database = input.database;
|
|
10
|
+
const installations = input.installations ??
|
|
11
|
+
(database ? new InstallationRepository(database) : undefined);
|
|
12
|
+
const commands = input.commands ??
|
|
13
|
+
(database ? new GuildCommandRepository(database) : undefined);
|
|
14
|
+
const runtime = input.runtime ??
|
|
15
|
+
(database ? new RuntimeStateRepository(database) : undefined);
|
|
16
|
+
if (!installations || !commands || !runtime) {
|
|
17
|
+
throw new Error("Guild command release requires a database or all three repositories");
|
|
18
|
+
}
|
|
12
19
|
if (await runtime.get(markerKey))
|
|
13
20
|
return { scheduled: 0, skippedIncomplete: 0, replay: true };
|
|
14
21
|
const guilds = await installations.listActiveGuilds();
|