@helyx/bot 0.4.37 → 0.4.38

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +17 -2
  3. package/dist/cloud-management-module.js +275 -206
  4. package/dist/command-registration-policy.d.ts +8 -0
  5. package/dist/command-registration-policy.js +39 -0
  6. package/dist/command-sync.d.ts +5 -1
  7. package/dist/command-sync.js +67 -3
  8. package/dist/control-server.d.ts +15 -3
  9. package/dist/control-server.js +34 -48
  10. package/dist/dashboard-commands.js +5 -1
  11. package/dist/developer-operations-module.d.ts +2 -0
  12. package/dist/developer-operations-module.js +65 -4
  13. package/dist/discord-rest.d.ts +5 -0
  14. package/dist/discord-rest.js +9 -0
  15. package/dist/events.js +4 -2
  16. package/dist/first-time-command-setup.d.ts +25 -0
  17. package/dist/first-time-command-setup.js +22 -0
  18. package/dist/guild-command-control-routes.d.ts +5 -0
  19. package/dist/guild-command-control-routes.js +191 -0
  20. package/dist/guild-command-discord-client.d.ts +21 -0
  21. package/dist/guild-command-discord-client.js +50 -0
  22. package/dist/guild-command-projection.d.ts +46 -0
  23. package/dist/guild-command-projection.js +254 -0
  24. package/dist/guild-command-read-service.d.ts +201 -0
  25. package/dist/guild-command-read-service.js +272 -0
  26. package/dist/guild-command-release.d.ts +19 -0
  27. package/dist/guild-command-release.js +47 -0
  28. package/dist/guild-command-worker.d.ts +49 -0
  29. package/dist/guild-command-worker.js +443 -0
  30. package/dist/guilds.d.ts +1 -1
  31. package/dist/guilds.js +41 -26
  32. package/dist/index.js +75 -4
  33. package/dist/interactions.d.ts +15 -4
  34. package/dist/interactions.js +285 -70
  35. package/dist/management-module.js +117 -41
  36. package/dist/module-logging.d.ts +1 -0
  37. package/dist/module-logging.js +5 -5
  38. package/dist/module-status-control.d.ts +19 -0
  39. package/dist/module-status-control.js +69 -0
  40. package/dist/modules.d.ts +2 -0
  41. package/dist/modules.js +21 -2
  42. package/dist/nexus-commands.d.ts +1 -12
  43. package/dist/nexus-commands.js +1 -99
  44. package/dist/oauth-module.js +134 -88
  45. package/dist/permissions-module.js +112 -39
  46. package/dist/privacy-module.d.ts +2 -0
  47. package/dist/privacy-module.js +60 -0
  48. package/dist/scheduled-task-runner.js +36 -1
  49. package/dist/shard-contracts.d.ts +24 -0
  50. package/dist/shard-contracts.js +20 -0
  51. package/dist/shard-router.d.ts +1 -0
  52. package/dist/shard-router.js +7 -0
  53. package/dist/shard-supervisor.d.ts +2 -1
  54. package/dist/shard-supervisor.js +94 -16
  55. package/dist/shard-worker.d.ts +5 -0
  56. package/dist/shard-worker.js +9 -1
  57. package/dist/shared-commands.d.ts +13 -0
  58. package/dist/shared-commands.js +163 -0
  59. package/dist/support-module.js +51 -51
  60. package/dist/trivia-data-provider.d.ts +26 -0
  61. package/dist/trivia-data-provider.js +51 -0
  62. package/package.json +5 -4
  63. package/dist/oauth-module.test.d.ts +0 -2
  64. package/dist/oauth-module.test.js +0 -172
@@ -2,6 +2,7 @@ import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
2
  import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
3
3
  const MODULE_ID = "helyx.oauth";
4
4
  const MODAL_ID = "helyx.oauth.revoke";
5
+ const OPEN_MODAL_ID = "helyx.oauth.revoke.open";
5
6
  const SERVICE_FIELD_ID = "helyx.oauth.revoke.service";
6
7
  const CONFIRM_FIELD_ID = "helyx.oauth.revoke.confirm";
