@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
@@ -22,61 +22,62 @@ export function createSupportModule(transport) {
22
22
  framework: "^0.1.0",
23
23
  dependencies: [],
24
24
  permissions: [],
25
- contributions: { commands: ["bug"], events: [] },
25
+ contributions: { commands: ["helyx.bug"], events: [] },
26
26
  },
27
- interactions: {
28
- commands: [
29
- {
30
- name: "bug",
31
- description: "Report a Helyx bug to the support team",
32
- acknowledgement: "immediate-modal",
33
- registration: "global",
34
- contexts: ["guild"],
35
- integrationTypes: ["guild_install"],
36
- async execute(context) {
37
- await context.showModal({
38
- customId: MODAL_ID,
39
- title: "Report a Helyx bug",
40
- fields: [
41
- {
42
- customId: MODULE_FIELD_ID,
43
- label: "Module or area",
44
- style: "short",
45
- required: true,
46
- minLength: 1,
47
- maxLength: 100,
48
- },
49
- {
50
- customId: SUMMARY_FIELD_ID,
51
- label: "Summary",
52
- style: "short",
53
- required: true,
54
- minLength: 10,
55
- maxLength: 200,
56
- },
57
- {
58
- customId: HAPPENED_FIELD_ID,
59
- label: "What happened?",
60
- style: "paragraph",
61
- required: true,
62
- minLength: 10,
63
- maxLength: 1_000,
64
- },
65
- {
66
- customId: DETAIL_FIELD_ID,
67
- label: "Steps to reproduce or extra detail",
68
- style: "paragraph",
69
- required: false,
70
- maxLength: 1_000,
71
- },
72
- ],
73
- });
74
- },
27
+ sharedCommands: [
28
+ {
29
+ family: "helyx",
30
+ path: "bug",
31
+ permissionId: "bug",
32
+ description: "Report a Helyx bug to the support team",
33
+ acknowledgement: "immediate-modal",
34
+ authorization: "public",
35
+ async execute(context) {
36
+ await context.showModal({
37
+ customId: MODAL_ID,
38
+ title: "Report a Helyx bug",
39
+ fields: [
40
+ {
41
+ customId: MODULE_FIELD_ID,
42
+ label: "Module or area",
43
+ style: "short",
44
+ required: true,
45
+ minLength: 1,
46
+ maxLength: 100,
47
+ },
48
+ {
49
+ customId: SUMMARY_FIELD_ID,
50
+ label: "Summary",
51
+ style: "short",
52
+ required: true,
53
+ minLength: 10,
54
+ maxLength: 200,
55
+ },
56
+ {
57
+ customId: HAPPENED_FIELD_ID,
58
+ label: "What happened?",
59
+ style: "paragraph",
60
+ required: true,
61
+ minLength: 10,
62
+ maxLength: 1_000,
63
+ },
64
+ {
65
+ customId: DETAIL_FIELD_ID,
66
+ label: "Steps to reproduce or extra detail",
67
+ style: "paragraph",
68
+ required: false,
69
+ maxLength: 1_000,
70
+ },
71
+ ],
72
+ });
75
73
  },
76
- ],
74
+ },
75
+ ],
76
+ interactions: {
77
77
  modals: [
78
78
  {
79
79
  customId: MODAL_ID,
80
+ acknowledgement: "deferred-ephemeral",
80
81
  async execute(context) {
81
82
  if (!context.serverId) {
82
83
  await context.reply(notSavedResponse());
@@ -117,7 +118,6 @@ export function createSupportModule(transport) {
117
118
  });
118
119
  return;
119
120
  }
120
- await context.deferReply?.(true);
121
121
  const detail = context.getField(DETAIL_FIELD_ID).trim();
122
122
  try {
123
123
  const result = await transport.submitBug({
@@ -0,0 +1,26 @@
1
+ import type { DatabasePool } from "@helyx/database";
2
+ import { type TriviaCategory } from "@helyx/sdk";
3
+ export interface TriviaCategoryData {
4
+ category: TriviaCategory;
5
+ easy: number;
6
+ medium: number;
7
+ hard: number;
8
+ }
9
+ export interface TriviaDataSummary {
10
+ uniqueQuestions: number;
11
+ types: {
12
+ multipleChoice: number;
13
+ guess: number;
14
+ trueFalse: number;
15
+ };
16
+ categories: readonly TriviaCategoryData[];
17
+ }
18
+ export interface TriviaDataProvider {
19
+ getSummary(): Promise<TriviaDataSummary>;
20
+ }
21
+ export declare class PostgresTriviaDataProvider implements TriviaDataProvider {
22
+ #private;
23
+ constructor(database: DatabasePool);
24
+ getSummary(): Promise<TriviaDataSummary>;
25
+ }
26
+ //# sourceMappingURL=trivia-data-provider.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { TRIVIA_CATEGORIES, } from "@helyx/sdk";
2
+ const difficulties = ["easy", "medium", "hard"];
3
+ export class PostgresTriviaDataProvider {
4
+ #database;
5
+ constructor(database) {
6
+ this.#database = database;
7
+ }
8
+ async getSummary() {
9
+ const [uniqueResult, countResult] = await Promise.all([
10
+ this.#database.query(`SELECT count(DISTINCT lower(btrim(question ->> 'text'))) AS unique_questions
11
+ FROM trivia_questions`),
12
+ this.#database.query(`SELECT category, difficulty, type, count(*) AS questions
13
+ FROM trivia_questions
14
+ GROUP BY category, difficulty, type
15
+ ORDER BY category, difficulty, type`),
16
+ ]);
17
+ const counts = new Map();
18
+ for (const row of countResult.rows) {
19
+ if (!TRIVIA_CATEGORIES.includes(row.category) ||
20
+ !difficulties.includes(row.difficulty) ||
21
+ !["text_choice", "text_input", "true_false"].includes(row.type))
22
+ throw new Error("Trivia data contains an unsupported group");
23
+ counts.set(`${row.category}:${row.difficulty}`, (counts.get(`${row.category}:${row.difficulty}`) ?? 0) +
24
+ nonNegativeInteger(row.questions, "Trivia question count"));
25
+ }
26
+ const typeCount = (type) => countResult.rows
27
+ .filter((row) => row.type === type)
28
+ .reduce((total, row) => total + nonNegativeInteger(row.questions, "Trivia type count"), 0);
29
+ return {
30
+ uniqueQuestions: nonNegativeInteger(uniqueResult.rows[0]?.unique_questions, "Unique trivia question count"),
31
+ types: {
32
+ multipleChoice: typeCount("text_choice"),
33
+ guess: typeCount("text_input"),
34
+ trueFalse: typeCount("true_false"),
35
+ },
36
+ categories: TRIVIA_CATEGORIES.map((category) => ({
37
+ category,
38
+ easy: counts.get(`${category}:easy`) ?? 0,
39
+ medium: counts.get(`${category}:medium`) ?? 0,
40
+ hard: counts.get(`${category}:hard`) ?? 0,
41
+ })),
42
+ };
43
+ }
44
+ }
45
+ function nonNegativeInteger(value, label) {
46
+ const parsed = typeof value === "string" ? Number(value) : value;
47
+ if (typeof parsed !== "number" || !Number.isSafeInteger(parsed) || parsed < 0)
48
+ throw new Error(`${label} is invalid`);
49
+ return parsed;
50
+ }
51
+ //# sourceMappingURL=trivia-data-provider.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -51,17 +51,18 @@
51
51
  "@helyx/config": ">=0.5.0 <0.6.0",
52
52
  "@helyx/control-protocol": ">=0.4.0 <0.5.0",
53
53
  "@helyx/core": ">=0.1.0 <0.2.0",
54
- "@helyx/database": ">=0.8.0 <0.9.0",
54
+ "@helyx/database": ">=0.8.6 <0.9.0",
55
55
  "@helyx/framework": ">=0.1.0 <0.2.0",
56
56
  "@helyx/module-manifest": ">=0.7.0 <0.8.0",
57
- "@helyx/platform": ">=0.6.0 <0.7.0",
58
- "@helyx/sdk": ">=0.8.0 <0.9.0",
57
+ "@helyx/platform": ">=0.6.2 <0.7.0",
58
+ "@helyx/sdk": ">=0.8.4 <0.9.0",
59
59
  "@helyx/status-client": ">=0.1.0 <0.2.0",
60
60
  "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
61
61
  "discord.js": "14.27.0",
62
62
  "fastify": "5.10.0",
63
63
  "pino": "10.3.1",
64
64
  "redis": "6.1.0",
65
+ "undici": "6.28.0",
65
66
  "ws": "8.21.1",
66
67
  "zod": "4.4.3"
67
68
  },
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=oauth-module.test.d.ts.map
@@ -1,172 +0,0 @@
1
- import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
- import { describe, expect, it, vi } from "vitest";
3
- import { createOAuthModule } from "./oauth-module.js";
4
- describe("global OAuth command", () => {
5
- it("is global, guild-scoped and available without management permissions", () => {
6
- const module = createOAuthModule();
7
- const command = module.interactions?.commands?.[0];
8
- const revoke = command?.subcommands?.[0];
9
- expect(command).toMatchObject({
10
- name: "oauth",
11
- registration: "global",
12
- contexts: ["guild"],
13
- integrationTypes: ["guild_install"],
14
- });
15
- expect(revoke).toMatchObject({
16
- name: "revoke",
17
- rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
18
- });
19
- expect(module.manifest.permissions).toEqual([]);
20
- expect(module.defaultPermissions?.commands["oauth.revoke"]).toMatchObject({
21
- everyone: true,
22
- requiredPermissions: [],
23
- });
24
- });
25
- it("does not open a modal when the acting user has no connections", async () => {
26
- const oauth = oauthService({ connections: [] });
27
- const showModal = vi.fn();
28
- const replies = [];
29
- const command = createOAuthModule().interactions?.commands?.[0]?.subcommands?.[0];
30
- await command?.execute(commandContext(oauth, {
31
- showModal,
32
- reply: (response) => {
33
- replies.push(response);
34
- return Promise.resolve();
35
- },
36
- }));
37
- expect(showModal).not.toHaveBeenCalled();
38
- expect(replies).toEqual([expect.objectContaining({ ephemeral: true })]);
39
- expect(replies[0]?.componentsV2?.text?.[0]?.content).toContain("do not have any OAuth services");
40
- });
41
- it("uses opaque selections and requires explicit removal confirmation", async () => {
42
- const oauth = oauthService({
43
- connections: [
44
- {
45
- selectionRef: "opaque-selection-reference-123",
46
- provider: "youtube",
47
- displayName: "Example creator",
48
- connectedAt: new Date().toISOString(),
49
- },
50
- ],
51
- });
52
- const showModal = vi.fn();
53
- const command = createOAuthModule().interactions?.commands?.[0]?.subcommands?.[0];
54
- await command?.execute(commandContext(oauth, { showModal }));
55
- const modal = showModal.mock.calls[0]?.[0];
56
- expect(modal?.title).toBe("Revoke OAuth access");
57
- const service = modal?.items?.find((item) => "customId" in item && item.customId.endsWith("service"));
58
- const confirmation = modal?.items?.find((item) => "customId" in item && item.customId.endsWith("confirm"));
59
- expect(service).toMatchObject({
60
- type: "string-select",
61
- required: true,
62
- options: [
63
- expect.objectContaining({ value: "opaque-selection-reference-123" }),
64
- ],
65
- });
66
- expect(confirmation).toMatchObject({
67
- type: "string-select",
68
- required: true,
69
- options: [expect.objectContaining({ value: "remove_access" })],
70
- });
71
- });
72
- it("revokes only for the acting user and returns an ephemeral advisory", async () => {
73
- const revoke = vi.fn().mockResolvedValue({
74
- provider: "twitch",
75
- outcome: "completed",
76
- providerRevocation: "failed",
77
- });
78
- const oauth = oauthService({ connections: [], revoke });
79
- const replies = [];
80
- const modal = createOAuthModule().interactions?.modals?.[0];
81
- await modal?.execute(modalContext(oauth, {
82
- reply: (response) => {
83
- replies.push(response);
84
- return Promise.resolve();
85
- },
86
- }));
87
- expect(revoke).toHaveBeenCalledWith({
88
- discordUserId: "12345678901234567",
89
- selectionRef: "opaque-selection-reference-123",
90
- confirmation: "remove_access",
91
- comment: "No longer needed",
92
- correlationId: "interaction-1",
93
- });
94
- expect(replies[0]).toMatchObject({ ephemeral: true });
95
- expect(replies[0]?.componentsV2?.text?.[0]?.content).toContain("connected-app settings");
96
- });
97
- });
98
- function oauthService(input) {
99
- return {
100
- connectionStatus: () => Promise.resolve([]),
101
- listConnections: () => Promise.resolve(input.connections),
102
- createAuthorization: () => Promise.resolve({
103
- authorizationUrl: "https://example.com",
104
- expiresAt: "",
105
- }),
106
- verifyEligibility: () => Promise.resolve({
107
- provider: "youtube",
108
- status: "unknown",
109
- connected: false,
110
- checkedAt: new Date().toISOString(),
111
- }),
112
- revoke: input.revoke ??
113
- (() => Promise.resolve({
114
- provider: "youtube",
115
- outcome: "completed",
116
- providerRevocation: "completed",
117
- })),
118
- };
119
- }
120
- function services(oauth) {
121
- return {
122
- has: (name) => name === HELYX_SERVICE_NAMES.oauth,
123
- get: (name) => {
124
- if (name !== HELYX_SERVICE_NAMES.oauth)
125
- throw new Error("unknown service");
126
- return oauth;
127
- },
128
- };
129
- }
130
- function commandContext(oauth, overrides = {}) {
131
- return {
132
- ...interactionBase(oauth),
133
- showModal: () => Promise.resolve(),
134
- getStringOption: () => null,
135
- getBooleanOption: () => null,
136
- getUserOption: () => null,
137
- getChannelOption: () => null,
138
- getRoleOption: () => null,
139
- ...overrides,
140
- };
141
- }
142
- function modalContext(oauth, overrides = {}) {
143
- return {
144
- ...interactionBase(oauth),
145
- getField: (customId) => customId.endsWith("comment") ? "No longer needed" : "",
146
- getStringSelectValues: (customId) => customId.endsWith("service")
147
- ? ["opaque-selection-reference-123"]
148
- : ["remove_access"],
149
- getSelectedUserIds: () => [],
150
- getSelectedRoleIds: () => [],
151
- getSelectedMentionables: () => ({ userIds: [], roleIds: [] }),
152
- getSelectedChannelIds: () => [],
153
- ...overrides,
154
- };
155
- }
156
- function interactionBase(oauth) {
157
- return {
158
- interactionId: "interaction-1",
159
- userId: "12345678901234567",
160
- userDisplayName: "Member",
161
- userRoleIds: [],
162
- isAdministrator: false,
163
- serverId: "12345678901234568",
164
- services: services(oauth),
165
- reply: () => Promise.resolve(),
166
- sendToChannel: () => Promise.resolve({ messageId: "message-1" }),
167
- deleteChannelMessage: () => Promise.resolve(),
168
- editChannelMessage: () => Promise.resolve(),
169
- addChannelMessageReaction: () => Promise.resolve(),
170
- };
171
- }
172
- //# sourceMappingURL=oauth-module.test.js.map