@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,218 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { DiscordAPIError, REST, RESTJSONErrorCodes, Routes, } from "discord.js";
|
|
3
|
+
import { discordRestOptions } from "./discord-rest.js";
|
|
4
|
+
import { canAccessModule } from "./premium-access.js";
|
|
5
|
+
const snowflake = /^\d{17,20}$/u;
|
|
6
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
7
|
+
/** The only onboarding boundary which can add a member. Never supplies roles or other admission overrides. */
|
|
8
|
+
export class DiscordCreatorGuildJoinService {
|
|
9
|
+
client;
|
|
10
|
+
services;
|
|
11
|
+
modules;
|
|
12
|
+
repository;
|
|
13
|
+
credentials;
|
|
14
|
+
#rest;
|
|
15
|
+
constructor(client, services, modules, repository, credentials) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.services = services;
|
|
18
|
+
this.modules = modules;
|
|
19
|
+
this.repository = repository;
|
|
20
|
+
this.credentials = credentials;
|
|
21
|
+
}
|
|
22
|
+
async inspect(input) {
|
|
23
|
+
if (!snowflake.test(input.guildId) ||
|
|
24
|
+
(input.memberUserId !== undefined && !snowflake.test(input.memberUserId)))
|
|
25
|
+
return { joining: "unavailable", memberReadiness: "unavailable" };
|
|
26
|
+
const guild = this.client.isReady()
|
|
27
|
+
? this.client.guilds.cache.get(input.guildId)
|
|
28
|
+
: undefined;
|
|
29
|
+
if (!guild?.available)
|
|
30
|
+
return { joining: "unavailable", memberReadiness: "unavailable" };
|
|
31
|
+
try {
|
|
32
|
+
if (!(await this.enabled(guild.id)))
|
|
33
|
+
return { joining: "unavailable", memberReadiness: "unavailable" };
|
|
34
|
+
await guild.fetch();
|
|
35
|
+
const paused = guild.features.includes("INVITES_DISABLED") ||
|
|
36
|
+
(guild.incidentsData?.invitesDisabledUntil?.getTime() ?? 0) >
|
|
37
|
+
Date.now();
|
|
38
|
+
return {
|
|
39
|
+
joining: paused ? "paused" : "open",
|
|
40
|
+
memberReadiness: input.memberUserId
|
|
41
|
+
? await this.readiness(guild, input.memberUserId)
|
|
42
|
+
: "member_missing",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { joining: "unavailable", memberReadiness: "unavailable" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async join(input) {
|
|
50
|
+
if (!snowflake.test(input.guildId) ||
|
|
51
|
+
!snowflake.test(input.memberUserId) ||
|
|
52
|
+
!uuid.test(input.sessionId) ||
|
|
53
|
+
!/^[A-Za-z0-9_-]{43}$/u.test(input.authorisationReference))
|
|
54
|
+
return {
|
|
55
|
+
outcome: "rejected",
|
|
56
|
+
memberReadiness: "unavailable",
|
|
57
|
+
safeCode: "invalid_authorisation",
|
|
58
|
+
};
|
|
59
|
+
return this.repository.runGuildExclusive(input.guildId, async () => {
|
|
60
|
+
const before = await this.inspect(input);
|
|
61
|
+
if (before.joining !== "open")
|
|
62
|
+
return {
|
|
63
|
+
outcome: "rejected",
|
|
64
|
+
memberReadiness: before.memberReadiness,
|
|
65
|
+
safeCode: "joining_unavailable",
|
|
66
|
+
};
|
|
67
|
+
if (before.memberReadiness !== "member_missing" &&
|
|
68
|
+
before.memberReadiness !== "unavailable")
|
|
69
|
+
return {
|
|
70
|
+
outcome: "already_member",
|
|
71
|
+
memberReadiness: before.memberReadiness,
|
|
72
|
+
};
|
|
73
|
+
if (before.memberReadiness === "unavailable")
|
|
74
|
+
return { outcome: "uncertain", memberReadiness: "unavailable" };
|
|
75
|
+
let credential;
|
|
76
|
+
try {
|
|
77
|
+
credential = await this.credentials(input);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// No Discord write has happened, so this is a definite no-join result.
|
|
81
|
+
return {
|
|
82
|
+
outcome: "rejected",
|
|
83
|
+
memberReadiness: "member_missing",
|
|
84
|
+
safeCode: "authorisation_unavailable",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!credential.accessToken ||
|
|
88
|
+
credential.applicationId !== this.client.user?.id ||
|
|
89
|
+
!this.client.token)
|
|
90
|
+
return {
|
|
91
|
+
outcome: "rejected",
|
|
92
|
+
memberReadiness: "member_missing",
|
|
93
|
+
safeCode: "application_mismatch",
|
|
94
|
+
};
|
|
95
|
+
// The broker call crosses a network boundary. Check admission again just
|
|
96
|
+
// before the only write; never replay this PUT after an uncertain result.
|
|
97
|
+
const current = await this.inspect(input);
|
|
98
|
+
if (current.joining !== "open")
|
|
99
|
+
return {
|
|
100
|
+
outcome: "rejected",
|
|
101
|
+
memberReadiness: current.memberReadiness,
|
|
102
|
+
safeCode: "joining_unavailable",
|
|
103
|
+
};
|
|
104
|
+
if (current.memberReadiness !== "member_missing")
|
|
105
|
+
return current.memberReadiness === "unavailable"
|
|
106
|
+
? { outcome: "uncertain", memberReadiness: "unavailable" }
|
|
107
|
+
: {
|
|
108
|
+
outcome: "already_member",
|
|
109
|
+
memberReadiness: current.memberReadiness,
|
|
110
|
+
};
|
|
111
|
+
this.#rest ??= new REST({
|
|
112
|
+
...discordRestOptions,
|
|
113
|
+
retries: 0,
|
|
114
|
+
timeout: 15_000,
|
|
115
|
+
}).setToken(this.client.token);
|
|
116
|
+
try {
|
|
117
|
+
await this.#rest.put(Routes.guildMember(input.guildId, input.memberUserId), { body: { access_token: credential.accessToken } });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const after = await this.inspect(input);
|
|
121
|
+
if (!["member_missing", "unavailable"].includes(after.memberReadiness)) {
|
|
122
|
+
await this.logJoined(input);
|
|
123
|
+
return { outcome: "joined", memberReadiness: after.memberReadiness };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
outcome: error instanceof DiscordAPIError &&
|
|
127
|
+
error.status >= 400 &&
|
|
128
|
+
error.status < 500 &&
|
|
129
|
+
error.status !== 429
|
|
130
|
+
? "rejected"
|
|
131
|
+
: "uncertain",
|
|
132
|
+
memberReadiness: after.memberReadiness,
|
|
133
|
+
safeCode: "join_not_confirmed",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const after = await this.inspect(input);
|
|
137
|
+
if (!["member_missing", "unavailable"].includes(after.memberReadiness))
|
|
138
|
+
await this.logJoined(input);
|
|
139
|
+
return {
|
|
140
|
+
outcome: ["member_missing", "unavailable"].includes(after.memberReadiness)
|
|
141
|
+
? "uncertain"
|
|
142
|
+
: "joined",
|
|
143
|
+
memberReadiness: after.memberReadiness,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async logJoined(input) {
|
|
148
|
+
// Joining is core-owned; emit the module-declared event only after a new
|
|
149
|
+
// admission is confirmed. Logging must never change the admission outcome.
|
|
150
|
+
try {
|
|
151
|
+
if (!this.services.has(HELYX_SERVICE_NAMES.moduleLogging))
|
|
152
|
+
return;
|
|
153
|
+
await this.services
|
|
154
|
+
.get(HELYX_SERVICE_NAMES.moduleLogging)
|
|
155
|
+
.emit({
|
|
156
|
+
guildId: input.guildId,
|
|
157
|
+
moduleId: "helyx.creator-connections",
|
|
158
|
+
eventId: "integrated-invite-joined",
|
|
159
|
+
summary: "Member joined through integrated invite",
|
|
160
|
+
details: [{ label: "Member", value: input.memberUserId }],
|
|
161
|
+
idempotencyKey: `cc-integrated-join:${input.sessionId}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Optional log delivery cannot turn a successful join into an error.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async enabled(guildId) {
|
|
169
|
+
const module = this.modules.find((candidate) => candidate.manifest.id === "helyx.creator-connections");
|
|
170
|
+
return Boolean(module &&
|
|
171
|
+
(await this.services
|
|
172
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
173
|
+
.isModuleEnabled(guildId, module.manifest.id)) &&
|
|
174
|
+
(await this.services
|
|
175
|
+
.get(HELYX_SERVICE_NAMES.moduleAvailability)
|
|
176
|
+
.isAvailable(module.manifest.id)) &&
|
|
177
|
+
(await canAccessModule(this.services, guildId, module)));
|
|
178
|
+
}
|
|
179
|
+
async readiness(guild, memberUserId) {
|
|
180
|
+
try {
|
|
181
|
+
const member = await guild.members.fetch({
|
|
182
|
+
user: memberUserId,
|
|
183
|
+
force: true,
|
|
184
|
+
});
|
|
185
|
+
if (member.pending)
|
|
186
|
+
return "screening_pending";
|
|
187
|
+
for (const module of this.modules) {
|
|
188
|
+
if (!module.admissionGate ||
|
|
189
|
+
!(await this.services
|
|
190
|
+
.get(HELYX_SERVICE_NAMES.installations)
|
|
191
|
+
.isModuleEnabled(guild.id, module.manifest.id)))
|
|
192
|
+
continue;
|
|
193
|
+
if (!(await this.services
|
|
194
|
+
.get(HELYX_SERVICE_NAMES.moduleAvailability)
|
|
195
|
+
.isAvailable(module.manifest.id)))
|
|
196
|
+
return "unavailable";
|
|
197
|
+
const gate = await module.admissionGate({
|
|
198
|
+
guildId: guild.id,
|
|
199
|
+
memberUserId,
|
|
200
|
+
roleIds: [...member.roles.cache.keys()],
|
|
201
|
+
services: this.services,
|
|
202
|
+
});
|
|
203
|
+
if (gate === "unavailable")
|
|
204
|
+
return "unavailable";
|
|
205
|
+
if (gate !== "ready")
|
|
206
|
+
return "access_pending";
|
|
207
|
+
}
|
|
208
|
+
return "ready";
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return error instanceof DiscordAPIError &&
|
|
212
|
+
error.code === RESTJSONErrorCodes.UnknownMember
|
|
213
|
+
? "member_missing"
|
|
214
|
+
: "unavailable";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=creator-guild-join-service.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CreatorInviteRepository } from "@helyx/database";
|
|
2
|
+
import { type CreatorInviteService, type CreatorOwnedInvite, type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
|
|
3
|
+
import { type Client } from "discord.js";
|
|
4
|
+
/** Hosted, guild-owning core adapter. Modules never receive its encryption key/store. */
|
|
5
|
+
export declare class DiscordCreatorInviteService implements CreatorInviteService {
|
|
6
|
+
#private;
|
|
7
|
+
private readonly client;
|
|
8
|
+
private readonly repository;
|
|
9
|
+
private readonly services;
|
|
10
|
+
private readonly module;
|
|
11
|
+
constructor(client: Client, repository: CreatorInviteRepository, services: ServiceAccess, module: ModuleDefinition | undefined, encodedEncryptionKey: string);
|
|
12
|
+
selectableRoles(input: Parameters<CreatorInviteService["selectableRoles"]>[0]): ReturnType<CreatorInviteService["selectableRoles"]>;
|
|
13
|
+
create(input: Parameters<CreatorInviteService["create"]>[0]): Promise<CreatorOwnedInvite>;
|
|
14
|
+
inspect(input: Parameters<CreatorInviteService["inspect"]>[0]): Promise<CreatorOwnedInvite>;
|
|
15
|
+
shareUrl(input: Parameters<CreatorInviteService["shareUrl"]>[0]): Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
publicOverview(input: {
|
|
19
|
+
guildId: string;
|
|
20
|
+
reference: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
guildName: string;
|
|
23
|
+
guildIconUrl: string | null;
|
|
24
|
+
discordInviteUrl: string;
|
|
25
|
+
}>;
|
|
26
|
+
revoke(input: Parameters<CreatorInviteService["revoke"]>[0]): ReturnType<CreatorInviteService["revoke"]>;
|
|
27
|
+
/** Cancels a durable intent without issuing a create POST, including rejected intents. */
|
|
28
|
+
cancelOperation(input: {
|
|
29
|
+
guildId: string;
|
|
30
|
+
operationId: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
state: "revoked" | "revocation_pending" | "unknown";
|
|
33
|
+
}>;
|
|
34
|
+
/** Runs outside module enablement; only proven access/safety loss initiates revocation. */
|
|
35
|
+
cleanupUnavailableInvites(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=creator-invite-service.d.ts.map
|