7
8
  const COMMENT_FIELD_ID = "helyx.oauth.revoke.comment";
@@ -14,6 +15,7 @@ const everyonePolicy = {
14
15
  deniedUserIds: [],
15
16
  };
16
17
  export function createOAuthModule() {
18
+ const preparedModals = new Map();
17
19
  return defineModule({
18
20
  manifest: {
19
21
  ...INTERNAL_MODULE_MANIFEST_FIELDS,
@@ -26,108 +28,153 @@ export function createOAuthModule() {
26
28
  framework: "^0.1.0",
27
29
  dependencies: [],
28
30
  permissions: [],
29
- contributions: { commands: ["oauth"], events: [] },
31
+ contributions: { commands: ["helyx.connections"], events: [] },
30
32
  },
31
33
  defaultPermissions: {
32
34
  module: everyonePolicy,
33
35
  commands: { "oauth.revoke": everyonePolicy },
34
36
  },
37
+ sharedCommands: [
38
+ {
39
+ family: "helyx",
40
+ path: "connections",
41
+ permissionId: "oauth.revoke",
42
+ description: "Review or revoke OAuth access granted to Helyx.",
43
+ acknowledgement: "deferred-ephemeral",
44
+ authorization: "public",
45
+ rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
46
+ execute: async (context) => {
47
+ const oauth = context.services.get(HELYX_SERVICE_NAMES.oauth);
48
+ let connections;
49
+ try {
50
+ connections = await oauth.listConnections({
51
+ discordUserId: context.userId,
52
+ });
53
+ }
54
+ catch {
55
+ await context.reply(unavailable());
56
+ return;
57
+ }
58
+ if (connections.length === 0) {
59
+ await context.reply({
60
+ ephemeral: true,
61
+ componentsV2: {
62
+ text: [
63
+ {
64
+ content: "You do not have any OAuth services connected to Helyx.",
65
+ },
66
+ ],
67
+ },
68
+ });
69
+ return;
70
+ }
71
+ const definition = {
72
+ customId: MODAL_ID,
73
+ title: "Revoke OAuth access",
74
+ items: [
75
+ {
76
+ type: "text-display",
77
+ content: "Choose one connection. This disconnects the service from Helyx everywhere. Any Helyx module, server or panel using it will lose access.",
78
+ },
79
+ {
80
+ type: "string-select",
81
+ customId: SERVICE_FIELD_ID,
82
+ label: "Connected service",
83
+ placeholder: "Choose a connection",
84
+ required: true,
85
+ minValues: 1,
86
+ maxValues: 1,
87
+ options: connections.slice(0, 25).map((connection) => ({
88
+ label: `${providerName(connection.provider)} — ${connection.displayName}`.slice(0, 100),
89
+ value: connection.selectionRef,
90
+ description: "Remove this connection",
91
+ })),
92
+ },
93
+ {
94
+ type: "string-select",
95
+ customId: CONFIRM_FIELD_ID,
96
+ label: "Confirm removal",
97
+ placeholder: "Select Remove access",
98
+ required: true,
99
+ minValues: 1,
100
+ maxValues: 1,
101
+ options: [
102
+ {
103
+ label: "Remove access",
104
+ value: "remove_access",
105
+ description: "Revoke access and delete my stored details",
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ customId: COMMENT_FIELD_ID,
111
+ label: "Reason or comment (optional)",
112
+ description: "Stored only in Helyx's restricted security audit.",
113
+ style: "paragraph",
114
+ required: false,
115
+ maxLength: 250,
116
+ },
117
+ ],
118
+ fields: [],
119
+ };
120
+ for (const [userId, prepared] of preparedModals) {
121
+ if (prepared.expiresAt <= Date.now())
122
+ preparedModals.delete(userId);
123
+ }
124
+ preparedModals.set(context.userId, {
125
+ definition,
126
+ expiresAt: Date.now() + 2 * 60 * 1_000,
127
+ });
128
+ await context.reply({
129
+ ephemeral: true,
130
+ componentsV2: {
131
+ text: [
132
+ {
133
+ content: `Found ${connections.length} connected service${connections.length === 1 ? "" : "s"}. Open the secure form to choose which connection to remove.`,
134
+ },
135
+ ],
136
+ },
137
+ buttons: [
138
+ {
139
+ type: "button",
140
+ customId: OPEN_MODAL_ID,
141
+ label: "Open connection form",
142
+ style: "primary",
143
+ },
144
+ ],
145
+ });
146
+ },
147
+ },
148
+ ],
35
149
  interactions: {
36
- commands: [
150
+ buttons: [
37
151
  {
38
- name: "oauth",
39
- description: "Manage OAuth access granted to Helyx.",
40
- registration: "global",
41
- contexts: ["guild"],
42
- integrationTypes: ["guild_install"],
43
- subcommands: [
44
- {
45
- name: "revoke",
46
- description: "Revoke a service and delete the OAuth details Helyx stores for you.",
47
- acknowledgement: "immediate-modal",
48
- rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
49
- execute: async (context) => {
50
- const oauth = context.services.get(HELYX_SERVICE_NAMES.oauth);
51
- let connections;
52
- try {
53
- connections = await oauth.listConnections({
54
- discordUserId: context.userId,
55
- });
56
- }
57
- catch {
58
- await context.reply(unavailable());
59
- return;
60
- }
61
- if (connections.length === 0) {
62
- await context.reply({
63
- ephemeral: true,
64
- componentsV2: {
65
- text: [
66
- {
67
- content: "You do not have any OAuth services connected to Helyx.",
68
- },
69
- ],
70
- },
71
- });
72
- return;
73
- }
74
- await context.showModal({
75
- customId: MODAL_ID,
76
- title: "Revoke OAuth access",
77
- items: [
78
- {
79
- type: "text-display",
80
- content: "Choose one connection. This disconnects the service from Helyx everywhere. Any Helyx module, server or panel using it will lose access.",
81
- },
82
- {
83
- type: "string-select",
84
- customId: SERVICE_FIELD_ID,
85
- label: "Connected service",
86
- placeholder: "Choose a connection",
87
- required: true,
88
- minValues: 1,
89
- maxValues: 1,
90
- options: connections.slice(0, 25).map((connection) => ({
91
- label: `${providerName(connection.provider)} — ${connection.displayName}`.slice(0, 100),
92
- value: connection.selectionRef,
93
- description: "Remove this connection",
94
- })),
95
- },
96
- {
97
- type: "string-select",
98
- customId: CONFIRM_FIELD_ID,
99
- label: "Confirm removal",
100
- placeholder: "Select Remove access",
101
- required: true,
102
- minValues: 1,
103
- maxValues: 1,
104
- options: [
105
- {
106
- label: "Remove access",
107
- value: "remove_access",
108
- description: "Revoke access and delete my stored details",
109
- },
110
- ],
111
- },
152
+ customId: OPEN_MODAL_ID,
153
+ authorization: "public",
154
+ execute: async (context) => {
155
+ const prepared = preparedModals.get(context.userId);
156
+ preparedModals.delete(context.userId);
157
+ if (!prepared || prepared.expiresAt <= Date.now()) {
158
+ await context.reply({
159
+ ephemeral: true,
160
+ componentsV2: {
161
+ text: [
112
162
  {
113
- customId: COMMENT_FIELD_ID,
114
- label: "Reason or comment (optional)",
115
- description: "Stored only in Helyx's restricted security audit.",
116
- style: "paragraph",
117
- required: false,
118
- maxLength: 250,
163
+ content: "This connection form expired. Run `/helyx connections` again.",
119
164
  },
120
165
  ],
121
- fields: [],
122
- });
123
- },
124
- },
125
- ],
166
+ },
167
+ });
168
+ return;
169
+ }
170
+ await context.showModal(prepared.definition);
171
+ },
126
172
  },
127
173
  ],
128
174
  modals: [
129
175
  {
130
176
  customId: MODAL_ID,
177
+ acknowledgement: "deferred-ephemeral",
131
178
  execute: async (context) => {
132
179
  const selectionRef = context.getStringSelectValues(SERVICE_FIELD_ID)[0];
133
180
  const confirmation = context.getStringSelectValues(CONFIRM_FIELD_ID)[0];
@@ -141,7 +188,6 @@ export function createOAuthModule() {
141
188
  return;
142
189
  }
143
190
  const comment = context.getField(COMMENT_FIELD_ID).trim();
144
- await context.deferReply?.(true);
145
191
  try {
146
192
  const result = await context.services
147
193
  .get(HELYX_SERVICE_NAMES.oauth)
@@ -10,6 +10,7 @@ const MODULE_NEXT_ID = "helyx.permissions.module.next";
10
10
  const TARGET_PREVIOUS_ID = "helyx.permissions.target.previous";
11
11
  const TARGET_NEXT_ID = "helyx.permissions.target.next";
12
12
  const POLICY_MODAL_ID = "helyx.permissions.policy";
13
+ const OPEN_POLICY_MODAL_ID = "helyx.permissions.policy.open";
13
14
  const EVERYONE_FIELD_ID = "helyx.permissions.policy.everyone";
14
15
  const REQUIRED_PERMISSIONS_FIELD_ID = "helyx.permissions.policy.required";
15
16
  const ALLOWED_MENTIONABLES_FIELD_ID = "helyx.permissions.policy.allowed";
@@ -25,6 +26,8 @@ export function createPermissionsModule(configurableModules) {
25
26
  const commandModules = [...modules.values()]
26
27
  .filter((module) => commandTargets(module).length > 0)
27
28
  .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name, "en-GB"));
29
+ const preparedPolicies = new Map();
30
+ const preparedPolicyKey = (serverId, userId) => `${serverId}:${userId}`;
28
31
  return defineModule({
29
32
  manifest: {
30
33
  ...INTERNAL_MODULE_MANIFEST_FIELDS,
@@ -37,7 +40,7 @@ export function createPermissionsModule(configurableModules) {
37
40
  framework: "^0.1.0",
38
41
  dependencies: [],
39
42
  permissions: [],
40
- contributions: { commands: ["permissions"], events: [] },
43
+ contributions: { commands: ["helyx.permissions"], events: [] },
41
44
  },
42
45
  defaultPermissions: {
43
46
  module: restrictedPolicy(),
@@ -45,47 +48,49 @@ export function createPermissionsModule(configurableModules) {
45
48
  permissions: restrictedPolicy(),
46
49
  },
47
50
  },
48
- interactions: {
49
- commands: [
50
- {
51
- name: "permissions",
52
- description: "Configure access to Helyx modules and commands",
53
- acknowledgement: "deferred-ephemeral",
54
- registration: "global",
55
- contexts: ["guild"],
56
- integrationTypes: ["guild_install"],
57
- rateLimit: { scope: "user", limit: 20, windowSeconds: 60 },
58
- async execute(context) {
59
- if (!context.serverId)
60
- return;
61
- if (commandModules.length === 0) {
62
- await context.reply({
63
- ephemeral: true,
64
- embed: {
65
- title: "No configurable modules",
66
- description: "Install a feature module that contributes Discord commands first.",
67
- color: 0xdc2626,
68
- },
69
- });
70
- return;
71
- }
72
- await context.services
73
- .get(HELYX_SERVICE_NAMES.sessions)
74
- .create({
75
- guildId: context.serverId,
76
- userId: context.userId,
77
- purpose: SESSION_PURPOSE,
78
- state: { modulePage: 0 },
79
- expiresAt: new Date(Date.now() + 10 * 60 * 1_000),
80
- replace: true,
51
+ sharedCommands: [
52
+ {
53
+ family: "helyx",
54
+ path: "permissions",
55
+ permissionId: "permissions",
56
+ description: "Configure access to Helyx modules and commands",
57
+ acknowledgement: "deferred-ephemeral",
58
+ rateLimit: { scope: "user", limit: 20, windowSeconds: 60 },
59
+ async execute(context) {
60
+ if (!context.serverId) {
61
+ await context.reply(serverOnlyResponse());
62
+ return;
63
+ }
64
+ if (commandModules.length === 0) {
65
+ await context.reply({
66
+ ephemeral: true,
67
+ embed: {
68
+ title: "No configurable modules",
69
+ description: "Install a feature module that contributes Discord commands first.",
70
+ color: 0xdc2626,
71
+ },
81
72
  });
82
- await context.reply(modulePickerResponse(commandModules, 0));
83
- },
73
+ return;
74
+ }
75
+ await context.services
76
+ .get(HELYX_SERVICE_NAMES.sessions)
77
+ .create({
78
+ guildId: context.serverId,
79
+ userId: context.userId,
80
+ purpose: SESSION_PURPOSE,
81
+ state: { modulePage: 0 },
82
+ expiresAt: new Date(Date.now() + 10 * 60 * 1_000),
83
+ replace: true,
84
+ });
85
+ await context.reply(modulePickerResponse(commandModules, 0));
84
86
  },
85
- ],
87
+ },
88
+ ],
89
+ interactions: {
86
90
  selects: [
87
91
  {
88
92
  customId: MODULE_SELECT_ID,
93
+ acknowledgement: "deferred-update",
89
94
  async execute(context) {
90
95
  const session = await getSession(context);
91
96
  if (!session || !context.serverId)
@@ -113,6 +118,7 @@ export function createPermissionsModule(configurableModules) {
113
118
  },
114
119
  {
115
120
  customId: TARGET_SELECT_ID,
121
+ acknowledgement: "deferred-update",
116
122
  async execute(context) {
117
123
  const session = await getSession(context);
118
124
  if (!session || !context.serverId)
@@ -156,7 +162,7 @@ export function createPermissionsModule(configurableModules) {
156
162
  },
157
163
  expectedVersion: session.version,
158
164
  });
159
- await context.showModal({
165
+ const definition = {
160
166
  customId: POLICY_MODAL_ID,
161
167
  title: "Edit permission policy",
162
168
  fields: [
@@ -213,11 +219,63 @@ export function createPermissionsModule(configurableModules) {
213
219
  defaultValues: deniedDefaults,
214
220
  },
215
221
  ],
222
+ };
223
+ for (const [key, prepared] of preparedPolicies) {
224
+ if (prepared.expiresAt <= Date.now())
225
+ preparedPolicies.delete(key);
226
+ }
227
+ preparedPolicies.set(preparedPolicyKey(context.serverId, context.userId), {
228
+ definition,
229
+ expiresAt: Date.now() + 2 * 60 * 1_000,
230
+ });
231
+ await context.reply({
232
+ ephemeral: true,
233
+ componentsV2: {
234
+ text: [
235
+ {
236
+ content: "The current permission policy is ready. Open the editor to make changes.",
237
+ },
238
+ ],
239
+ },
240
+ buttons: [
241
+ {
242
+ type: "button",
243
+ customId: OPEN_POLICY_MODAL_ID,
244
+ label: "Open policy editor",
245
+ style: "primary",
246
+ },
247
+ ],
216
248
  });
217
249
  },
218
250
  },
219
251
  ],
220
252
  buttons: [
253
+ {
254
+ customId: OPEN_POLICY_MODAL_ID,
255
+ authorization: "public",
256
+ async execute(context) {
257
+ const key = context.serverId
258
+ ? preparedPolicyKey(context.serverId, context.userId)
259
+ : null;
260
+ const prepared = key ? preparedPolicies.get(key) : undefined;
261
+ if (key)
262
+ preparedPolicies.delete(key);
263
+ if (!prepared || prepared.expiresAt <= Date.now()) {
264
+ await context.reply({
265
+ ephemeral: true,
266
+ componentsV2: {
267
+ text: [
268
+ {
269
+ content: "This policy editor expired. Run `/helyx permissions` again.",
270
+ },
271
+ ],
272
+ },
273
+ });
274
+ return;
275
+ }
276
+ await context.showModal(prepared.definition);
277
+ },
278
+ },
221
279
  modulePageButton(MODULE_PREVIOUS_ID, -1),
222
280
  modulePageButton(MODULE_NEXT_ID, 1),
223
281
  targetPageButton(TARGET_PREVIOUS_ID, -1),
@@ -226,6 +284,7 @@ export function createPermissionsModule(configurableModules) {
226
284
  modals: [
227
285
  {
228
286
  customId: POLICY_MODAL_ID,
287
+ acknowledgement: "deferred-ephemeral",
229
288
  async execute(context) {
230
289
  const session = await getSession(context);
231
290
  if (!session || !context.serverId)
@@ -309,6 +368,7 @@ export function createPermissionsModule(configurableModules) {
309
368
  function modulePageButton(customId, change) {
310
369
  return {
311
370
  customId,
371
+ acknowledgement: "deferred-update",
312
372
  async execute(context) {
313
373
  const session = await getSession(context);
314
374
  if (!session)
@@ -329,6 +389,7 @@ export function createPermissionsModule(configurableModules) {
329
389
  function targetPageButton(customId, change) {
330
390
  return {
331
391
  customId,
392
+ acknowledgement: "deferred-update",
332
393
  async execute(context) {
333
394
  const session = await getSession(context);
334
395
  if (!session)
@@ -352,6 +413,18 @@ export function createPermissionsModule(configurableModules) {
352
413
  };
353
414
  }
354
415
  }
416
+ function serverOnlyResponse() {
417
+ return {
418
+ ephemeral: true,
419
+ componentsV2: {
420
+ text: [
421
+ {
422
+ content: "This command needs a server. Open a server where Helyx is installed, then use `/helyx permissions` there.",
423
+ },
424
+ ],
425
+ },
426
+ };
427
+ }
355
428
  function restrictedPolicy() {
356
429
  return {
357
430
  everyone: false,
@@ -373,7 +446,7 @@ function commandTargets(module) {
373
446
  return nested;
374
447
  return [command.name];
375
448
  });
376
- targets.push(...(module.nexusCommands ?? []).map((command) => `nexus.${command.path}`));
449
+ targets.push(...(module.sharedCommands ?? []).map((command) => command.permissionId ?? `${command.family}.${command.path}`), ...(module.nexusCommands ?? []).map((command) => `nexus.${command.path}`));
377
450
  return [...new Set(targets)];
378
451
  }
379
452
  function modulePickerResponse(modules, requestedPage) {
@@ -0,0 +1,2 @@
1
+ export declare function createPrivacyModule(): import("@helyx/sdk").ModuleDefinition;
2
+ //# sourceMappingURL=privacy-module.d.ts.map
@@ -0,0 +1,60 @@
1
+ import { defineModule } from "@helyx/sdk";
2
+ import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
3
+ const everyonePolicy = {
4
+ everyone: true,
5
+ requiredPermissions: [],
6
+ allowedRoleIds: [],
7
+ deniedRoleIds: [],
8
+ allowedUserIds: [],
9
+ deniedUserIds: [],
10
+ };
11
+ export function createPrivacyModule() {
12
+ return defineModule({
13
+ manifest: {
14
+ ...INTERNAL_MODULE_MANIFEST_FIELDS,
15
+ releaseStatus: "live",
16
+ id: "helyx.privacy",
17
+ name: "Helyx Privacy",
18
+ version: "0.1.0",
19
+ description: "Provides an always-available route to Helyx data controls.",
20
+ entry: "./dist/privacy-module.js",
21
+ framework: "^0.1.0",
22
+ dependencies: [],
23
+ permissions: [],
24
+ contributions: { commands: ["helyx.privacy"], events: [] },
25
+ },
26
+ defaultPermissions: {
27
+ module: everyonePolicy,
28
+ commands: { "helyx.privacy": everyonePolicy },
29
+ },
30
+ sharedCommands: [
31
+ {
32
+ family: "helyx",
33
+ path: "privacy",
34
+ permissionId: "helyx.privacy",
35
+ description: "Open the Helyx Privacy Centre and data controls",
36
+ acknowledgement: "immediate-response",
37
+ authorization: "public",
38
+ async execute(context) {
39
+ await context.reply({
40
+ ephemeral: true,
41
+ componentsV2: {
42
+ text: [
43
+ {
44
+ content: "## Helyx Privacy Centre\nView your data and request changes or deletion at [app.helyx.gg/privacy](https://app.helyx.gg/privacy).",
45
+ },
46
+ ],
47
+ },
48
+ });
49
+ },
50
+ },
51
+ ],
52
+ start() {
53
+ return Promise.resolve();
54
+ },
55
+ stop() {
56
+ return Promise.resolve();
57
+ },
58
+ });
59
+ }
60
+ //# sourceMappingURL=privacy-module.js.map
@@ -1,4 +1,4 @@
1
- import { CORE_RECORD_RETENTION_TASK, ScheduledTaskRepository, } from "@helyx/database";
1
+ import { CORE_RECORD_RETENTION_TASK, MODULE_ACTIVATION_RECOVERY_TASK, ScheduledTaskRepository, } from "@helyx/database";
2
2
  import { getModuleScheduledTaskContribution, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
3
3
  import { randomUUID } from "node:crypto";
4
4
  const DEFAULT_POLL_INTERVAL_MS = 1_000;
@@ -279,6 +279,16 @@ export function createScheduledTaskRunner(input) {
279
279
  delivery: "one-shot",
280
280
  runWhenDisabled: true,
281
281
  },
282
+ ...input.modules
283
+ .filter((module) => typeof module.onActivated === "function" ||
284
+ typeof module.onDeactivated === "function")
285
+ .map((module) => ({
286
+ moduleId: module.manifest.id,
287
+ taskKind: MODULE_ACTIVATION_RECOVERY_TASK.taskKind,
288
+ taskVersion: MODULE_ACTIVATION_RECOVERY_TASK.taskVersion,
289
+ delivery: "one-shot",
290
+ runWhenDisabled: true,
291
+ })),
282
292
  ...input.modules.flatMap((module) => (module.scheduledTasks ?? []).map((contribution) => ({
283
293
  moduleId: module.manifest.id,
284
294
  taskKind: contribution.kind,
@@ -328,6 +338,24 @@ export function createLocalScheduledTaskRoute(input) {
328
338
  const module = modules.get(task.moduleId);
329
339
  if (!module)
330
340
  return deferred("module_unavailable", DEFAULT_POLL_INTERVAL_MS * 30);
341
+ if (task.taskKind === MODULE_ACTIVATION_RECOVERY_TASK.taskKind &&
342
+ task.taskVersion === MODULE_ACTIVATION_RECOVERY_TASK.taskVersion &&
343
+ task.delivery === "one-shot") {
344
+ const state = parseActivationRecoveryState(task.payload);
345
+ if (!state || !task.guildId)
346
+ return { outcome: "requires_review", safeCode: "payload_invalid" };
347
+ const enabled = await input.services
348
+ .get(HELYX_SERVICE_NAMES.installations)
349
+ .isModuleEnabled(task.guildId, task.moduleId);
350
+ if (enabled !== (state === "enabled"))
351
+ return { outcome: "completed" };
352
+ const context = { guildId: task.guildId, services: input.services };
353
+ if (state === "enabled")
354
+ await module.onActivated?.(context);
355
+ else
356
+ await module.onDeactivated?.(context);
357
+ return { outcome: "completed" };
358
+ }
331
359
  if (typeof input.services.has === "function" &&
332
360
  input.services.has(HELYX_SERVICE_NAMES.moduleAvailability)) {
333
361
  const availability = input.services.get(HELYX_SERVICE_NAMES.moduleAvailability);
@@ -368,6 +396,13 @@ export function createLocalScheduledTaskRoute(input) {
368
396
  }, task);
369
397
  };
370
398
  }
399
+ function parseActivationRecoveryState(payload) {
400
+ if (Object.keys(payload).length !== 1)
401
+ return null;
402
+ return payload.stateId === "enabled" || payload.stateId === "disabled"
403
+ ? payload.stateId
404
+ : null;
405
+ }
371
406
  function toExecution(task) {
372
407
  const common = {
373
408
  taskId: task.record.id,