@usebridger/core 0.2.1 → 0.3.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/dist/adapter.d.mts +1 -1
- package/dist/index.d.mts +58 -39
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +344 -44
- package/dist/index.mjs.map +1 -1
- package/dist/{reconnection-BGpkqq4C.d.mts → reconnection-COfwSj9L.d.mts} +91 -52
- package/dist/reconnection-COfwSj9L.d.mts.map +1 -0
- package/package.json +1 -1
- package/dist/reconnection-BGpkqq4C.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { a as sessionStorage, i as pluginStorage, n as assertTenantId, o as shar
|
|
|
3
3
|
import { b as isCapability, c as normalizeQuery, g as assertCapability, h as assertCanSend, l as normalizeWhere, m as UnsupportedError, o as normalizeDocument, r as transportIssues, s as normalizeIndexes, t as ReconnectionPolicy, u as createMessage, v as groupActionCapability, x as missingCapabilities, y as hasCapability } from "./reconnection--5ulo2FJ.mjs";
|
|
4
4
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { pino, stdSerializers } from "pino";
|
|
6
|
+
import { styleText } from "node:util";
|
|
6
7
|
import { existsSync } from "node:fs";
|
|
7
8
|
import { readdir } from "node:fs/promises";
|
|
8
9
|
import { extname, join, resolve } from "node:path";
|
|
@@ -10,7 +11,7 @@ import { pathToFileURL } from "node:url";
|
|
|
10
11
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
12
|
//#region src/text/format.ts
|
|
12
13
|
/** Monta o `FormattedText`; os nós já vêm validados e congelados. */
|
|
13
|
-
function formatted(nodes) {
|
|
14
|
+
function formatted$1(nodes) {
|
|
14
15
|
return Object.freeze({
|
|
15
16
|
type: "formatted",
|
|
16
17
|
nodes: Object.freeze([...nodes])
|
|
@@ -30,8 +31,8 @@ function isFormatted(value) {
|
|
|
30
31
|
}
|
|
31
32
|
function style(type, parts) {
|
|
32
33
|
const children = nodesOf(parts, type);
|
|
33
|
-
if (children.length === 0) return formatted([]);
|
|
34
|
-
return formatted([Object.freeze({
|
|
34
|
+
if (children.length === 0) return formatted$1([]);
|
|
35
|
+
return formatted$1([Object.freeze({
|
|
35
36
|
type,
|
|
36
37
|
children: Object.freeze(children)
|
|
37
38
|
})]);
|
|
@@ -45,8 +46,8 @@ function italic(...parts) {
|
|
|
45
46
|
/** Código em linha. O conteúdo é sempre literal: não aceita outra formatação dentro. */
|
|
46
47
|
function code(text) {
|
|
47
48
|
if (typeof text !== "string") throw new TypeError("code: esperava string");
|
|
48
|
-
if (text === "") return formatted([]);
|
|
49
|
-
return formatted([Object.freeze({
|
|
49
|
+
if (text === "") return formatted$1([]);
|
|
50
|
+
return formatted$1([Object.freeze({
|
|
50
51
|
type: "code",
|
|
51
52
|
text
|
|
52
53
|
})]);
|
|
@@ -64,7 +65,7 @@ function link(url, label) {
|
|
|
64
65
|
}
|
|
65
66
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new TypeError(`link: só http e https são aceitos: ${url}`);
|
|
66
67
|
const children = nodesOf([label ?? url], "link");
|
|
67
|
-
return formatted([Object.freeze({
|
|
68
|
+
return formatted$1([Object.freeze({
|
|
68
69
|
type: "link",
|
|
69
70
|
url,
|
|
70
71
|
children: Object.freeze(children.length ? children : [url])
|
|
@@ -82,7 +83,7 @@ function mention(contact) {
|
|
|
82
83
|
phone: contact.phone ?? null,
|
|
83
84
|
...contact.username !== void 0 && { username: contact.username }
|
|
84
85
|
});
|
|
85
|
-
return formatted([Object.freeze({
|
|
86
|
+
return formatted$1([Object.freeze({
|
|
86
87
|
type: "mention",
|
|
87
88
|
contact: target
|
|
88
89
|
})]);
|
|
@@ -100,7 +101,7 @@ function fmt(strings, ...values) {
|
|
|
100
101
|
parts.push(typeof value === "number" ? String(value) : value);
|
|
101
102
|
}
|
|
102
103
|
});
|
|
103
|
-
return formatted(nodesOf(parts, "fmt"));
|
|
104
|
+
return formatted$1(nodesOf(parts, "fmt"));
|
|
104
105
|
}
|
|
105
106
|
/**
|
|
106
107
|
* Texto visível, sem marcação: o que a pessoa lê. Uma menção vira `@` mais o usuário, o nome, o
|
|
@@ -311,6 +312,41 @@ function createPrefixStore(config, port) {
|
|
|
311
312
|
}
|
|
312
313
|
//#endregion
|
|
313
314
|
//#region src/commands/command.ts
|
|
315
|
+
/** Decompõe a entrada sem validar; a validação fica em `assertCommandDefinition`. */
|
|
316
|
+
function parseAccept(spec) {
|
|
317
|
+
const quoted = spec.startsWith("quoted:");
|
|
318
|
+
const rest = quoted ? spec.slice(7) : spec;
|
|
319
|
+
const colon = rest.indexOf(":");
|
|
320
|
+
if (colon === -1) return {
|
|
321
|
+
quoted,
|
|
322
|
+
type: rest,
|
|
323
|
+
mimetype: null
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
quoted,
|
|
327
|
+
type: rest.slice(0, colon),
|
|
328
|
+
mimetype: rest.slice(colon + 1).toLowerCase()
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* O mimetype da mídia casa com o da entrada. Compara só a essência: `audio/ogg; codecs=opus`
|
|
333
|
+
* casa com `audio/ogg`, porque os parâmetros variam por plataforma.
|
|
334
|
+
*/
|
|
335
|
+
function mimetypeMatches(mimetype, pattern) {
|
|
336
|
+
const essence = mimetype.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
337
|
+
if (pattern.endsWith("/*")) return essence.startsWith(pattern.slice(0, -1));
|
|
338
|
+
return essence === pattern;
|
|
339
|
+
}
|
|
340
|
+
const MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
341
|
+
"image",
|
|
342
|
+
"video",
|
|
343
|
+
"audio",
|
|
344
|
+
"voice",
|
|
345
|
+
"sticker",
|
|
346
|
+
"document"
|
|
347
|
+
]);
|
|
348
|
+
/** `tipo/subtipo` ou `tipo/*`, nos caracteres que o RFC 6838 permite. */
|
|
349
|
+
const MIMETYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/(\*|[a-z0-9][a-z0-9!#$&^_.+-]*)$/;
|
|
314
350
|
const TOKEN = /^\S+$/;
|
|
315
351
|
function assertToken(token, what) {
|
|
316
352
|
if (!TOKEN.test(token)) throw new TypeError(`${what} de comando inválido: ${JSON.stringify(token)} (vazio ou com espaço)`);
|
|
@@ -318,16 +354,26 @@ function assertToken(token, what) {
|
|
|
318
354
|
/**
|
|
319
355
|
* Lança `TypeError` se o nome ou algum alias for vazio ou tiver espaço (nunca casaria) e
|
|
320
356
|
* `RangeError` se `timeoutMs` não for finito e > 0 (o `setTimeout` trataria `Infinity`/`NaN`
|
|
321
|
-
* como 1 ms e o comando estouraria na hora)
|
|
357
|
+
* como 1 ms e o comando estouraria na hora) ou se `concurrency` não for inteiro >= 1 (0 nunca
|
|
358
|
+
* rodaria). Entrada de `accepts` com mimetype mal formado, ou num tipo sem mídia, e capability
|
|
359
|
+
* desconhecida em `requires` também lançam `TypeError`.
|
|
322
360
|
*/
|
|
323
361
|
function assertCommandDefinition(definition) {
|
|
324
362
|
assertToken(definition.name, "Nome");
|
|
325
363
|
for (const alias of definition.aliases ?? []) assertToken(alias, "Alias");
|
|
326
364
|
const { timeoutMs } = definition;
|
|
327
365
|
if (timeoutMs !== void 0 && !(Number.isFinite(timeoutMs) && timeoutMs > 0)) throw new RangeError(`Prazo do comando "${definition.name}" inválido: ${timeoutMs}`);
|
|
366
|
+
const { concurrency } = definition;
|
|
367
|
+
if (concurrency !== void 0 && !(Number.isInteger(concurrency) && concurrency >= 1)) throw new RangeError(`Limite de concorrência do comando "${definition.name}" inválido: ${concurrency}`);
|
|
368
|
+
for (const spec of definition.accepts ?? []) {
|
|
369
|
+
const { type, mimetype } = parseAccept(spec);
|
|
370
|
+
if (mimetype === null) continue;
|
|
371
|
+
if (!MEDIA_TYPES.has(type) || !MIMETYPE.test(mimetype)) throw new TypeError(`accepts do comando "${definition.name}" inválido: ${JSON.stringify(spec)} (mimetype só em tipo com mídia, como "tipo/subtipo" ou "tipo/*")`);
|
|
372
|
+
}
|
|
373
|
+
for (const capability of definition.requires ?? []) if (!isCapability(capability)) throw new TypeError(`requires do comando "${definition.name}": capability desconhecida ${JSON.stringify(capability)}`);
|
|
328
374
|
}
|
|
329
375
|
/**
|
|
330
|
-
* Define um comando. Valida nome, aliases e
|
|
376
|
+
* Define um comando. Valida nome, aliases, prazo e limite na definição, para o erro apontar o plugin que
|
|
331
377
|
* declarou e não aparecer só quando alguém digitar o comando.
|
|
332
378
|
*/
|
|
333
379
|
function command(definition) {
|
|
@@ -713,19 +759,41 @@ const FIRST_WHITESPACE = /\s/;
|
|
|
713
759
|
function mediaOf(message) {
|
|
714
760
|
return message !== null && "media" in message ? message.media : null;
|
|
715
761
|
}
|
|
716
|
-
/** Primeira entrada de `accepts` que casa, na ordem declarada. */
|
|
762
|
+
/** Primeira entrada de `accepts` que casa (tipo e, se houver, mimetype), na ordem declarada. */
|
|
717
763
|
function resolveAccepts(message, accepts) {
|
|
718
764
|
for (const spec of accepts) {
|
|
719
|
-
const quoted = spec
|
|
765
|
+
const { quoted, type, mimetype } = parseAccept(spec);
|
|
720
766
|
const target = quoted ? message.quoted : message;
|
|
721
|
-
|
|
722
|
-
if (
|
|
767
|
+
if (target === null || target.type !== type) continue;
|
|
768
|
+
if (mimetype !== null) {
|
|
769
|
+
const media = mediaOf(target);
|
|
770
|
+
if (media === null || !mimetypeMatches(media.mimetype, mimetype)) continue;
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
723
773
|
spec,
|
|
724
774
|
message: target
|
|
725
775
|
};
|
|
726
776
|
}
|
|
727
777
|
return null;
|
|
728
778
|
}
|
|
779
|
+
/** Roda o `run`; o erro, síncrono ou não, vira `failed`. Nunca rejeita. */
|
|
780
|
+
async function runCommand(definition, ctx, command) {
|
|
781
|
+
try {
|
|
782
|
+
await definition.run(ctx);
|
|
783
|
+
return {
|
|
784
|
+
consumed: true,
|
|
785
|
+
status: "ran",
|
|
786
|
+
command
|
|
787
|
+
};
|
|
788
|
+
} catch (error) {
|
|
789
|
+
return {
|
|
790
|
+
consumed: true,
|
|
791
|
+
status: "failed",
|
|
792
|
+
command,
|
|
793
|
+
error
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
}
|
|
729
797
|
function createCommandRouter(options = {}) {
|
|
730
798
|
const option = options.prefix ?? "!";
|
|
731
799
|
let prefixOf;
|
|
@@ -852,16 +920,17 @@ function createCommandRouter(options = {}) {
|
|
|
852
920
|
}
|
|
853
921
|
media = mediaOf(accepted.message);
|
|
854
922
|
} else media = mediaOf(message) ?? mediaOf(message.quoted);
|
|
855
|
-
const
|
|
923
|
+
const done = runCommand(definition, Object.assign(base, {
|
|
856
924
|
accepted,
|
|
857
925
|
media
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
return {
|
|
926
|
+
}), command);
|
|
927
|
+
if (definition.background) return {
|
|
861
928
|
consumed: true,
|
|
862
|
-
status: "
|
|
863
|
-
command
|
|
929
|
+
status: "background",
|
|
930
|
+
command,
|
|
931
|
+
done
|
|
864
932
|
};
|
|
933
|
+
return await done;
|
|
865
934
|
} catch (error) {
|
|
866
935
|
return {
|
|
867
936
|
consumed: true,
|
|
@@ -914,6 +983,71 @@ function normalizeOwners(owners) {
|
|
|
914
983
|
return [...unique.values()];
|
|
915
984
|
}
|
|
916
985
|
//#endregion
|
|
986
|
+
//#region src/logger/pretty.ts
|
|
987
|
+
const LEVELS = {
|
|
988
|
+
10: ["TRACE", "gray"],
|
|
989
|
+
20: ["DEBUG", "blue"],
|
|
990
|
+
30: ["INFO ", "green"],
|
|
991
|
+
40: ["WARN ", "yellow"],
|
|
992
|
+
50: ["ERROR", "red"],
|
|
993
|
+
60: ["FATAL", ["bgRed", "white"]]
|
|
994
|
+
};
|
|
995
|
+
/** Campos que o cabeçalho já mostra ou que não ajudam a ler no terminal. */
|
|
996
|
+
const HIDDEN = /* @__PURE__ */ new Set([
|
|
997
|
+
"level",
|
|
998
|
+
"time",
|
|
999
|
+
"pid",
|
|
1000
|
+
"hostname",
|
|
1001
|
+
"msg",
|
|
1002
|
+
"plugin",
|
|
1003
|
+
"err"
|
|
1004
|
+
]);
|
|
1005
|
+
/**
|
|
1006
|
+
* Embrulha `target` para receber texto legível em vez de JSON. `color` liga as cores ANSI; o
|
|
1007
|
+
* `styleText` ainda as desliga com `NO_COLOR` ou quando o stdout não é um terminal.
|
|
1008
|
+
*/
|
|
1009
|
+
function prettyDestination(target, color) {
|
|
1010
|
+
const paint = (style, text) => color ? styleText(style, text, { stream: process.stdout }) : text;
|
|
1011
|
+
return { write(line) {
|
|
1012
|
+
let record;
|
|
1013
|
+
try {
|
|
1014
|
+
record = JSON.parse(line);
|
|
1015
|
+
} catch {
|
|
1016
|
+
target.write(line);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
target.write(`${formatRecord(record, paint)}\n`);
|
|
1020
|
+
} };
|
|
1021
|
+
}
|
|
1022
|
+
function formatRecord(record, paint) {
|
|
1023
|
+
const [label, style] = LEVELS[record["level"]] ?? ["? ", "white"];
|
|
1024
|
+
const parts = [paint("gray", clock(record["time"])), paint(style, label)];
|
|
1025
|
+
if (typeof record["plugin"] === "string") parts.push(paint("cyan", `[${record["plugin"]}]`));
|
|
1026
|
+
parts.push(String(record["msg"] ?? ""));
|
|
1027
|
+
const fields = Object.entries(record).filter(([key]) => !HIDDEN.has(key)).map(([key, value]) => paint("gray", `${key}=${formatValue(value)}`));
|
|
1028
|
+
let text = [...parts, ...fields].join(" ");
|
|
1029
|
+
if (record["err"] !== void 0) text += `\n${indent(formatError(record["err"]))}`;
|
|
1030
|
+
return text;
|
|
1031
|
+
}
|
|
1032
|
+
/** Hora local `HH:MM:SS`; o dia fica de fora, o terminal é para acompanhar ao vivo. */
|
|
1033
|
+
function clock(time) {
|
|
1034
|
+
return (typeof time === "number" ? new Date(time) : /* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
|
|
1035
|
+
}
|
|
1036
|
+
function formatValue(value) {
|
|
1037
|
+
if (typeof value === "string") return /^[^\s"=]+$/.test(value) ? value : JSON.stringify(value);
|
|
1038
|
+
return JSON.stringify(value) ?? String(value);
|
|
1039
|
+
}
|
|
1040
|
+
/** Stack do erro e as das causas, em sequência; sem stack, `tipo: mensagem` ou o JSON. */
|
|
1041
|
+
function formatError(err) {
|
|
1042
|
+
if (typeof err !== "object" || err === null) return String(err);
|
|
1043
|
+
const { type, message, stack, cause } = err;
|
|
1044
|
+
const own = stack ?? (message === void 0 ? JSON.stringify(err) : `${type}: ${message}`);
|
|
1045
|
+
return cause === void 0 ? own : `${own}\ncausa: ${formatError(cause)}`;
|
|
1046
|
+
}
|
|
1047
|
+
function indent(text) {
|
|
1048
|
+
return text.replaceAll(/^/gm, " ");
|
|
1049
|
+
}
|
|
1050
|
+
//#endregion
|
|
917
1051
|
//#region src/logger/logger.ts
|
|
918
1052
|
const CENSOR = "[REDACTED]";
|
|
919
1053
|
/**
|
|
@@ -921,7 +1055,7 @@ const CENSOR = "[REDACTED]";
|
|
|
921
1055
|
* campo `err` é serializado com `message`, `stack` e `cause`.
|
|
922
1056
|
*/
|
|
923
1057
|
function createLogger(options = {}) {
|
|
924
|
-
const destination = withSecretsCensored(options
|
|
1058
|
+
const destination = withSecretsCensored(formatted(options), options.secrets);
|
|
925
1059
|
const root = pino({
|
|
926
1060
|
level: options.level ?? "info",
|
|
927
1061
|
serializers: { err: stdSerializers.errWithCause },
|
|
@@ -932,6 +1066,10 @@ function createLogger(options = {}) {
|
|
|
932
1066
|
}, destination);
|
|
933
1067
|
return wrap(options.bindings ? root.child(options.bindings) : root);
|
|
934
1068
|
}
|
|
1069
|
+
function formatted(options) {
|
|
1070
|
+
if (options.format !== "pretty") return options.destination;
|
|
1071
|
+
return prettyDestination(options.destination ?? pino.destination(1), !options.destination);
|
|
1072
|
+
}
|
|
935
1073
|
function withSecretsCensored(destination, secrets) {
|
|
936
1074
|
if (secrets === void 0) return destination;
|
|
937
1075
|
let source;
|
|
@@ -984,6 +1122,31 @@ function createNoopLogger() {
|
|
|
984
1122
|
return NOOP_LOGGER;
|
|
985
1123
|
}
|
|
986
1124
|
//#endregion
|
|
1125
|
+
//#region src/plugin/messages.ts
|
|
1126
|
+
/** Placeholder que o kernel preenche: o prefixo de comando do chat (ADR 0063). */
|
|
1127
|
+
const PREFIX_PLACEHOLDER = "prefix";
|
|
1128
|
+
const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
1129
|
+
/** Nomes dos placeholders do texto, na ordem em que aparecem, sem repetição. */
|
|
1130
|
+
function placeholders(text) {
|
|
1131
|
+
return [...new Set(Array.from(text.matchAll(PLACEHOLDER), (match) => match[1]))];
|
|
1132
|
+
}
|
|
1133
|
+
/** Preenche os placeholders de `text`; `key` só entra na mensagem de erro. */
|
|
1134
|
+
function fillMessage(text, values, key) {
|
|
1135
|
+
return text.replaceAll(PLACEHOLDER, (_match, name) => {
|
|
1136
|
+
const value = Object.hasOwn(values, name) ? values[name] : void 0;
|
|
1137
|
+
if (value === void 0) throw new TypeError(`mensagem "${key}": falta o valor do placeholder {${name}}`);
|
|
1138
|
+
return String(value);
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/** O `ctx.t`: a mensagem `key` das `messages` do plugin, com o `{prefix}` do chat. */
|
|
1142
|
+
function formatMessage(messages, key, values, prefix) {
|
|
1143
|
+
if (!Object.hasOwn(messages, key)) throw new TypeError(`mensagem "${key}" não declarada em messages`);
|
|
1144
|
+
return fillMessage(messages[key], {
|
|
1145
|
+
...values,
|
|
1146
|
+
[PREFIX_PLACEHOLDER]: prefix
|
|
1147
|
+
}, key);
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
987
1150
|
//#region src/config/schema.ts
|
|
988
1151
|
/** Texto que substitui um valor secreto em qualquer exibição (dashboard, `describe`). */
|
|
989
1152
|
const SECRET_MASK = "********";
|
|
@@ -1203,6 +1366,17 @@ var PluginConfigError = class extends Error {
|
|
|
1203
1366
|
//#endregion
|
|
1204
1367
|
//#region src/config/plugin-configs.ts
|
|
1205
1368
|
const MESSAGES_KEY = "messages";
|
|
1369
|
+
/**
|
|
1370
|
+
* Problema da sobrescrita que usa placeholder que o texto padrão não tem (o `{prefix}` vale
|
|
1371
|
+
* sempre); `null` se está tudo certo.
|
|
1372
|
+
*/
|
|
1373
|
+
function placeholderIssue(text, base) {
|
|
1374
|
+
const allowed = [...placeholders(base), PREFIX_PLACEHOLDER];
|
|
1375
|
+
const unknown = placeholders(text).filter((name) => !allowed.includes(name));
|
|
1376
|
+
if (unknown.length === 0) return null;
|
|
1377
|
+
const show = (names) => names.map((name) => `{${name}}`).join(", ");
|
|
1378
|
+
return `placeholder desconhecido ${show(unknown)}; a mensagem aceita ${show(allowed)}`;
|
|
1379
|
+
}
|
|
1206
1380
|
const SECRETS_OWNER = (plugin) => `plugin:${plugin}`;
|
|
1207
1381
|
const isPlainObject = (value) => {
|
|
1208
1382
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -1456,8 +1630,15 @@ function createPluginConfigs(options) {
|
|
|
1456
1630
|
message: "deve ser texto"
|
|
1457
1631
|
});
|
|
1458
1632
|
else {
|
|
1459
|
-
|
|
1460
|
-
|
|
1633
|
+
const problem = placeholderIssue(text, base[key]);
|
|
1634
|
+
if (problem !== null) issues.push({
|
|
1635
|
+
...at,
|
|
1636
|
+
message: problem
|
|
1637
|
+
});
|
|
1638
|
+
else {
|
|
1639
|
+
messages[key] = text;
|
|
1640
|
+
messageSources[at.path] = at.source;
|
|
1641
|
+
}
|
|
1461
1642
|
}
|
|
1462
1643
|
}
|
|
1463
1644
|
if (issues.length > 0) return {
|
|
@@ -2639,7 +2820,7 @@ const SEPARATORS = [
|
|
|
2639
2820
|
function splitText(text, limits, measure) {
|
|
2640
2821
|
if (measure(text) <= limits.first) return [text];
|
|
2641
2822
|
const raw = typeof text === "string";
|
|
2642
|
-
const build = (segments) => raw ? segments.map((s) => s.text).join("") : formatted(rebuild(segments, 0));
|
|
2823
|
+
const build = (segments) => raw ? segments.map((s) => s.text).join("") : formatted$1(rebuild(segments, 0));
|
|
2643
2824
|
const chunks = [];
|
|
2644
2825
|
const fits = (segments) => measure(build(trim(segments))) <= (chunks.length === 0 ? limits.first : limits.rest);
|
|
2645
2826
|
const pack = (segments, level) => {
|
|
@@ -3943,17 +4124,18 @@ function definePlugin(definition) {
|
|
|
3943
4124
|
* Não edite à mão: `pnpm version-packages` roda o `changeset version` e reescreve esta linha
|
|
3944
4125
|
* (`scripts/sync-version.ts`); o `version.test.ts` falha se ela divergir do `package.json`.
|
|
3945
4126
|
*/
|
|
3946
|
-
const CORE_VERSION = "0.
|
|
4127
|
+
const CORE_VERSION = "0.3.0";
|
|
3947
4128
|
//#endregion
|
|
3948
4129
|
//#region src/plugin/declared.ts
|
|
3949
4130
|
/** Registra os `commands` e o `on` do manifesto. Chamado pelo host antes do `setup`. */
|
|
3950
4131
|
function installDeclared(definition, ctx) {
|
|
3951
4132
|
for (const [name, declared] of Object.entries(definition.commands ?? {})) {
|
|
3952
|
-
const { run, ...rest } = declared;
|
|
4133
|
+
const { run, onReject, ...rest } = declared;
|
|
3953
4134
|
ctx.commands.add({
|
|
3954
4135
|
...rest,
|
|
3955
4136
|
name,
|
|
3956
|
-
run: (c) => run.call(declared, c, ctx)
|
|
4137
|
+
run: (c) => run.call(declared, c, ctx),
|
|
4138
|
+
...onReject && { onReject: (c, rejection) => onReject.call(declared, c, rejection, ctx) }
|
|
3957
4139
|
});
|
|
3958
4140
|
}
|
|
3959
4141
|
for (const [event, listener] of Object.entries(definition.on ?? {})) {
|
|
@@ -4141,7 +4323,7 @@ const reportBase = ({ definition, origin }) => ({
|
|
|
4141
4323
|
/** Cria o host de plugins. Não roda nada até `start()`. */
|
|
4142
4324
|
function createPluginHost(options) {
|
|
4143
4325
|
const { transport, createContext, log } = options;
|
|
4144
|
-
const coreVersion = options.coreVersion ?? "0.
|
|
4326
|
+
const coreVersion = options.coreVersion ?? "0.3.0";
|
|
4145
4327
|
if (!parseVersion(coreVersion)) throw new RangeError(`coreVersion inválida: "${coreVersion}"`);
|
|
4146
4328
|
const setupTimeoutMs = options.setupTimeoutMs ?? 1e4;
|
|
4147
4329
|
const teardownTimeoutMs = options.teardownTimeoutMs ?? 5e3;
|
|
@@ -5409,7 +5591,12 @@ var MessageContextImpl = class {
|
|
|
5409
5591
|
return state.log;
|
|
5410
5592
|
}
|
|
5411
5593
|
react(emoji) {
|
|
5412
|
-
|
|
5594
|
+
const { deps } = this[STATE];
|
|
5595
|
+
if (!deps.reactions) {
|
|
5596
|
+
this.log.debug("react ignorado: o transport não tem a capability \"reactions\"", { emoji });
|
|
5597
|
+
return Promise.resolve();
|
|
5598
|
+
}
|
|
5599
|
+
return deps.sender.react(this.message.key, emoji, { priority: "high" });
|
|
5413
5600
|
}
|
|
5414
5601
|
};
|
|
5415
5602
|
function replyFor(deps, message, owner) {
|
|
@@ -5554,12 +5741,28 @@ const signalDescriptor = { get() {
|
|
|
5554
5741
|
return deadlineOf(this).signal;
|
|
5555
5742
|
} };
|
|
5556
5743
|
/**
|
|
5557
|
-
*
|
|
5744
|
+
* Descritor do `t`: a função presa à mensagem da visão, criada no primeiro acesso e fixada, para
|
|
5745
|
+
* `({ t }) => ...` funcionar desestruturado.
|
|
5746
|
+
*/
|
|
5747
|
+
function textDescriptor(text) {
|
|
5748
|
+
return {
|
|
5749
|
+
configurable: true,
|
|
5750
|
+
get() {
|
|
5751
|
+
const { message } = this;
|
|
5752
|
+
const t = (key, values) => text(message, key, values);
|
|
5753
|
+
Object.defineProperty(this, "t", { value: t });
|
|
5754
|
+
return t;
|
|
5755
|
+
}
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
/**
|
|
5759
|
+
* Descritores comuns às execuções que respondem a uma mensagem (comando e passo): `log` e `t` do
|
|
5558
5760
|
* plugin, `signal`, e `reply`, `react` e `expectReply` presos ao prazo.
|
|
5559
5761
|
*/
|
|
5560
|
-
function executionDescriptors(plugin, pluginLog, expect, scope) {
|
|
5762
|
+
function executionDescriptors(plugin, pluginLog, expect, text, scope) {
|
|
5561
5763
|
return {
|
|
5562
5764
|
log: pluginLogDescriptor(pluginLog),
|
|
5765
|
+
t: textDescriptor(text),
|
|
5563
5766
|
signal: signalDescriptor,
|
|
5564
5767
|
reply: expiringReplyDescriptor(plugin, (view) => pluginReply(view, plugin) ?? Object.getPrototypeOf(view).reply, scope),
|
|
5565
5768
|
react: expiringReactDescriptor(plugin, (view) => {
|
|
@@ -5570,12 +5773,12 @@ function executionDescriptors(plugin, pluginLog, expect, scope) {
|
|
|
5570
5773
|
};
|
|
5571
5774
|
}
|
|
5572
5775
|
/** Deriva os contextos de comando de um plugin. */
|
|
5573
|
-
function commandViewFactory(plugin, pluginLog, expect) {
|
|
5776
|
+
function commandViewFactory(plugin, pluginLog, expect, text) {
|
|
5574
5777
|
const scope = (view) => ({
|
|
5575
5778
|
label: `comando "${view.command}"`,
|
|
5576
5779
|
fields: { command: view.command }
|
|
5577
5780
|
});
|
|
5578
|
-
const runDescriptors = executionDescriptors(plugin, pluginLog, expect, scope);
|
|
5781
|
+
const runDescriptors = executionDescriptors(plugin, pluginLog, expect, text, scope);
|
|
5579
5782
|
return { run(ctx, deadline) {
|
|
5580
5783
|
const view = Object.create(ctx, runDescriptors);
|
|
5581
5784
|
view[DEADLINE] = deadline;
|
|
@@ -5586,12 +5789,12 @@ function commandViewFactory(plugin, pluginLog, expect) {
|
|
|
5586
5789
|
* Deriva, para os passos de conversa de um plugin, o contexto com `step`, `data` e os campos de
|
|
5587
5790
|
* execução do comando (`log`, `signal`, `reply`, `react`, `expectReply`).
|
|
5588
5791
|
*/
|
|
5589
|
-
function stepViewFactory(plugin, pluginLog, expect) {
|
|
5792
|
+
function stepViewFactory(plugin, pluginLog, expect, text) {
|
|
5590
5793
|
const scope = (view) => ({
|
|
5591
5794
|
label: `passo "${view.step}"`,
|
|
5592
5795
|
fields: { step: view.step }
|
|
5593
5796
|
});
|
|
5594
|
-
const descriptors = executionDescriptors(plugin, pluginLog, expect, scope);
|
|
5797
|
+
const descriptors = executionDescriptors(plugin, pluginLog, expect, text, scope);
|
|
5595
5798
|
return (ctx, deadline, step, data) => {
|
|
5596
5799
|
const view = Object.create(ctx, descriptors);
|
|
5597
5800
|
Object.assign(view, {
|
|
@@ -5623,7 +5826,7 @@ function roleViewFactory(pluginLog) {
|
|
|
5623
5826
|
* compartilhados entre os listeners da emissão; o barramento pendura na visão o `Deadline` do
|
|
5624
5827
|
* listener, que dá o `signal` e prende o `reply`.
|
|
5625
5828
|
*/
|
|
5626
|
-
function listenerViewFactory(plugin, pluginLog, expect) {
|
|
5829
|
+
function listenerViewFactory(plugin, pluginLog, expect, text) {
|
|
5627
5830
|
const kernel = (view) => view[KERNEL_CONTEXT];
|
|
5628
5831
|
const scope = (view) => ({
|
|
5629
5832
|
label: `listener de "${view.event}"`,
|
|
@@ -5643,11 +5846,65 @@ function listenerViewFactory(plugin, pluginLog, expect) {
|
|
|
5643
5846
|
}, scope),
|
|
5644
5847
|
react: expiringReactDescriptor(plugin, kernel, scope),
|
|
5645
5848
|
expectReply: expectReplyDescriptor(plugin, expect, scope),
|
|
5646
|
-
log: pluginLogDescriptor(pluginLog)
|
|
5849
|
+
log: pluginLogDescriptor(pluginLog),
|
|
5850
|
+
t: textDescriptor(text)
|
|
5647
5851
|
};
|
|
5648
5852
|
return (ctx) => Object.create(ctx, descriptors);
|
|
5649
5853
|
}
|
|
5650
5854
|
//#endregion
|
|
5855
|
+
//#region src/commands/concurrency.ts
|
|
5856
|
+
function createConcurrencyLimit(max) {
|
|
5857
|
+
let active = 0;
|
|
5858
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
5859
|
+
const release = () => {
|
|
5860
|
+
const next = waiting.values().next();
|
|
5861
|
+
if (next.done) {
|
|
5862
|
+
active--;
|
|
5863
|
+
return;
|
|
5864
|
+
}
|
|
5865
|
+
waiting.delete(next.value);
|
|
5866
|
+
next.value();
|
|
5867
|
+
};
|
|
5868
|
+
const start = (task) => {
|
|
5869
|
+
let result;
|
|
5870
|
+
try {
|
|
5871
|
+
result = task();
|
|
5872
|
+
} catch (error) {
|
|
5873
|
+
release();
|
|
5874
|
+
throw error;
|
|
5875
|
+
}
|
|
5876
|
+
if (isThenable$2(result)) result.then(release, release);
|
|
5877
|
+
else release();
|
|
5878
|
+
return result;
|
|
5879
|
+
};
|
|
5880
|
+
return { run(task, signal) {
|
|
5881
|
+
if (active < max) {
|
|
5882
|
+
active++;
|
|
5883
|
+
return start(task);
|
|
5884
|
+
}
|
|
5885
|
+
return new Promise((resolve, reject) => {
|
|
5886
|
+
if (signal.aborted) {
|
|
5887
|
+
reject(signal.reason);
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
const onAbort = () => {
|
|
5891
|
+
waiting.delete(go);
|
|
5892
|
+
reject(signal.reason);
|
|
5893
|
+
};
|
|
5894
|
+
const go = () => {
|
|
5895
|
+
signal.removeEventListener("abort", onAbort);
|
|
5896
|
+
try {
|
|
5897
|
+
resolve(start(task));
|
|
5898
|
+
} catch (error) {
|
|
5899
|
+
reject(error);
|
|
5900
|
+
}
|
|
5901
|
+
};
|
|
5902
|
+
waiting.add(go);
|
|
5903
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5904
|
+
});
|
|
5905
|
+
} };
|
|
5906
|
+
}
|
|
5907
|
+
//#endregion
|
|
5651
5908
|
//#region src/storage/tenant.ts
|
|
5652
5909
|
/**
|
|
5653
5910
|
* Storage do plugin que segue o `scope`. Os storages por tenant ficam em cache (as coleções do
|
|
@@ -5741,9 +5998,10 @@ function createPluginContextFactory(deps) {
|
|
|
5741
5998
|
};
|
|
5742
5999
|
const live = (operation, fn) => (...args) => lifetime.expired ? refuse(operation) : fn(...args);
|
|
5743
6000
|
const expect = (message, step, options) => deps.conversations.expect(name, message.chat.id, message.sender.id, step, options);
|
|
5744
|
-
const
|
|
5745
|
-
const
|
|
5746
|
-
const
|
|
6001
|
+
const text = (message, key, values) => formatMessage(messages, key, values, deps.prefixes.get(message.chat));
|
|
6002
|
+
const commandViews = commandViewFactory(name, log, expect, text);
|
|
6003
|
+
const listenerView = listenerViewFactory(name, log, expect, text);
|
|
6004
|
+
const stepView = stepViewFactory(name, log, expect, text);
|
|
5747
6005
|
const roleView = roleViewFactory(log);
|
|
5748
6006
|
const events = deps.bus.forPlugin(name, {
|
|
5749
6007
|
view: (event) => isMessageEvent(event) ? listenerView : void 0,
|
|
@@ -5766,6 +6024,15 @@ function createPluginContextFactory(deps) {
|
|
|
5766
6024
|
commands: {
|
|
5767
6025
|
add(definition) {
|
|
5768
6026
|
guard("commands.add");
|
|
6027
|
+
assertCommandDefinition(definition);
|
|
6028
|
+
const missing = missingCapabilities(deps.transport, definition.requires ?? []);
|
|
6029
|
+
if (missing.length > 0) {
|
|
6030
|
+
log.info(`comando "${definition.name}" não registrado: capability ausente no transport`, {
|
|
6031
|
+
command: definition.name,
|
|
6032
|
+
missing
|
|
6033
|
+
});
|
|
6034
|
+
return;
|
|
6035
|
+
}
|
|
5769
6036
|
deps.router.registry.add(name, wrapCommand(name, definition, commandViews, lifetime, deps));
|
|
5770
6037
|
},
|
|
5771
6038
|
list: () => deps.router.registry.list().map(commandInfo)
|
|
@@ -5918,19 +6185,26 @@ function liveTenantStorage(storage, live) {
|
|
|
5918
6185
|
function wrapCommand(plugin, definition, views, lifetime, deps) {
|
|
5919
6186
|
const { run, onReject, name } = definition;
|
|
5920
6187
|
const timeoutMs = definition.timeoutMs ?? deps.commandTimeoutMs;
|
|
6188
|
+
const limit = definition.concurrency === void 0 ? void 0 : createConcurrencyLimit(definition.concurrency);
|
|
5921
6189
|
const timed = (stage, execute) => {
|
|
5922
6190
|
const deadline = new Deadline(lifetime);
|
|
5923
6191
|
return settleWithin(execute(deadline), timeoutMs, () => {
|
|
5924
6192
|
const error = new CommandTimeoutError(plugin, name, timeoutMs, stage);
|
|
5925
6193
|
deadline.expire(error);
|
|
5926
6194
|
return error;
|
|
5927
|
-
}, (error) =>
|
|
6195
|
+
}, (error) => {
|
|
6196
|
+
if (error === deadline.reason) return;
|
|
6197
|
+
deps.onLateCommandError(plugin, name, error);
|
|
6198
|
+
}, deps.armed, deadline);
|
|
5928
6199
|
};
|
|
5929
6200
|
return {
|
|
5930
6201
|
...definition,
|
|
5931
6202
|
run: (ctx) => timed("run", (deadline) => {
|
|
5932
|
-
const
|
|
5933
|
-
|
|
6203
|
+
const start = () => {
|
|
6204
|
+
const view = views.run(ctx, deadline);
|
|
6205
|
+
return replyWithResult(view, run(view));
|
|
6206
|
+
};
|
|
6207
|
+
return limit === void 0 ? start() : limit.run(start, deadline.signal);
|
|
5934
6208
|
}),
|
|
5935
6209
|
...onReject && { onReject: (ctx, rejection) => timed("onReject", (deadline) => onReject(views.run(ctx, deadline), rejection)) }
|
|
5936
6210
|
};
|
|
@@ -6391,6 +6665,19 @@ function createBot(config) {
|
|
|
6391
6665
|
const timeouts = config.timeouts ?? {};
|
|
6392
6666
|
const commandTimeoutMs = timeouts.commandMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
6393
6667
|
const armed = new ArmedTimers();
|
|
6668
|
+
const background = /* @__PURE__ */ new Set();
|
|
6669
|
+
const backgroundWaiters = [];
|
|
6670
|
+
const releaseBackgroundWaiters = () => {
|
|
6671
|
+
for (const resolve of backgroundWaiters.splice(0)) resolve();
|
|
6672
|
+
};
|
|
6673
|
+
const trackBackground = (task) => {
|
|
6674
|
+
background.add(task);
|
|
6675
|
+
task.finally(() => {
|
|
6676
|
+
background.delete(task);
|
|
6677
|
+
if (background.size === 0) releaseBackgroundWaiters();
|
|
6678
|
+
});
|
|
6679
|
+
};
|
|
6680
|
+
const backgroundIdle = () => background.size === 0 ? Promise.resolve() : new Promise((resolve) => backgroundWaiters.push(resolve));
|
|
6394
6681
|
const bus = createEventBus({
|
|
6395
6682
|
listenerTimeoutMs: timeouts.listenerMs,
|
|
6396
6683
|
armed,
|
|
@@ -6454,6 +6741,7 @@ function createBot(config) {
|
|
|
6454
6741
|
const contextDeps = {
|
|
6455
6742
|
sender: send,
|
|
6456
6743
|
quote: hasCapability(transport, "quoted"),
|
|
6744
|
+
reactions: hasCapability(transport, "reactions"),
|
|
6457
6745
|
log: getLog,
|
|
6458
6746
|
actions: prepareActions
|
|
6459
6747
|
};
|
|
@@ -6537,6 +6825,14 @@ function createBot(config) {
|
|
|
6537
6825
|
/** Recusa, falha e evento `command` do comando que consumiu a mensagem ou o clique. */
|
|
6538
6826
|
async function afterCommand(ctx, result) {
|
|
6539
6827
|
const { message } = ctx;
|
|
6828
|
+
if (result.status === "background") {
|
|
6829
|
+
trackBackground(result.done.then((outcome) => afterCommand(ctx, outcome)).catch((error) => ctx.log.error("falha ao concluir o comando em segundo plano", {
|
|
6830
|
+
plugin: result.command.plugin,
|
|
6831
|
+
command: result.command.name,
|
|
6832
|
+
err: error
|
|
6833
|
+
})));
|
|
6834
|
+
return;
|
|
6835
|
+
}
|
|
6540
6836
|
if (result.status === "rejected" && result.reply !== null) ctx.reply(result.reply).catch((error) => ctx.log.warn("falha ao enviar a recusa do comando", {
|
|
6541
6837
|
plugin: result.command.plugin,
|
|
6542
6838
|
command: result.command.name,
|
|
@@ -6848,6 +7144,8 @@ function createBot(config) {
|
|
|
6848
7144
|
await scheduler.stop(aborted);
|
|
6849
7145
|
await host?.stop(aborted);
|
|
6850
7146
|
armed.disarmAll();
|
|
7147
|
+
background.clear();
|
|
7148
|
+
releaseBackgroundWaiters();
|
|
6851
7149
|
conversations.close();
|
|
6852
7150
|
actions.clear();
|
|
6853
7151
|
outbound.close({ drain: false });
|
|
@@ -6944,6 +7242,7 @@ function createBot(config) {
|
|
|
6944
7242
|
try {
|
|
6945
7243
|
log = config.logger ?? createLogger({
|
|
6946
7244
|
level: config.logLevel ?? "info",
|
|
7245
|
+
...config.logFormat && { format: config.logFormat },
|
|
6947
7246
|
secrets
|
|
6948
7247
|
});
|
|
6949
7248
|
transportLog = log.child({ transport: transport.name });
|
|
@@ -7047,8 +7346,9 @@ function createBot(config) {
|
|
|
7047
7346
|
*/
|
|
7048
7347
|
async function settled() {
|
|
7049
7348
|
if (state === "starting") await readyPromise;
|
|
7050
|
-
while (!(inbound.stats().activeChats === 0 && bus.idle && outbound.stats().activeChats === 0)) {
|
|
7349
|
+
while (!(inbound.stats().activeChats === 0 && background.size === 0 && bus.idle && outbound.stats().activeChats === 0)) {
|
|
7051
7350
|
await inbound.onIdle();
|
|
7351
|
+
await backgroundIdle();
|
|
7052
7352
|
await bus.onIdle();
|
|
7053
7353
|
await outbound.onIdle();
|
|
7054
7354
|
}
|