@coffer-org/plugin-telegram 1.3.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import type { LoadOpts } from './config.ts';
2
+ export declare function stripHtml(s: string): string;
2
3
  export type StartOpts = LoadOpts;
3
4
  export declare function startBot(opts?: StartOpts): Promise<boolean>;
4
5
  export declare function stopBot(): Promise<void>;
@@ -1,13 +1,44 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { TelegramClient, Api } from 'telegram';
4
- import { LogLevel } from 'telegram/extensions/Logger.js';
5
- import { StringSession } from 'telegram/sessions/index.js';
6
- import { NewMessage } from 'telegram/events/index.js';
7
- import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
3
+ import { TelegramClient, Api } from 'teleproto';
4
+ import { LogLevel } from 'teleproto/extensions/Logger.js';
5
+ import { StringSession } from 'teleproto/sessions/index.js';
6
+ import { NewMessage } from 'teleproto/events/index.js';
7
+ import { handleIncoming, loadGatePolicy } from '@coffer-org/plugin-orchestrator/runtime';
8
+ import { recordUser, recordAssistant, buildChain } from "./chain-store.js";
8
9
  import { loadBotConfig, hasCredentials } from "./config.js";
9
10
  import { getLogger } from '@coffer-org/sdk/logger';
10
11
  const log = getLogger('telegram');
