@helyx/bot 1.6.0 → 1.7.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 +6 -0
- package/command-root-catalogue.json +13 -5
- package/dist/cloud-connector.js +2 -0
- package/dist/cloud-control-dispatcher.js +20 -0
- package/dist/command-registration-policy.js +1 -0
- package/dist/control-directory-contracts.d.ts +50 -0
- package/dist/control-directory-contracts.js +2 -0
- package/dist/control-server-schemas.d.ts +3 -3
- package/dist/control-server.d.ts +10 -46
- package/dist/control-server.js +50 -3
- package/dist/creator-invite-service.d.ts +7 -0
- package/dist/creator-invite-service.js +17 -0
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/events.d.ts +1 -0
- package/dist/events.js +2 -0
- package/dist/help-credentials.d.ts +25 -0
- package/dist/help-credentials.js +149 -0
- package/dist/help-module.d.ts +27 -0
- package/dist/help-module.js +288 -0
- package/dist/help-runtime.d.ts +3 -0
- package/dist/help-runtime.js +42 -0
- package/dist/help-transport.d.ts +53 -0
- package/dist/help-transport.js +267 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/interactions.js +10 -6
- package/dist/server-overview-control.d.ts +35 -0
- package/dist/server-overview-control.js +362 -0
- package/dist/server-overview-discord.d.ts +33 -0
- package/dist/server-overview-discord.js +212 -0
- package/dist/server-overview-profile.d.ts +8 -0
- package/dist/server-overview-profile.js +62 -0
- package/dist/server-overview-routes.d.ts +4 -0
- package/dist/server-overview-routes.js +41 -0
- package/dist/shard-contracts.d.ts +66 -0
- package/dist/shard-contracts.js +23 -0
- package/dist/shard-coordinator.d.ts +1 -1
- package/dist/shard-router.d.ts +9 -0
- package/dist/shard-router.js +6 -0
- package/dist/shard-supervisor.d.ts +1 -1
- package/dist/shard-supervisor.js +4 -0
- package/dist/shard-worker.d.ts +1 -1
- package/dist/shard-worker.js +4 -0
- package/dist/verification-service.d.ts +4 -0
- package/dist/verification-service.js +10 -0
- package/documentation/README.md +1 -0
- package/documentation/guides/help-command.md +62 -0
- package/package.json +6 -6
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHmac, randomBytes, randomUUID, } from "node:crypto";
|
|
2
|
+
import { mkdir, lstat, readFile, writeFile, rename, rm, chmod, } from "node:fs/promises";
|
|
3
|
+
import { resolve, dirname } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export const HELP_API_ORIGIN = "https://api.helyx.gg";
|
|
6
|
+
const credentialSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
installationId: z.string().uuid(),
|
|
9
|
+
token: z.string().regex(/^hhp_[A-Za-z0-9_-]{43}$/u),
|
|
10
|
+
expiresAt: z.string().datetime(),
|
|
11
|
+
})
|
|
12
|
+
.strict();
|
|
13
|
+
export const parseHelpCredential = (value) => credentialSchema.parse(value);
|
|
14
|
+
const storedSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
schemaVersion: z.literal(1),
|
|
17
|
+
installationId: z.string().uuid(),
|
|
18
|
+
expiresAt: z.string().datetime(),
|
|
19
|
+
iv: z.string().regex(/^[a-f0-9]{24}$/u),
|
|
20
|
+
tag: z.string().regex(/^[a-f0-9]{32}$/u),
|
|
21
|
+
ciphertext: z.string().regex(/^[a-f0-9]{94}$/u),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
export class HelpCredentialError extends Error {
|
|
25
|
+
constructor() {
|
|
26
|
+
super("The private Help credential could not be read or saved. Check the local data-encryption key and file permissions.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const missing = (error) => !!error &&
|
|
30
|
+
typeof error === "object" &&
|
|
31
|
+
"code" in error &&
|
|
32
|
+
error.code === "ENOENT";
|
|
33
|
+
/** Encrypted, opt-in file; separate from Cloud Connect and every provider secret. */
|
|
34
|
+
export class HelpCredentialStore {
|
|
35
|
+
path;
|
|
36
|
+
key;
|
|
37
|
+
constructor(directory, dataKey) {
|
|
38
|
+
this.path = resolve(directory, ".helyx/help-credential.json");
|
|
39
|
+
const decoded = Buffer.from(dataKey, "base64");
|
|
40
|
+
if (decoded.length !== 32)
|
|
41
|
+
throw new HelpCredentialError();
|
|
42
|
+
this.key = createHmac("sha256", decoded)
|
|
43
|
+
.update("helyx.help.credential.v1")
|
|
44
|
+
.digest();
|
|
45
|
+
}
|
|
46
|
+
aad(id, expiresAt) {
|
|
47
|
+
return Buffer.from(`helyx.help.credential.v1:${id}:${expiresAt}`);
|
|
48
|
+
}
|
|
49
|
+
async checkDirectory() {
|
|
50
|
+
try {
|
|
51
|
+
const stat = await lstat(dirname(this.path));
|
|
52
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
53
|
+
throw new HelpCredentialError();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (!missing(error))
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async read() {
|
|
61
|
+
try {
|
|
62
|
+
await this.checkDirectory();
|
|
63
|
+
let stat;
|
|
64
|
+
try {
|
|
65
|
+
stat = await lstat(this.path);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (missing(error))
|
|
69
|
+
return null;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
|
|
73
|
+
throw new HelpCredentialError();
|
|
74
|
+
const value = storedSchema.parse(JSON.parse(await readFile(this.path, "utf8"))), decipher = createDecipheriv("aes-256-gcm", this.key, Buffer.from(value.iv, "hex"));
|
|
75
|
+
decipher.setAAD(this.aad(value.installationId, value.expiresAt));
|
|
76
|
+
decipher.setAuthTag(Buffer.from(value.tag, "hex"));
|
|
77
|
+
const token = Buffer.concat([
|
|
78
|
+
decipher.update(Buffer.from(value.ciphertext, "hex")),
|
|
79
|
+
decipher.final(),
|
|
80
|
+
]).toString("utf8");
|
|
81
|
+
return credentialSchema.parse({
|
|
82
|
+
installationId: value.installationId,
|
|
83
|
+
expiresAt: value.expiresAt,
|
|
84
|
+
token,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new HelpCredentialError();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async save(input) {
|
|
92
|
+
const temporary = this.path + `.${randomUUID()}.tmp`;
|
|
93
|
+
try {
|
|
94
|
+
const credential = credentialSchema.parse(input);
|
|
95
|
+
await this.checkDirectory();
|
|
96
|
+
await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
97
|
+
await this.checkDirectory();
|
|
98
|
+
await chmod(dirname(this.path), 0o700);
|
|
99
|
+
const existing = await lstat(this.path).catch((error) => {
|
|
100
|
+
if (missing(error))
|
|
101
|
+
return null;
|
|
102
|
+
throw error;
|
|
103
|
+
});
|
|
104
|
+
if (existing && (!existing.isFile() || existing.isSymbolicLink()))
|
|
105
|
+
throw new HelpCredentialError();
|
|
106
|
+
const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", this.key, iv);
|
|
107
|
+
cipher.setAAD(this.aad(credential.installationId, credential.expiresAt));
|
|
108
|
+
const ciphertext = Buffer.concat([
|
|
109
|
+
cipher.update(credential.token, "utf8"),
|
|
110
|
+
cipher.final(),
|
|
111
|
+
]);
|
|
112
|
+
await writeFile(temporary, JSON.stringify({
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
installationId: credential.installationId,
|
|
115
|
+
expiresAt: credential.expiresAt,
|
|
116
|
+
iv: iv.toString("hex"),
|
|
117
|
+
tag: cipher.getAuthTag().toString("hex"),
|
|
118
|
+
ciphertext: ciphertext.toString("hex"),
|
|
119
|
+
}) + "\n", { flag: "wx", mode: 0o600 });
|
|
120
|
+
await rename(temporary, this.path);
|
|
121
|
+
await chmod(this.path, 0o600);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new HelpCredentialError();
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async remove() {
|
|
131
|
+
try {
|
|
132
|
+
await this.checkDirectory();
|
|
133
|
+
const stat = await lstat(this.path).catch((error) => {
|
|
134
|
+
if (missing(error))
|
|
135
|
+
return null;
|
|
136
|
+
throw error;
|
|
137
|
+
});
|
|
138
|
+
if (!stat)
|
|
139
|
+
return;
|
|
140
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
141
|
+
throw new HelpCredentialError();
|
|
142
|
+
await rm(this.path);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new HelpCredentialError();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=help-credentials.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ModuleDefinition } from "@helyx/sdk";
|
|
2
|
+
import { type HelpRuntimeCollection, type HelpRuntimeResult, type HelpArea } from "@helyx/core";
|
|
3
|
+
export interface HelpTransport {
|
|
4
|
+
query(input: {
|
|
5
|
+
question: string;
|
|
6
|
+
module?: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
guildId?: string;
|
|
9
|
+
}): Promise<HelpRuntimeResult & {
|
|
10
|
+
choiceToken?: string;
|
|
11
|
+
}>;
|
|
12
|
+
choose(input: {
|
|
13
|
+
choiceToken: string;
|
|
14
|
+
faqId: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
guildId?: string;
|
|
17
|
+
}): Promise<HelpRuntimeResult>;
|
|
18
|
+
close?(): void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** Platform-owned FAQ lookup. Local Help needs no account, network or platform service. */
|
|
21
|
+
export declare function createHelpModule(options?: {
|
|
22
|
+
deployment?: "hosted" | "self-hosted";
|
|
23
|
+
transport?: HelpTransport;
|
|
24
|
+
collection?: HelpRuntimeCollection;
|
|
25
|
+
areas?: readonly HelpArea[];
|
|
26
|
+
}): ModuleDefinition;
|
|
27
|
+
//# sourceMappingURL=help-module.d.ts.map
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { escapeMarkdown } from "discord.js";
|
|
3
|
+
import { defineModule, } from "@helyx/sdk";
|
|
4
|
+
import { BUNDLED_HELP_COLLECTION, BUNDLED_HELP_AREAS, BUNDLED_HELP_RUNTIME_COLLECTION, HELP_DOCS_URL, helpRuntimeFromLegacy, parseHelpRuntimeCollection, helpLexicalResult, } from "@helyx/core";
|
|
5
|
+
import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
6
|
+
const prefix = "helyx.help.choice.";
|
|
7
|
+
const everyone = {
|
|
8
|
+
everyone: true,
|
|
9
|
+
requiredPermissions: [],
|
|
10
|
+
allowedRoleIds: [],
|
|
11
|
+
deniedRoleIds: [],
|
|
12
|
+
allowedUserIds: [],
|
|
13
|
+
deniedUserIds: [],
|
|
14
|
+
};
|
|
15
|
+
const safeLabel = (text, max) => text
|
|
16
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
17
|
+
.slice(0, max)
|
|
18
|
+
.replace(/[\uD800-\uDBFF]$/u, "");
|
|
19
|
+
const fold = (text) => text.normalize("NFKC").toLowerCase().trim();
|
|
20
|
+
const text = (content) => ({
|
|
21
|
+
ephemeral: true,
|
|
22
|
+
allowedRoleMentionIds: [],
|
|
23
|
+
allowedUserMentionIds: [],
|
|
24
|
+
componentsV2: { accentColor: 0x8b5cf6, text: [{ content }] },
|
|
25
|
+
});
|
|
26
|
+
/** Platform-owned FAQ lookup. Local Help needs no account, network or platform service. */
|
|
27
|
+
export function createHelpModule(options = {}) {
|
|
28
|
+
const packaged = options.collection ?? BUNDLED_HELP_RUNTIME_COLLECTION;
|
|
29
|
+
const collection = packaged
|
|
30
|
+
? parseHelpRuntimeCollection(packaged)
|
|
31
|
+
: helpRuntimeFromLegacy(BUNDLED_HELP_COLLECTION.faqs);
|
|
32
|
+
const areas = [...(options.areas ?? BUNDLED_HELP_AREAS)].sort((a, b) => Number(a.code) - Number(b.code));
|
|
33
|
+
const choices = new Map(), rates = new Map();
|
|
34
|
+
function admit(userId) {
|
|
35
|
+
const current = rates.get(userId);
|
|
36
|
+
if (current) {
|
|
37
|
+
if (current.count >= 5)
|
|
38
|
+
return false;
|
|
39
|
+
current.count++;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (rates.size >= 2000)
|
|
43
|
+
return false;
|
|
44
|
+
const timer = setTimeout(() => rates.delete(userId), 60000);
|
|
45
|
+
timer.unref();
|
|
46
|
+
rates.set(userId, { count: 1, timer });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const identity = (context) => ({
|
|
50
|
+
userId: context.userId,
|
|
51
|
+
...(context.serverId ? { guildId: context.serverId } : {}),
|
|
52
|
+
});
|
|
53
|
+
async function present(context, result, local = false, question) {
|
|
54
|
+
const freshness = local
|
|
55
|
+
? `Local Help · bundled documentation ${collection.revision.slice(0, 12)}`
|
|
56
|
+
: result.degraded
|
|
57
|
+
? "Stored Help answer · reduced matching"
|
|
58
|
+
: "Stored Help answer";
|
|
59
|
+
if (result.kind === "invalid") {
|
|
60
|
+
await context.reply(text("Keep your question to 600 characters."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (result.kind === "no_match") {
|
|
64
|
+
await context.reply(text(`I couldn’t find a reliable answer. Try naming the module or rewording your question.\n\n[Browse Helyx documentation](${HELP_DOCS_URL}).`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (result.kind === "answer") {
|
|
68
|
+
await answer(context, result.faq, freshness);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!result.faqs.length ||
|
|
72
|
+
result.faqs.length > 3 ||
|
|
73
|
+
(!local && !result.choiceToken)) {
|
|
74
|
+
await context.reply(text(`Help is temporarily unavailable. [Browse the documentation](${HELP_DOCS_URL}).`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (choices.size >= 1000) {
|
|
78
|
+
await context.reply(text("Help is busy. Try your question again in a moment."));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const nonce = randomUUID(), timer = setTimeout(() => choices.delete(nonce), 120000);
|
|
82
|
+
timer.unref();
|
|
83
|
+
choices.set(nonce, {
|
|
84
|
+
expiresAt: Date.now() + 120000,
|
|
85
|
+
userId: context.userId,
|
|
86
|
+
serverId: context.serverId ?? null,
|
|
87
|
+
ids: result.faqs.map((faq) => faq.id),
|
|
88
|
+
revision: result.revision,
|
|
89
|
+
token: local ? null : result.choiceToken,
|
|
90
|
+
timer,
|
|
91
|
+
});
|
|
92
|
+
const introduction = result.faqs.length > 1
|
|
93
|
+
? "I’ve found a few questions that match or are similar to yours:"
|
|
94
|
+
: "I’ve found a question that may match yours:";
|
|
95
|
+
const userQuestionText = question
|
|
96
|
+
? `\n\n${escapeMarkdown(question.trim())}`
|
|
97
|
+
: "";
|
|
98
|
+
await context.reply({
|
|
99
|
+
...text(`${introduction}${userQuestionText}`),
|
|
100
|
+
buttons: result.faqs.map((faq) => ({
|
|
101
|
+
type: "button",
|
|
102
|
+
customId: `${prefix}${nonce}.${faq.id}`,
|
|
103
|
+
label: safeLabel(faq.source.kind === "custom"
|
|
104
|
+
? faq.question
|
|
105
|
+
: `${faq.areaName} · ${faq.question}`, 80),
|
|
106
|
+
style: "secondary",
|
|
107
|
+
})),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function answer(context, faq, freshness) {
|
|
111
|
+
const heading = `## ${safeLabel(faq.question, 600)}`;
|
|
112
|
+
const source = faq.source.kind === "website" ? `[Read this FAQ](${faq.source.url})` : "";
|
|
113
|
+
const notice = freshness.startsWith("Local Help")
|
|
114
|
+
? "\n\n-# Offline documentation snapshot"
|
|
115
|
+
: freshness.includes("reduced matching")
|
|
116
|
+
? "\n\n-# Reduced matching"
|
|
117
|
+
: "";
|
|
118
|
+
const footer = `\n\n-# ID:${faq.id}`;
|
|
119
|
+
const content = `${heading}\n\n${faq.answerMarkdown}${faq.context.length ? "\n\n" + faq.context.join("\n\n") : ""}${source ? "\n\n" + source : ""}${notice}${footer}`;
|
|
120
|
+
if (faq.source.kind === "website" && content.length > 7000) {
|
|
121
|
+
await context.reply(text(`${heading}\n\nThis stored answer and its context are too long for Discord. ${source}${notice}${footer}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Send the entire stored answer; do not silently cut a warning or negation.
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let rest = content;
|
|
127
|
+
while (rest.length) {
|
|
128
|
+
let end = Math.min(3500, rest.length);
|
|
129
|
+
if (end < rest.length && /[\uD800-\uDBFF]/u.test(rest[end - 1]))
|
|
130
|
+
end--;
|
|
131
|
+
chunks.push(rest.slice(0, end));
|
|
132
|
+
rest = rest.slice(end);
|
|
133
|
+
}
|
|
134
|
+
for (const [index, chunk] of chunks.entries())
|
|
135
|
+
await context.reply(text(chunks.length > 1
|
|
136
|
+
? `${chunk}\n\n(${index + 1}/${chunks.length})`
|
|
137
|
+
: chunk));
|
|
138
|
+
}
|
|
139
|
+
return defineModule({
|
|
140
|
+
manifest: {
|
|
141
|
+
...INTERNAL_MODULE_MANIFEST_FIELDS,
|
|
142
|
+
releaseStatus: "live",
|
|
143
|
+
id: "helyx.help",
|
|
144
|
+
name: "Helyx Help",
|
|
145
|
+
version: "0.1.0",
|
|
146
|
+
description: "Always-available help from approved Helyx FAQs.",
|
|
147
|
+
entry: "./dist/help-module.js",
|
|
148
|
+
framework: "^0.1.0",
|
|
149
|
+
dependencies: [],
|
|
150
|
+
permissions: [],
|
|
151
|
+
contributions: { commands: ["help"], events: [] },
|
|
152
|
+
},
|
|
153
|
+
defaultPermissions: { module: everyone, commands: { help: everyone } },
|
|
154
|
+
interactions: {
|
|
155
|
+
commands: [
|
|
156
|
+
{
|
|
157
|
+
name: "help",
|
|
158
|
+
permissionId: "help",
|
|
159
|
+
description: "Find an answer in the Helyx FAQs",
|
|
160
|
+
registrationPolicy: "platform-global",
|
|
161
|
+
acknowledgement: "deferred-ephemeral",
|
|
162
|
+
authorization: "public",
|
|
163
|
+
contexts: ["guild", "bot_dm"],
|
|
164
|
+
integrationTypes: ["guild_install"],
|
|
165
|
+
options: [
|
|
166
|
+
{
|
|
167
|
+
type: "string",
|
|
168
|
+
name: "question",
|
|
169
|
+
description: "Your question about Helyx",
|
|
170
|
+
maxLength: 600,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: "string",
|
|
174
|
+
name: "module",
|
|
175
|
+
description: "Optional module or documentation area",
|
|
176
|
+
autocomplete: true,
|
|
177
|
+
maxLength: 200,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
async autocomplete(context) {
|
|
181
|
+
const value = fold(context.focusedValue);
|
|
182
|
+
await context.respond(context.focusedOptionName === "module"
|
|
183
|
+
? areas
|
|
184
|
+
.filter((area) => fold(`${area.code} ${area.name} ${area.area}`).includes(value))
|
|
185
|
+
.slice(0, 25)
|
|
186
|
+
.map((area) => ({
|
|
187
|
+
name: safeLabel(`${area.code} · ${area.name}`, 100),
|
|
188
|
+
value: area.code,
|
|
189
|
+
}))
|
|
190
|
+
: []);
|
|
191
|
+
},
|
|
192
|
+
async execute(context) {
|
|
193
|
+
const question = context.getStringOption("question"), moduleInput = context.getStringOption("module");
|
|
194
|
+
if (!question?.trim()) {
|
|
195
|
+
await context.reply(text(`Ask a question about Helyx, with an optional module or area.\n\nExample: \`/help question: The command is not visible module: Feedback\`\n\nHelp returns stored FAQ answers. [Browse the documentation](${HELP_DOCS_URL}).`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (question.length > 600 ||
|
|
199
|
+
!question.isWellFormed() ||
|
|
200
|
+
/[\p{Cc}\p{Cf}]/u.test(question.replace(/[\r\n\t]/gu, ""))) {
|
|
201
|
+
await context.reply(text("Use a question of at most 600 characters with ordinary readable text."));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const area = moduleInput
|
|
205
|
+
? areas.find((area) => [area.code, area.area, area.name, `helyx.${area.area}`].some((value) => fold(value) === fold(moduleInput)))
|
|
206
|
+
: undefined;
|
|
207
|
+
if (moduleInput && !area) {
|
|
208
|
+
await context.reply(text("Choose a module or area from the suggestions, or enter its three-digit number."));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!admit(context.userId)) {
|
|
212
|
+
await context.reply(text("You’ve reached Help’s short limit. Try again in a minute."));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (options.transport)
|
|
216
|
+
try {
|
|
217
|
+
await present(context, await options.transport.query({
|
|
218
|
+
question,
|
|
219
|
+
...(area ? { module: area.code } : {}),
|
|
220
|
+
...identity(context),
|
|
221
|
+
}), false, question);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
if (options.deployment === "hosted") {
|
|
226
|
+
await context.reply(text(`Online Help is temporarily unavailable. [Browse the documentation](${HELP_DOCS_URL}) or try again shortly.`));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
await present(context, helpLexicalResult(collection.faqs, question, collection.revision, area?.code), true, question);
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
buttons: [
|
|
235
|
+
{
|
|
236
|
+
customIdPrefix: prefix,
|
|
237
|
+
acknowledgement: "deferred-update",
|
|
238
|
+
authorization: "public",
|
|
239
|
+
async execute(context) {
|
|
240
|
+
const parts = /^helyx\.help\.choice\.([a-f0-9-]{36})\.([1-9]\d{5})$/u.exec(context.customId ?? ""), state = parts ? choices.get(parts[1]) : undefined;
|
|
241
|
+
if (!parts ||
|
|
242
|
+
!state ||
|
|
243
|
+
state.expiresAt <= Date.now() ||
|
|
244
|
+
state.userId !== context.userId ||
|
|
245
|
+
state.serverId !== (context.serverId ?? null) ||
|
|
246
|
+
!state.ids.includes(parts[2])) {
|
|
247
|
+
await context.reply(text("This selection is unavailable. Ask `/help` again."));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
choices.delete(parts[1]);
|
|
251
|
+
clearTimeout(state.timer);
|
|
252
|
+
if (state.token && options.transport)
|
|
253
|
+
try {
|
|
254
|
+
await present(context, await options.transport.choose({
|
|
255
|
+
choiceToken: state.token,
|
|
256
|
+
faqId: parts[2],
|
|
257
|
+
...identity(context),
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
await context.reply(text("This answer changed or the selection expired. Ask `/help` again."));
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const faq = state.revision === collection.revision
|
|
265
|
+
? collection.faqs.find((faq) => faq.id === parts[2])
|
|
266
|
+
: undefined;
|
|
267
|
+
if (!faq)
|
|
268
|
+
await context.reply(text("This selection expired. Ask `/help` again."));
|
|
269
|
+
else
|
|
270
|
+
await answer(context, faq, `Local Help · bundled documentation ${collection.revision.slice(0, 12)}`);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
start: () => Promise.resolve(),
|
|
277
|
+
stop: async () => {
|
|
278
|
+
for (const choice of choices.values())
|
|
279
|
+
clearTimeout(choice.timer);
|
|
280
|
+
choices.clear();
|
|
281
|
+
for (const rate of rates.values())
|
|
282
|
+
clearTimeout(rate.timer);
|
|
283
|
+
rates.clear();
|
|
284
|
+
await options.transport?.close?.();
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=help-module.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { BotEnvironment } from "@helyx/config";
|
|
2
|
+
export declare function createRuntimeHelpModule(environment: Pick<BotEnvironment, "HELYX_RUNTIME_PROFILE" | "HELYX_DATA_ENCRYPTION_KEY" | "HELYX_CLOUD_API_URL" | "HELYX_BOT_CONTROL_SECRET">): import("@helyx/sdk").ModuleDefinition;
|
|
3
|
+
//# sourceMappingURL=help-runtime.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createHelpModule } from "./help-module.js";
|
|
3
|
+
import { HelpCredentialStore } from "./help-credentials.js";
|
|
4
|
+
import { FileHelpTransport, HttpHelpTransport, HelpTransportError, } from "./help-transport.js";
|
|
5
|
+
export function createRuntimeHelpModule(environment) {
|
|
6
|
+
let botVersion = "unknown";
|
|
7
|
+
try {
|
|
8
|
+
const value = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
|
+
if (typeof value.version === "string" &&
|
|
10
|
+
/^[A-Za-z0-9.+_-]{1,50}$/u.test(value.version))
|
|
11
|
+
botVersion = value.version;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* Packaging validation checks the real package metadata separately. */
|
|
15
|
+
}
|
|
16
|
+
let transport;
|
|
17
|
+
if (environment.HELYX_RUNTIME_PROFILE === "hosted") {
|
|
18
|
+
try {
|
|
19
|
+
if (!environment.HELYX_BOT_CONTROL_SECRET)
|
|
20
|
+
throw new HelpTransportError();
|
|
21
|
+
transport = new HttpHelpTransport({
|
|
22
|
+
kind: "hosted",
|
|
23
|
+
token: environment.HELYX_BOT_CONTROL_SECRET,
|
|
24
|
+
baseUrl: environment.HELYX_CLOUD_API_URL,
|
|
25
|
+
botVersion,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
transport = {
|
|
30
|
+
query: () => Promise.reject(new HelpTransportError()),
|
|
31
|
+
choose: () => Promise.reject(new HelpTransportError()),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (environment.HELYX_DATA_ENCRYPTION_KEY)
|
|
36
|
+
transport = new FileHelpTransport(new HelpCredentialStore(process.cwd(), environment.HELYX_DATA_ENCRYPTION_KEY), botVersion);
|
|
37
|
+
return createHelpModule({
|
|
38
|
+
deployment: environment.HELYX_RUNTIME_PROFILE,
|
|
39
|
+
...(transport ? { transport } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=help-runtime.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type HelpRuntimeResult } from "@helyx/core";
|
|
2
|
+
import { type HelpCredentialStore } from "./help-credentials.js";
|
|
3
|
+
import type { HelpTransport } from "./help-module.js";
|
|
4
|
+
export declare class HelpTransportError extends Error {
|
|
5
|
+
readonly code: "unavailable" | "expired_choice";
|
|
6
|
+
constructor(code?: "unavailable" | "expired_choice");
|
|
7
|
+
}
|
|
8
|
+
export declare class HttpHelpTransport implements HelpTransport {
|
|
9
|
+
private readonly options;
|
|
10
|
+
private blocked;
|
|
11
|
+
private blockedUntil;
|
|
12
|
+
private failures;
|
|
13
|
+
private readonly origin;
|
|
14
|
+
private readonly controller;
|
|
15
|
+
constructor(options: {
|
|
16
|
+
token: string;
|
|
17
|
+
kind: "hosted" | "self-host";
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
botVersion: string;
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
});
|
|
22
|
+
query(input: Parameters<HelpTransport["query"]>[0]): Promise<HelpRuntimeResult & {
|
|
23
|
+
choiceToken?: string;
|
|
24
|
+
}>;
|
|
25
|
+
choose(input: Parameters<HelpTransport["choose"]>[0]): Promise<HelpRuntimeResult & {
|
|
26
|
+
choiceToken?: string;
|
|
27
|
+
}>;
|
|
28
|
+
close(): void;
|
|
29
|
+
private request;
|
|
30
|
+
}
|
|
31
|
+
/** Read per request so local disconnect/replacement takes effect without a restart. */
|
|
32
|
+
export declare class FileHelpTransport implements HelpTransport {
|
|
33
|
+
private readonly store;
|
|
34
|
+
private readonly botVersion;
|
|
35
|
+
private readonly fetcher?;
|
|
36
|
+
private current;
|
|
37
|
+
constructor(store: HelpCredentialStore, botVersion: string, fetcher?: typeof fetch | undefined);
|
|
38
|
+
private transport;
|
|
39
|
+
query(input: Parameters<HelpTransport["query"]>[0]): Promise<HelpRuntimeResult & {
|
|
40
|
+
choiceToken?: string;
|
|
41
|
+
}>;
|
|
42
|
+
choose(input: Parameters<HelpTransport["choose"]>[0]): Promise<HelpRuntimeResult & {
|
|
43
|
+
choiceToken?: string;
|
|
44
|
+
}>;
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function exchangeHelpEnrolment(code: string, fetcher?: typeof fetch): Promise<{
|
|
48
|
+
installationId: string;
|
|
49
|
+
token: string;
|
|
50
|
+
expiresAt: string;
|
|
51
|
+
}>;
|
|
52
|
+
export declare function revokeHelpCredential(token: string, fetcher?: typeof fetch): Promise<void>;
|
|
53
|
+
//# sourceMappingURL=help-transport.d.ts.map
|