12
+ const FALLBACK_REPLY_WINDOW_MS = 1_800_000;
13
+ const lastByChat = new Map();
14
+ async function replyWindowMs() {
15
+ try {
16
+ return (await loadGatePolicy()).replyWindow * 1000;
17
+ }
18
+ catch (err) {
19
+ log.warn(`gate policy load: ${err instanceof Error ? err.message : String(err)} — using default reply window`);
20
+ return FALLBACK_REPLY_WINDOW_MS;
21
+ }
22
+ }
23
+ export function stripHtml(s) {
24
+ return s
25
+ .replace(/<[^>]*>/g, '')
26
+ .replace(/&lt;/g, '<')
27
+ .replace(/&gt;/g, '>')
28
+ .replace(/&quot;/g, '"')
29
+ .replace(/&#39;/g, "'")
30
+ .replace(/&amp;/g, '&');
31
+ }
32
+ const TELEGRAM_FORMAT = [
33
+ 'CRITICAL OUTPUT FORMAT — Telegram HTML ONLY. Format the reply strictly with Telegram\'s HTML subset (<b>, <i>, <code>, <a>, …) and NOTHING else. NEVER use Markdown — no *bold*, _italic_, **bold**, `code`, ```blocks```, # headings, - bullets, or | tables. Markdown does not render here; it shows up as literal characters and breaks the message. When in doubt, use an HTML tag or plain text.',
34
+ 'Inline styling (tags may nest): <b>bold</b>, <i>italic</i>, <u>underline</u>, <s>strike</s>, <code>inline code</code>, <spoiler>spoiler</spoiler>. Links: <a href="https://…">text</a> (also <a href="tg://user?id=123">mention</a>).',
35
+ 'Blocks: <pre>code block</pre> or, for syntax highlight, <pre><code class="language-python">…</code></pre>. Quotes: <blockquote>quote</blockquote>, or <blockquote expandable>long collapsible quote</blockquote>. Bullet lists: start plain lines with "• ".',
36
+ 'Only those tags are parsed. NOT supported here (do not use): <tg-spoiler>, <span>, <ins>, <strike>, <tg-time> — use <spoiler>, <u>, <s> instead. Avoid <tg-emoji> (needs a custom-emoji id + Premium); use ordinary emoji.',
37
+ 'DO NOT use markdown (*, _, #, ```), tables, or images. Escape literal <, >, and & in prose as &lt;, &gt;, and &amp; so they are not read as tags.',
38
+ 'Render any TABLE as a <pre> block with space-aligned columns — real tables are unsupported.',
39
+ 'Beyond the tags, use Unicode symbols and emoji to structure and enrich replies — they render everywhere: bullets (•, ▪, –), separators (─────), arrows (→), status marks (✅ ⚠️ ❌ ⏳), and topic emoji (📅 💰 🏥 🛒 🌱 …). They make messages scannable and friendly.',
40
+ 'Keep replies short and skimmable (messages cap at 4096 chars): prefer short paragraphs and "• " bullet lists over long prose.',
41
+ ].join('\n');
11
42
  function loadSessionString(file) {
12
43
  try {
13
44
  return fs.readFileSync(file, 'utf-8');
@@ -46,36 +77,62 @@ export async function startBot(opts = {}) {
46
77
  const me = (await tg.getMe());
47
78
  const meId = me?.id?.toString();
48
79
  log.info(`connected as @${me?.username || me?.firstName || meId}`);
49
- async function sendMd(peer, text) {
80
+ async function sendHtml(peer, text, replyTo) {
50
81
  try {
51
- return await tg.sendMessage(peer, { message: text, parseMode: 'md' });
82
+ return await tg.sendMessage(peer, { message: text, parseMode: 'html', replyTo });
52
83
  }
53
84
  catch {
54
85
  try {
55
- return await tg.sendMessage(peer, { message: text });
86
+ return await tg.sendMessage(peer, { message: stripHtml(text), replyTo });
56
87
  }
57
88
  catch {
58
89
  return undefined;
59
90
  }
60
91
  }
61
92
  }
62
- async function editMd(peer, msgId, text) {
93
+ async function editHtml(peer, msgId, text) {
63
94
  try {
64
- await tg.editMessage(peer, { message: msgId, text, parseMode: 'md' });
95
+ await tg.editMessage(peer, { message: msgId, text, parseMode: 'html' });
65
96
  }
66
97
  catch {
67
98
  try {
68
- await tg.editMessage(peer, { message: msgId, text });
99
+ await tg.editMessage(peer, { message: msgId, text: stripHtml(text) });
69
100
  }
70
101
  catch {
71
102
  }
72
103
  }
73
104
  }
74
105
  function startTypingPeer(peer) {
75
- const ping = () => tg.invoke(new Api.messages.SetTyping({ peer, action: new Api.SendMessageTypingAction() })).catch(() => { });
76
- void ping();
77
- const timer = setInterval(() => void ping(), 5000);
78
- return () => clearInterval(timer);
106
+ let logged = false;
107
+ let stopped = false;
108
+ let timer;
109
+ const warn = (err) => {
110
+ if (logged)
111
+ return;
112
+ logged = true;
113
+ log.warn(`typing indicator: ${err instanceof Error ? err.message : String(err)}`);
114
+ };
115
+ void (async () => {
116
+ let input;
117
+ try {
118
+ input = await tg.getInputEntity(peer);
119
+ }
120
+ catch (err) {
121
+ warn(err);
122
+ return;
123
+ }
124
+ if (stopped)
125
+ return;
126
+ const ping = () => tg
127
+ .invoke(new Api.messages.SetTyping({ peer: input, action: new Api.SendMessageTypingAction() }))
128
+ .catch(warn);
129
+ void ping();
130
+ timer = setInterval(() => void ping(), 5000);
131
+ })();
132
+ return () => {
133
+ stopped = true;
134
+ clearInterval(timer);
135
+ };
79
136
  }
80
137
  tg.addEventHandler(async (event) => {
81
138
  try {
@@ -100,31 +157,78 @@ export async function startBot(opts = {}) {
100
157
  catch {
101
158
  }
102
159
  log.info(`msg [${chatLabel}]: ${text.slice(0, 80)}`);
103
- const chatId = String(event.chatId ?? '?');
104
- const userId = message.senderId?.toString() ?? '?';
105
- const replyTo = message.replyTo?.replyToMsgId ?? null;
106
- const connector = {
107
- id: 'telegram',
108
- capabilities: { streaming: true, typing: true, maxMessageLength: 4096 },
109
- async sendMessage(_chatId, msgText) {
110
- const sent = await sendMd(peer, msgText);
111
- if (!sent)
160
+ const stopTyping = startTypingPeer(peer);
161
+ try {
162
+ const chatId = String(event.chatId ?? '?');
163
+ const userId = message.senderId?.toString() ?? '?';
164
+ const incomingMsgId = String(message.id);
165
+ const replyTo = message.replyTo?.replyToMsgId ?? null;
166
+ let replyToId = replyTo != null ? String(replyTo) : null;
167
+ const nowMs = Date.now();
168
+ if (!replyToId) {
169
+ const prev = lastByChat.get(chatId);
170
+ if (prev && nowMs - prev.ts < (await replyWindowMs()))
171
+ replyToId = prev.msgId;
172
+ }
173
+ lastByChat.set(chatId, { msgId: incomingMsgId, ts: nowMs });
174
+ let displayName;
175
+ try {
176
+ const s = (await message.getSender());
177
+ displayName = s?.firstName || s?.username || undefined;
178
+ }
179
+ catch {
180
+ }
181
+ await recordUser({ chatId, msgId: incomingMsgId, sender: displayName, text, ts: message.date ?? 0, replyToId });
182
+ const fetch = async (id) => {
183
+ try {
184
+ const arr = (await tg.getMessages(peer, { ids: [Number(id)] }));
185
+ const m = arr?.[0];
186
+ if (!m)
187
+ return null;
188
+ return {
189
+ text: m.message ?? '',
190
+ senderId: m.senderId?.toString() ?? '',
191
+ replyToId: m.replyTo?.replyToMsgId?.toString() ?? null,
192
+ date: m.date ?? 0,
193
+ };
194
+ }
195
+ catch {
112
196
  return null;
113
- return { msgId: String(sent.id) };
114
- },
115
- async editMessage(_chatId, msgId, msgText) {
116
- await editMd(peer, Number(msgId), msgText);
117
- },
118
- startTyping(_chatId) {
119
- return startTypingPeer(peer);
120
- },
121
- };
122
- await handleIncoming(connector, {
123
- chatId,
124
- userId,
125
- text,
126
- replyToId: replyTo != null ? String(replyTo) : null,
127
- });
197
+ }
198
+ };
199
+ const messages = await buildChain(incomingMsgId, { chatId, fetch, botId: meId ?? '' });
200
+ const connector = {
201
+ id: 'telegram',
202
+ capabilities: { streaming: true, typing: true, maxMessageLength: 4096 },
203
+ async sendMessage(_chatId, msgText) {
204
+ const sent = await sendHtml(peer, msgText, Number(incomingMsgId));
205
+ if (!sent)
206
+ return null;
207
+ return { msgId: String(sent.id) };
208
+ },
209
+ async editMessage(_chatId, editId, msgText) {
210
+ await editHtml(peer, Number(editId), msgText);
211
+ },
212
+ startTyping(_chatId) {
213
+ return () => { };
214
+ },
215
+ async recordAssistant(m) {
216
+ await recordAssistant({ ...m, chatId, now: Math.floor(Date.now() / 1000) });
217
+ if (m.botMsgId)
218
+ lastByChat.set(chatId, { msgId: m.botMsgId, ts: Date.now() });
219
+ },
220
+ };
221
+ await handleIncoming(connector, {
222
+ connectorId: 'telegram',
223
+ chatId,
224
+ channelSystem: TELEGRAM_FORMAT,
225
+ sender: { id: userId, displayName },
226
+ messages,
227
+ });
228
+ }
229
+ finally {
230
+ stopTyping();
231
+ }
128
232
  }
129
233
  catch (err) {
130
234
  log.error(`message handling: ${err instanceof Error ? err.message : String(err)}`);
@@ -0,0 +1,28 @@
1
+ import type { ConvMessage } from '@coffer-org/plugin-orchestrator/runtime';
2
+ export type FetchMsg = (msgId: string) => Promise<{
3
+ text: string;
4
+ senderId: string;
5
+ replyToId: string | null;
6
+ date: number;
7
+ } | null>;
8
+ export declare function recordUser(m: {
9
+ chatId: string;
10
+ msgId: string;
11
+ sender?: string | null;
12
+ text: string;
13
+ ts: number;
14
+ replyToId: string | null;
15
+ }): Promise<void>;
16
+ export declare function recordAssistant(m: {
17
+ chatId: string;
18
+ parentMsgId: string | null;
19
+ botMsgId: string | null;
20
+ text: string;
21
+ now: number;
22
+ }): Promise<void>;
23
+ export declare function buildChain(headMsgId: string, opts: {
24
+ chatId: string;
25
+ fetch: FetchMsg;
26
+ botId: string;
27
+ maxDepth?: number;
28
+ }): Promise<ConvMessage[]>;
@@ -0,0 +1,46 @@
1
+ import { getThreadMessage, putThreadMessage } from '@coffer-org/server/thread-store';
2
+ import { getLogger } from '@coffer-org/sdk/logger';
3
+ const log = getLogger('telegram');
4
+ const CONNECTOR = 'telegram';
5
+ const DEFAULT_MAX_DEPTH = 30;
6
+ export async function recordUser(m) {
7
+ await putThreadMessage({ connector: CONNECTOR, chatId: m.chatId, msgId: m.msgId, role: 'user', sender: m.sender ?? null, text: m.text, ts: m.ts, replyToId: m.replyToId });
8
+ }
9
+ export async function recordAssistant(m) {
10
+ if (m.botMsgId == null)
11
+ return;
12
+ await putThreadMessage({
13
+ connector: CONNECTOR,
14
+ chatId: m.chatId,
15
+ msgId: m.botMsgId,
16
+ role: 'assistant',
17
+ text: m.text,
18
+ ts: m.now,
19
+ replyToId: m.parentMsgId,
20
+ });
21
+ }
22
+ export async function buildChain(headMsgId, opts) {
23
+ const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
24
+ const acc = [];
25
+ let cur = headMsgId;
26
+ const seen = new Set();
27
+ while (cur && acc.length < max && !seen.has(cur)) {
28
+ seen.add(cur);
29
+ let stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
30
+ if (!stored) {
31
+ const fetched = await opts.fetch(cur);
32
+ if (!fetched)
33
+ break;
34
+ const role = fetched.senderId === opts.botId ? 'assistant' : 'user';
35
+ await putThreadMessage({ connector: CONNECTOR, chatId: opts.chatId, msgId: cur, role, sender: null, text: fetched.text, ts: fetched.date, replyToId: fetched.replyToId });
36
+ stored = { msgId: cur, role, sender: null, text: fetched.text, ts: fetched.date, replyToId: fetched.replyToId };
37
+ }
38
+ acc.push({ role: stored.role, content: stored.text, sender: stored.sender, msgId: stored.msgId, ts: stored.ts });
39
+ cur = stored.replyToId;
40
+ }
41
+ if (acc.length >= max)
42
+ log.info(`chain capped at ${max} for ${headMsgId}`);
43
+ while (acc.length && acc.at(-1)?.role !== 'user')
44
+ acc.pop();
45
+ return acc.reverse();
46
+ }
@@ -1,7 +1,9 @@
1
1
  import { getPluginSettings } from '@coffer-org/server/plugin-runtime';
2
+ import { pruneThreadMessages } from '@coffer-org/server/thread-store';
2
3
  import { startBot, stopBot } from "./bot.js";
3
4
  import { getLogger } from '@coffer-org/sdk/logger';
4
5
  const log = getLogger('telegram');
6
+ const THREAD_TTL_MS = Number(process.env['TELEGRAM_THREAD_TTL_MS'] ?? 7 * 86_400_000);
5
7
  export { startBot, stopBot };
6
8
  export { loadBotConfig, hasCredentials } from "./config.js";
7
9
  export const serverHooks = {
@@ -12,4 +14,13 @@ export const serverHooks = {
12
14
  teardown: async () => {
13
15
  await stopBot();
14
16
  },
17
+ backgroundTasks: [
18
+ {
19
+ name: 'telegram-thread-prune',
20
+ intervalMs: 86_400_000,
21
+ run: async () => {
22
+ await pruneThreadMessages('telegram', Math.floor((Date.now() - THREAD_TTL_MS) / 1000));
23
+ },
24
+ },
25
+ ],
15
26
  };
package/dist/schema.js CHANGED
@@ -21,14 +21,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- //#region ../sdk/dist/plugin.js
24
+ //#region ../sdk/src/plugin.ts
25
25
  function definePlugin(p) {
26
26
  if (!p.id) throw new Error("[plugin] missing id");
27
27
  if (!p.version) console.warn(`[plugin] ${p.id}: missing version`);
28
28
  return p;
29
29
  }
30
30
  //#endregion
31
- //#region ../sdk/dist/settings.js
31
+ //#region ../sdk/src/settings.ts
32
32
  function defineSettings(s) {
33
33
  return s;
34
34
  }
@@ -154,7 +154,10 @@ function assignProp(target, prop, value) {
154
154
  }
155
155
  function mergeDefs(...defs) {
156
156
  const mergedDescriptors = {};
157
- for (const def of defs) Object.assign(mergedDescriptors, Object.getOwnPropertyDescriptors(def));
157
+ for (const def of defs) {
158
+ const descriptors = Object.getOwnPropertyDescriptors(def);
159
+ Object.assign(mergedDescriptors, descriptors);
160
+ }
158
161
  return Object.defineProperties({}, mergedDescriptors);
159
162
  }
160
163
  function esc(str) {
@@ -476,7 +479,7 @@ var _parse = (_Err) => (schema, value, _ctx, _params) => {
476
479
  }, ctx);
477
480
  if (result instanceof Promise) throw new $ZodAsyncError();
478
481
  if (result.issues.length) {
479
- const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
482
+ const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
480
483
  captureStackTrace(e, _params?.callee);
481
484
  throw e;
482
485
  }
@@ -493,7 +496,7 @@ var _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
493
496
  }, ctx);
494
497
  if (result instanceof Promise) result = await result;
495
498
  if (result.issues.length) {
496
- const e = new (params?.Err ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
499
+ const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));
497
500
  captureStackTrace(e, params?.callee);
498
501
  throw e;
499
502
  }
@@ -1589,13 +1592,13 @@ var $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
1589
1592
  }
1590
1593
  return propValues;
1591
1594
  });
1592
- const isObject$1 = isObject;
1595
+ const isObject$2 = isObject;
1593
1596
  const catchall = def.catchall;
1594
1597
  let value;
1595
1598
  inst._zod.parse = (payload, ctx) => {
1596
1599
  value ?? (value = _normalized.value);
1597
1600
  const input = payload.value;
1598
- if (!isObject$1(input)) {
1601
+ if (!isObject$2(input)) {
1599
1602
  payload.issues.push({
1600
1603
  expected: "object",
1601
1604
  code: "invalid_type",
@@ -1718,7 +1721,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
1718
1721
  return (payload, ctx) => fn(shape, payload, ctx);
1719
1722
  };
1720
1723
  let fastpass;
1721
- const isObject$2 = isObject;
1724
+ const isObject$1 = isObject;
1722
1725
  const jit = !globalConfig.jitless;
1723
1726
  const fastEnabled = jit && allowsEval.value;
1724
1727
  const catchall = def.catchall;
@@ -1726,7 +1729,7 @@ var $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) => {
1726
1729
  inst._zod.parse = (payload, ctx) => {
1727
1730
  value ?? (value = _normalized.value);
1728
1731
  const input = payload.value;
1729
- if (!isObject$2(input)) {
1732
+ if (!isObject$1(input)) {
1730
1733
  payload.issues.push({
1731
1734
  expected: "object",
1732
1735
  code: "invalid_type",
@@ -1945,7 +1948,7 @@ var $ZodOptional = /*@__PURE__*/ $constructor("$ZodOptional", (inst, def) => {
1945
1948
  inst._zod.optin = "optional";
1946
1949
  inst._zod.optout = "optional";
1947
1950
  defineLazy(inst._zod, "values", () => {
1948
- return def.innerType._zod.values ? new Set([...def.innerType._zod.values, void 0]) : void 0;
1951
+ return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, void 0]) : void 0;
1949
1952
  });
1950
1953
  defineLazy(inst._zod, "pattern", () => {
1951
1954
  const pattern = def.innerType._zod.pattern;
@@ -1979,7 +1982,7 @@ var $ZodNullable = /*@__PURE__*/ $constructor("$ZodNullable", (inst, def) => {
1979
1982
  return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;
1980
1983
  });
1981
1984
  defineLazy(inst._zod, "values", () => {
1982
- return def.innerType._zod.values ? new Set([...def.innerType._zod.values, null]) : void 0;
1985
+ return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, null]) : void 0;
1983
1986
  });
1984
1987
  inst._zod.parse = (payload, ctx) => {
1985
1988
  if (payload.value === null) return payload;
@@ -4078,7 +4081,7 @@ function number(params) {
4078
4081
  return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
4079
4082
  }
4080
4083
  //#endregion
4081
- //#region ../sdk/dist/units.js
4084
+ //#region ../sdk/src/units.ts
4082
4085
  var UNITS_CURRENCY = [
4083
4086
  {
4084
4087
  value: "UAH",
@@ -4386,7 +4389,13 @@ function resolveUnits(u) {
4386
4389
  return typeof u === "string" ? UNITS_MAP[u] : u;
4387
4390
  }
4388
4391
  //#endregion
4389
- //#region ../sdk/dist/currencies.js
4392
+ //#region ../sdk/src/currencies.ts
4393
+ /**
4394
+ * Валюти з вбудованого `Intl` (ISO 4217) — БЕЗ hand-list і без npm-залежності.
4395
+ * Коди — `Intl.supportedValuesOf('currency')`; символи/назви локалізовані через
4396
+ * `Intl.NumberFormat`/`Intl.DisplayNames`.
4397
+ */
4398
+ /** Усі ISO 4217 коди. Fallback — кілька основних (на випадок старого рантайму). */
4390
4399
  var CURRENCY_CODES = (() => {
4391
4400
  try {
4392
4401
  return Intl.supportedValuesOf("currency");
@@ -4401,24 +4410,34 @@ var CURRENCY_CODES = (() => {
4401
4410
  }
4402
4411
  })();
4403
4412
  var CODE_SET = new Set(CURRENCY_CODES);
4413
+ /** Чи валідний ISO 4217 код. */
4404
4414
  var isCurrencyCode = (c) => CODE_SET.has(c);
4405
4415
  //#endregion
4406
- //#region ../sdk/dist/fields/validation.js
4416
+ //#region ../sdk/src/fields/validation.ts
4417
+ /** Структуроване повідомлення для zod: JSON {code, params}. Декодує mutate.ts. */
4407
4418
  function vmsg(code, params) {
4408
4419
  return JSON.stringify(params ? {
4409
4420
  code,
4410
4421
  params
4411
4422
  } : { code });
4412
4423
  }
4424
+ /** v4 error-map: повідомлення для відсутнього значення (колишній required_error). */
4413
4425
  function reqErr(code = "required") {
4414
4426
  return { error: (iss) => iss.input === void 0 ? vmsg(code) : void 0 };
4415
4427
  }
4428
+ /** v4 error-map: повідомлення для невалідного типу (колишній invalid_type_error). */
4416
4429
  function typeErr(code = "invalid_type") {
4417
4430
  return { error: (iss) => iss.code === "invalid_type" ? vmsg(code) : void 0 };
4418
4431
  }
4432
+ /** v4 error-map: required + invalid_type разом (колишні required_error + invalid_type_error). */
4419
4433
  function reqTypeErr() {
4420
4434
  return { error: (iss) => iss.code === "invalid_type" ? iss.input === void 0 ? vmsg("required") : vmsg("invalid_type") : void 0 };
4421
4435
  }
4436
+ /** Parse a JSON string, else return the native value (object/array) as-is.
4437
+ * Tolerant input for native form-state AND legacy JSON-string payloads.
4438
+ * A string that fails JSON.parse is returned unchanged — callers detect
4439
+ * parse failure via `typeof result === 'string'` (inners here are object/array,
4440
+ * never a bare string). */
4422
4441
  function jsonValue(raw) {
4423
4442
  if (typeof raw === "string") try {
4424
4443
  return JSON.parse(raw);
@@ -4427,6 +4446,12 @@ function jsonValue(raw) {
4427
4446
  }
4428
4447
  return raw;
4429
4448
  }
4449
+ /**
4450
+ * Фабрика для полів, що зберігають JSON і валідуються вкладеною zod-схемою.
4451
+ * Приймає нативне object/array (native form-state) АБО JSON-рядок (legacy).
4452
+ * Single-parse через jsonValue → inner.safeParse → issue `code`; рядок, що не
4453
+ * розпарсився, лишається рядком → код 'json'.
4454
+ */
4430
4455
  function jsonRefined(inner, code) {
4431
4456
  return unknown().superRefine((raw, ctx) => {
4432
4457
  const parsed = jsonValue(raw);
@@ -4455,7 +4480,7 @@ function optionalize(schema, required) {
4455
4480
  }).pipe(schema));
4456
4481
  }
4457
4482
  //#endregion
4458
- //#region ../sdk/dist/fields/normalize.js
4483
+ //#region ../sdk/src/fields/normalize.ts
4459
4484
  function normalizeOpts(rawIn) {
4460
4485
  const raw = rawIn;
4461
4486
  const r = raw.rules ?? {};
@@ -4495,7 +4520,19 @@ function normalizeOpts(rawIn) {
4495
4520
  return Object.fromEntries(Object.entries(result).filter(([, v]) => v !== void 0));
4496
4521
  }
4497
4522
  //#endregion
4498
- //#region ../sdk/dist/field-presets.js
4523
+ //#region ../sdk/src/field-presets.ts
4524
+ /**
4525
+ * Пресети полів — тонкі обгортки над базовими примітивами з fields.ts.
4526
+ *
4527
+ * Кожен пресет = один kind (один віджет). Пресети не приймають `format` —
4528
+ * вони самі є семантичними типами. min/max/step — через `config`.
4529
+ *
4530
+ * Пресети додаються до `f` через `composeF`, який гарантує, що жоден пресет
4531
+ * не перебиває примітив.
4532
+ *
4533
+ * Циклічний імпорт з fields.ts безпечний: фабрики/хелпери — hoisted-декларації,
4534
+ * а пресети викликають їх лише в тілі своїх функцій.
4535
+ */
4499
4536
  function email(raw) {
4500
4537
  const o = normalizeOpts(raw);
4501
4538
  const required = o.required ?? false;
@@ -4549,6 +4586,14 @@ function password(raw) {
4549
4586
  zod: optionalize(s, required)
4550
4587
  });
4551
4588
  }
4589
+ /**
4590
+ * Internal API-token list — keyValue-shaped collection (name + write-only
4591
+ * token + timestamps), custom renderer (`kind:'internalApiToken'`, own
4592
+ * create/revoke UI — same trick as url()/perWeekday()). "Internal" = not a
4593
+ * general-purpose field type for plugin shelves, only for the core account
4594
+ * settings page. `token` is `kind:'password'` so maskSecrets/preserveTree
4595
+ * already mask/preserve it for free.
4596
+ */
4552
4597
  function internalApiToken(o) {
4553
4598
  return group({
4554
4599
  key: o.key,
@@ -4641,8 +4686,10 @@ function link(raw) {
4641
4686
  }, o.multiple ?? false));
4642
4687
  }
4643
4688
  var TEL_RE = /^\+?[\d\s()-]{4,}$/;
4689
+ /** Loose URL: будь-який scheme:// АБО host-з-крапкою (+опц. порт/шлях). Без пробілів. */
4644
4690
  var LINK_RE = /^([a-z][a-z0-9+.-]*:\/\/\S+|[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?)$/i;
4645
- var CSS_COLOR_NAMES = new Set([
4691
+ /** CSS named colors (CSS Color Module L4) — для f.colorname. */
4692
+ var CSS_COLOR_NAMES = /* @__PURE__ */ new Set([
4646
4693
  "aliceblue",
4647
4694
  "antiquewhite",
4648
4695
  "aqua",
@@ -4900,6 +4947,7 @@ function reminder(raw) {
4900
4947
  }
4901
4948
  var _real = real;
4902
4949
  var _int = int;
4950
+ /** Відсоток 0..100 — real з rules:{min:0,max:100}. */
4903
4951
  function percent(o) {
4904
4952
  return _real({
4905
4953
  ...o,
@@ -4910,6 +4958,7 @@ function percent(o) {
4910
4958
  }
4911
4959
  });
4912
4960
  }
4961
+ /** Рік — int з rules:{min:1900,max:2100}; межі можна перекрити через rules.min/max. */
4913
4962
  function year(o) {
4914
4963
  return _int({
4915
4964
  ...o,
@@ -5101,7 +5150,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5101
5150
  },
5102
5151
  cs: {
5103
5152
  name: "Czech",
5104
- nativeName: "Čeština"
5153
+ nativeName: "čeština"
5105
5154
  },
5106
5155
  cu: {
5107
5156
  name: "Old Church Slavonic",
@@ -5565,7 +5614,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5565
5614
  },
5566
5615
  sk: {
5567
5616
  name: "Slovak",
5568
- nativeName: "Slovenčina"
5617
+ nativeName: "slovenčtina"
5569
5618
  },
5570
5619
  sl: {
5571
5620
  name: "Slovenian",
@@ -5730,7 +5779,7 @@ var require_data = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5730
5779
  };
5731
5780
  }));
5732
5781
  //#endregion
5733
- //#region ../sdk/dist/fields/constants.js
5782
+ //#region ../sdk/src/fields/constants.ts
5734
5783
  var import_src = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
5735
5784
  var LANGUAGES_LIST = require_data();
5736
5785
  var LANGUAGES = {};
@@ -5786,7 +5835,21 @@ import_src.default.getAllCodes().map((code) => ({
5786
5835
  name: import_src.default.getNativeName(code)
5787
5836
  })).sort((a, b) => a.name.localeCompare(b.name));
5788
5837
  //#endregion
5789
- //#region ../sdk/dist/fields.js
5838
+ //#region ../sdk/src/fields.ts
5839
+ /**
5840
+ * The field type system — the heart of the platform. A pure, isomorphic module.
5841
+ * One field description → three consumers:
5842
+ * 1. `zod` — validation (identical on server and client)
5843
+ * 2. `column` — the DB column type
5844
+ * 3. `prim`/`kind` — keys for resolving the renderer (see web/render/registry)
5845
+ */
5846
+ /**
5847
+ * A visual or storage group of fields (one level of nesting — no groups inside groups).
5848
+ * Without `key` → a layout group (not stored). With `key` → storage:
5849
+ * - embedded (key, no multiple): stored as a column or a nested object.
5850
+ * - collection (key + multiple): a child table (array of rows).
5851
+ * A storage group requires a `label` and only `FieldItem` children (with a storage key).
5852
+ */
5790
5853
  function group(o) {
5791
5854
  const r = o.rules ?? {};
5792
5855
  const v = o.view ?? {};
@@ -5809,6 +5872,7 @@ function group(o) {
5809
5872
  fields: o.fields
5810
5873
  };
5811
5874
  }
5875
+ /** Layout group, flow nowrap + horizontal scroll (single line). Sugar over group(). */
5812
5876
  function row(o) {
5813
5877
  return group({
5814
5878
  label: o.label,
@@ -5819,6 +5883,7 @@ function row(o) {
5819
5883
  }
5820
5884
  });
5821
5885
  }
5886
+ /** Tabular collection (array of rows → <table>, aligned by columns). Sugar. */
5822
5887
  function table(o) {
5823
5888
  return group({
5824
5889
  key: o.key,
@@ -5833,6 +5898,11 @@ function table(o) {
5833
5898
  }
5834
5899
  });
5835
5900
  }
5901
+ /**
5902
+ * Layout group with a 2D layout (rows×columns → aligned CSS grid).
5903
+ * Keyless (flat parent fields). Rows are glued together with brk() separators
5904
+ * into a flat fields; the renderer reconstructs the 2D by splitting on brk. Sugar over group().
5905
+ */
5836
5906
  function sheet(o) {
5837
5907
  const flat = [];
5838
5908
  o.fields.forEach((rowFields, i) => {
@@ -5848,6 +5918,12 @@ function sheet(o) {
5848
5918
  }
5849
5919
  });
5850
5920
  }
5921
+ /**
5922
+ * Composite URL — embedded-group sugar. Storage: separate nested columns
5923
+ * (`key__scheme`, `key__host`, `key__port`, …) in the same table. The renderer
5924
+ * is custom (a single link/input, `kind:'url'`) — string↔parts via
5925
+ * parseUrl/buildUrl. Partial/invalid URLs are stored by parts.
5926
+ */
5851
5927
  function url(o) {
5852
5928
  return group({
5853
5929
  key: o.key,
@@ -5881,18 +5957,22 @@ function keyed(o) {
5881
5957
  fixed: o.fixed
5882
5958
  };
5883
5959
  }
5960
+ /** Horizontal divider. */
5884
5961
  function divider() {
5885
5962
  return { el: "divider" };
5886
5963
  }
5964
+ /** Forced flow-row wrap (subsequent fields start on a new row). No <hr>. */
5887
5965
  function brk() {
5888
5966
  return { el: "break" };
5889
5967
  }
5968
+ /** Reference markdown block by i18n key. */
5890
5969
  function info(textKey) {
5891
5970
  return {
5892
5971
  el: "info",
5893
5972
  text: textKey
5894
5973
  };
5895
5974
  }
5975
+ /** Action button: invokes the handler registered in actionRegistry under the key `value`. */
5896
5976
  function button(o) {
5897
5977
  return {
5898
5978
  el: "button",
@@ -5902,6 +5982,7 @@ function button(o) {
5902
5982
  variant: o.variant
5903
5983
  };
5904
5984
  }
5985
+ /** Wraps FieldMeta into FieldItem/StaticEl depending on opts.key/opts.value. */
5905
5986
  function wrapKey(opts, meta) {
5906
5987
  let m = meta;
5907
5988
  if (opts.noEditControl) m = {
@@ -5911,6 +5992,34 @@ function wrapKey(opts, meta) {
5911
5992
  noEditControl: true
5912
5993
  }
5913
5994
  };
5995
+ if (opts.emphasis) m = {
5996
+ ...m,
5997
+ hints: {
5998
+ ...m.hints,
5999
+ emphasis: opts.emphasis
6000
+ }
6001
+ };
6002
+ if (opts.noLabel) m = {
6003
+ ...m,
6004
+ hints: {
6005
+ ...m.hints,
6006
+ noLabel: true
6007
+ }
6008
+ };
6009
+ if (opts.role) m = {
6010
+ ...m,
6011
+ hints: {
6012
+ ...m.hints,
6013
+ role: opts.role
6014
+ }
6015
+ };
6016
+ if (opts.pinned) m = {
6017
+ ...m,
6018
+ hints: {
6019
+ ...m.hints,
6020
+ pinned: true
6021
+ }
6022
+ };
5914
6023
  if (opts.default !== void 0) m = {
5915
6024
  ...m,
5916
6025
  default: opts.default
@@ -5940,6 +6049,14 @@ function safeJsonParse(s) {
5940
6049
  return;
5941
6050
  }
5942
6051
  }
6052
+ /**
6053
+ * Wraps a field into a JSON array of values (storage: TEXT). The inner zod is `base.zod`.
6054
+ * Modifier order: base → applyMultiple.
6055
+ *
6056
+ * Exception: kind 'image'/'media' handle multiple themselves in the renderer (gallery) —
6057
+ * the MultipleField* wrappers on the web skip them.
6058
+ */
6059
+ /** Zod "JSON array of inner values" — shared by applyMultiple and bindSelectSourceZod. */
5943
6060
  function multipleZod(inner) {
5944
6061
  return unknown().superRefine((raw, ctx) => {
5945
6062
  let arr;
@@ -6266,8 +6383,8 @@ function relation(raw) {
6266
6383
  displayKey: o.displayKey ?? "name"
6267
6384
  },
6268
6385
  relation: {
6269
- vault: raw.options.vault,
6270
- type: raw.options.module
6386
+ library: raw.options.library,
6387
+ type: raw.options.shelf
6271
6388
  },
6272
6389
  ...multi && { json: true },
6273
6390
  zod: optionalize(s, required)
@@ -6532,6 +6649,11 @@ function geo(raw) {
6532
6649
  zod: optionalize(s, required)
6533
6650
  });
6534
6651
  }
6652
+ /**
6653
+ * 7 values, Mon–Sun. A thin wrapper over keyed({fixed}): a fixed collection,
6654
+ * by=select('weekdays'), one real value per day. Its own kind 'per-weekday'
6655
+ * dispatches a compact renderer. Storage: a grandchild table of {day, value} rows.
6656
+ */
6535
6657
  function perWeekday(o) {
6536
6658
  return {
6537
6659
  ...keyed({
@@ -6824,6 +6946,11 @@ function embed(raw) {
6824
6946
  zod: optionalize(s, required)
6825
6947
  });
6826
6948
  }
6949
+ /**
6950
+ * Base primitives (storage-aligned) + structural types. Presets (thin wrappers
6951
+ * over these primitives) are added below from field-presets.ts and CANNOT override
6952
+ * any key from here (guard in composeF).
6953
+ */
6827
6954
  var PRIMITIVES = {
6828
6955
  string,
6829
6956
  text,
@@ -6873,6 +7000,7 @@ var PRIMITIVES = {
6873
7000
  info,
6874
7001
  button
6875
7002
  };
7003
+ /** Assembles `f`, guaranteeing no preset shadows a primitive. */
6876
7004
  function composeF(presets) {
6877
7005
  for (const k of Object.keys(presets)) if (k in PRIMITIVES) throw new Error(`[f] preset '${k}' overrides a primitive`);
6878
7006
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/plugin-telegram",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -25,11 +25,11 @@
25
25
  "test": "node --import tsx --test \"src/runtime/*.test.ts\""
26
26
  },
27
27
  "dependencies": {
28
- "@coffer-org/plugin-orchestrator": "^1.3.1",
29
- "@coffer-org/sdk": "^1.4.0",
30
- "@coffer-org/server": "^1.4.0",
28
+ "@coffer-org/plugin-orchestrator": "^1.4.0",
29
+ "@coffer-org/sdk": "^1.5.0",
30
+ "@coffer-org/server": "^1.8.0",
31
31
  "input": "^1.0.1",
32
- "telegram": "^2.26.0"
32
+ "teleproto": "^1.228.1"
33
33
  },
34
34
  "devDependencies": {},
35
35
  "coffer": {