spectrum-ts 4.2.0 → 5.1.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.
Files changed (46) hide show
  1. package/README.md +29 -67
  2. package/dist/authoring.d.ts +1 -6
  3. package/dist/authoring.js +2 -36
  4. package/dist/elysia.d.ts +1 -94
  5. package/dist/elysia.js +2 -15
  6. package/dist/express.d.ts +1 -62
  7. package/dist/express.js +2 -19
  8. package/dist/hono.d.ts +1 -64
  9. package/dist/hono.js +2 -11
  10. package/dist/index.d.ts +1 -2851
  11. package/dist/index.js +2 -3763
  12. package/dist/manifest.json +5 -5
  13. package/dist/providers/imessage/index.d.ts +1 -222
  14. package/dist/providers/imessage/index.js +2 -25
  15. package/dist/providers/index.d.ts +6 -19
  16. package/dist/providers/index.js +6 -34
  17. package/dist/providers/slack/index.d.ts +1 -46
  18. package/dist/providers/slack/index.js +2 -11
  19. package/dist/providers/telegram/index.d.ts +1 -45
  20. package/dist/providers/telegram/index.js +2 -13
  21. package/dist/providers/terminal/index.d.ts +1 -119
  22. package/dist/providers/terminal/index.js +2 -13
  23. package/dist/providers/whatsapp-business/index.d.ts +1 -27
  24. package/dist/providers/whatsapp-business/index.js +2 -14
  25. package/package.json +11 -38
  26. package/dist/attachment-CnivEhr6.d.ts +0 -29
  27. package/dist/authoring-b9AhXgPI.d.ts +0 -304
  28. package/dist/chunk-2D27WW5B.js +0 -63
  29. package/dist/chunk-34FQGGD7.js +0 -34
  30. package/dist/chunk-3GEJYGZK.js +0 -84
  31. package/dist/chunk-5XEFJBN2.js +0 -197
  32. package/dist/chunk-6UZFVXQF.js +0 -374
  33. package/dist/chunk-A37PM5N2.js +0 -91
  34. package/dist/chunk-ARL2NOBO.js +0 -887
  35. package/dist/chunk-B52VPQO3.js +0 -1379
  36. package/dist/chunk-DMPDLSFU.js +0 -864
  37. package/dist/chunk-FAIFTUV2.js +0 -139
  38. package/dist/chunk-LZXPLXZF.js +0 -35
  39. package/dist/chunk-N6THJDZV.js +0 -929
  40. package/dist/chunk-NLMQ75LH.js +0 -2980
  41. package/dist/chunk-UXAKIXVM.js +0 -409
  42. package/dist/chunk-WXLQNANA.js +0 -539
  43. package/dist/chunk-ZR3TKZMT.js +0 -129
  44. package/dist/read-C4uvozGX.d.ts +0 -53
  45. package/dist/types-CyfLJXgu.d.ts +0 -1530
  46. package/dist/types-ZgFTj5hJ.d.ts +0 -87
@@ -1,929 +0,0 @@
1
- import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
- import {
3
- fusor
4
- } from "./chunk-34FQGGD7.js";
5
- import {
6
- asVoice
7
- } from "./chunk-FAIFTUV2.js";
8
- import {
9
- asGroup
10
- } from "./chunk-LZXPLXZF.js";
11
- import {
12
- toVCard
13
- } from "./chunk-6UZFVXQF.js";
14
- import {
15
- UnsupportedError,
16
- asMarkdown,
17
- definePlatform,
18
- renderInlineTokens
19
- } from "./chunk-B52VPQO3.js";
20
- import {
21
- asAttachment,
22
- asCustom,
23
- asReaction,
24
- asText
25
- } from "./chunk-UXAKIXVM.js";
26
-
27
- // src/providers/telegram/config.ts
28
- import z from "zod";
29
- var TELEGRAM_PLATFORM = "telegram";
30
- var DEFAULT_BASE_URL = "https://api.telegram.org";
31
- var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
32
- var BOT_TOKEN_PATTERN = /^\d+:[A-Za-z0-9_-]+$/;
33
- var configSchema = z.object({
34
- /** Bot token from @BotFather (outbound API calls + media downloads). */
35
- botToken: z.string().regex(BOT_TOKEN_PATTERN, "botToken must be in the form '<id>:<token>'"),
36
- /**
37
- * The `secret_token` passed to `setWebhook`. When present, inbound webhooks
38
- * are verified against the `X-Telegram-Bot-Api-Secret-Token` header; when
39
- * omitted, the check is skipped. Telegram does not HMAC-sign the body, so
40
- * this shared token is the only inbound authentication.
41
- */
42
- webhookSecret: z.string().regex(SECRET_TOKEN_PATTERN).optional(),
43
- /** Override the Bot API base URL. Defaults to `https://api.telegram.org`. */
44
- baseUrl: z.url().default(DEFAULT_BASE_URL)
45
- });
46
- var botIdFromToken = (botToken) => botToken.split(":")[0] ?? "";
47
-
48
- // src/providers/telegram/client.ts
49
- import { createTelegramClient, getFile } from "@photon-ai/telegram-ts";
50
- var REQUEST_TIMEOUT_MS = 3e4;
51
- var TRAILING_SLASHES = /\/+$/;
52
- var telegramClient = (config) => createTelegramClient({ token: config.botToken, baseUrl: config.baseUrl });
53
- var appendFormValue = (form, key, value) => {
54
- if (typeof value === "string" || value instanceof Blob) {
55
- form.append(key, value);
56
- } else if (value instanceof Date) {
57
- form.append(key, value.toISOString());
58
- } else {
59
- form.append(key, JSON.stringify(value));
60
- }
61
- };
62
- var toFormData = (body) => {
63
- const form = new FormData();
64
- for (const [key, value] of Object.entries(body)) {
65
- if (value === void 0 || value === null) {
66
- continue;
67
- }
68
- appendFormValue(form, key, value);
69
- }
70
- return form;
71
- };
72
- var executeSpec = async (client, spec) => {
73
- const url = `/${spec.method}`;
74
- if (spec.file) {
75
- const file = new File(
76
- [new Uint8Array(spec.file.bytes)],
77
- spec.file.filename,
78
- { type: spec.file.mimeType }
79
- );
80
- const res2 = await client.post({
81
- body: { ...spec.params, [spec.file.field]: file },
82
- bodySerializer: toFormData,
83
- headers: { "Content-Type": null },
84
- throwOnError: true,
85
- url
86
- });
87
- return res2.data.result;
88
- }
89
- const res = await client.post({ body: spec.params, throwOnError: true, url });
90
- return res.data.result;
91
- };
92
- var downloadFile = async (config, fileId) => {
93
- const client = telegramClient(config);
94
- const meta = await getFile({
95
- body: { file_id: fileId },
96
- client,
97
- throwOnError: true
98
- });
99
- const filePath = meta.result?.file_path;
100
- if (!filePath) {
101
- throw new Error(`Telegram getFile returned no file_path for ${fileId}`);
102
- }
103
- const base = config.baseUrl.replace(TRAILING_SLASHES, "");
104
- const res = await fetch(`${base}/file/bot${config.botToken}/${filePath}`, {
105
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
106
- });
107
- if (!res.ok) {
108
- throw new Error(
109
- `Telegram media download failed: ${res.status} ${res.statusText}`
110
- );
111
- }
112
- return Buffer.from(await res.arrayBuffer());
113
- };
114
-
115
- // src/providers/telegram/inbound/media.ts
116
- var DEFAULT_VIDEO_MIME = "video/mp4";
117
- var DEFAULT_AUDIO_MIME = "audio/mpeg";
118
- var DEFAULT_VOICE_MIME = "audio/ogg";
119
- var DEFAULT_DOC_MIME = "application/octet-stream";
120
- var pixelArea = (photo) => photo.file_size ?? photo.width * photo.height;
121
- var pickLargestPhoto = (photos) => photos.reduce(
122
- (best, next) => pixelArea(next) > pixelArea(best) ? next : best
123
- );
124
- var lazyRead = (config, fileId) => () => downloadFile(config, fileId);
125
- var fileAttachment = (config, file, fallbackExt, fallbackMime) => asAttachment({
126
- id: file.file_id,
127
- name: file.file_name ?? `${file.file_unique_id}.${fallbackExt}`,
128
- mimeType: file.mime_type ?? fallbackMime,
129
- size: file.file_size,
130
- read: lazyRead(config, file.file_id)
131
- });
132
- var stickerAttachment = (config, sticker) => {
133
- let ext = "webp";
134
- let mimeType = "image/webp";
135
- if (sticker.is_animated) {
136
- ext = "tgs";
137
- mimeType = "application/x-tgsticker";
138
- } else if (sticker.is_video) {
139
- ext = "webm";
140
- mimeType = "video/webm";
141
- }
142
- return asAttachment({
143
- id: sticker.file_id,
144
- name: `${sticker.file_unique_id}.${ext}`,
145
- mimeType,
146
- size: sticker.file_size,
147
- read: lazyRead(config, sticker.file_id)
148
- });
149
- };
150
- var mediaToContent = (msg, config) => {
151
- if (msg.voice) {
152
- return asVoice({
153
- mimeType: msg.voice.mime_type ?? DEFAULT_VOICE_MIME,
154
- duration: msg.voice.duration,
155
- size: msg.voice.file_size,
156
- read: lazyRead(config, msg.voice.file_id)
157
- });
158
- }
159
- if (msg.video_note) {
160
- return fileAttachment(config, msg.video_note, "mp4", DEFAULT_VIDEO_MIME);
161
- }
162
- if (msg.animation) {
163
- return fileAttachment(config, msg.animation, "mp4", DEFAULT_VIDEO_MIME);
164
- }
165
- if (msg.video) {
166
- return fileAttachment(config, msg.video, "mp4", DEFAULT_VIDEO_MIME);
167
- }
168
- if (msg.audio) {
169
- return fileAttachment(config, msg.audio, "mp3", DEFAULT_AUDIO_MIME);
170
- }
171
- if (msg.document) {
172
- return fileAttachment(config, msg.document, "bin", DEFAULT_DOC_MIME);
173
- }
174
- if (msg.photo && msg.photo.length > 0) {
175
- return fileAttachment(
176
- config,
177
- pickLargestPhoto(msg.photo),
178
- "jpg",
179
- "image/jpeg"
180
- );
181
- }
182
- if (msg.sticker) {
183
- return stickerAttachment(config, msg.sticker);
184
- }
185
- return;
186
- };
187
- var messageToContent = (msg, config) => {
188
- const media = mediaToContent(msg, config);
189
- if (media) {
190
- const caption = msg.caption?.trim();
191
- return caption ? [asText(caption), media] : [media];
192
- }
193
- const text = msg.text;
194
- if (text !== void 0 && text.length > 0) {
195
- return [asText(text)];
196
- }
197
- return [];
198
- };
199
-
200
- // src/providers/telegram/inbound/messages.ts
201
- var MILLIS_PER_SECOND = 1e3;
202
- var stubMessage = (id, content) => ({ id, content });
203
- var senderRef = (user) => ({
204
- id: String(user.id),
205
- ...user.username ? { handle: user.username } : {},
206
- isMe: false
207
- });
208
- var toRecordContent = (contents, messageId) => {
209
- if (contents.length === 0) {
210
- return;
211
- }
212
- if (contents.length === 1) {
213
- return contents[0];
214
- }
215
- return asGroup({
216
- items: contents.map(
217
- (content, index) => stubMessage(`${messageId}:${index}`, content)
218
- )
219
- });
220
- };
221
- var fromMessage = (msg, config) => {
222
- if (msg.from && String(msg.from.id) === botIdFromToken(config.botToken)) {
223
- return;
224
- }
225
- const content = toRecordContent(
226
- messageToContent(msg, config),
227
- String(msg.message_id)
228
- );
229
- if (!content) {
230
- return;
231
- }
232
- return {
233
- id: String(msg.message_id),
234
- content,
235
- ...msg.from ? { sender: senderRef(msg.from) } : {},
236
- space: { id: String(msg.chat.id) },
237
- timestamp: new Date(msg.date * MILLIS_PER_SECOND)
238
- };
239
- };
240
- var emojiReactions = (reactions) => reactions.filter((r) => r.type === "emoji").map((r) => r.emoji);
241
- var fromReaction = (reaction) => {
242
- const added = emojiReactions(reaction.new_reaction);
243
- if (added.length === 0) {
244
- return;
245
- }
246
- const previous = new Set(emojiReactions(reaction.old_reaction));
247
- const emoji = added.find((e) => !previous.has(e)) ?? added[0];
248
- if (!emoji) {
249
- return;
250
- }
251
- const target = stubMessage(
252
- String(reaction.message_id),
253
- asCustom({ telegram: "reaction-target" })
254
- );
255
- const actorId = reaction.user ? String(reaction.user.id) : "anonymous";
256
- return {
257
- id: `reaction:${reaction.chat.id}:${reaction.message_id}:${reaction.date}:${actorId}:${emoji}`,
258
- content: asReaction({ emoji, target }),
259
- ...reaction.user ? { sender: senderRef(reaction.user) } : {},
260
- space: { id: String(reaction.chat.id) },
261
- timestamp: new Date(reaction.date * MILLIS_PER_SECOND)
262
- };
263
- };
264
- var handleMessages = ({
265
- payload: update,
266
- config
267
- }) => {
268
- const message = update.message ?? update.channel_post;
269
- if (message) {
270
- return fromMessage(message, config);
271
- }
272
- if (update.message_reaction) {
273
- return fromReaction(update.message_reaction);
274
- }
275
- return;
276
- };
277
-
278
- // src/providers/telegram/outbound/send.ts
279
- import {
280
- editMessageText,
281
- sendChatAction,
282
- setMessageReaction
283
- } from "@photon-ai/telegram-ts";
284
-
285
- // src/providers/telegram/reactions.ts
286
- var ALLOWED_REACTION_EMOJI = /* @__PURE__ */ new Set([
287
- "\u{1F44D}",
288
- "\u{1F44E}",
289
- "\u2764",
290
- "\u{1F525}",
291
- "\u{1F970}",
292
- "\u{1F44F}",
293
- "\u{1F601}",
294
- "\u{1F914}",
295
- "\u{1F92F}",
296
- "\u{1F631}",
297
- "\u{1F92C}",
298
- "\u{1F622}",
299
- "\u{1F389}",
300
- "\u{1F929}",
301
- "\u{1F92E}",
302
- "\u{1F4A9}",
303
- "\u{1F64F}",
304
- "\u{1F44C}",
305
- "\u{1F54A}",
306
- "\u{1F921}",
307
- "\u{1F971}",
308
- "\u{1F974}",
309
- "\u{1F60D}",
310
- "\u{1F433}",
311
- "\u2764\u200D\u{1F525}",
312
- "\u{1F31A}",
313
- "\u{1F32D}",
314
- "\u{1F4AF}",
315
- "\u{1F923}",
316
- "\u26A1",
317
- "\u{1F34C}",
318
- "\u{1F3C6}",
319
- "\u{1F494}",
320
- "\u{1F928}",
321
- "\u{1F610}",
322
- "\u{1F353}",
323
- "\u{1F37E}",
324
- "\u{1F48B}",
325
- "\u{1F595}",
326
- "\u{1F608}",
327
- "\u{1F634}",
328
- "\u{1F62D}",
329
- "\u{1F913}",
330
- "\u{1F47B}",
331
- "\u{1F468}\u200D\u{1F4BB}",
332
- "\u{1F440}",
333
- "\u{1F383}",
334
- "\u{1F648}",
335
- "\u{1F607}",
336
- "\u{1F628}",
337
- "\u{1F91D}",
338
- "\u270D",
339
- "\u{1F917}",
340
- "\u{1FAE1}",
341
- "\u{1F385}",
342
- "\u{1F384}",
343
- "\u2603",
344
- "\u{1F485}",
345
- "\u{1F92A}",
346
- "\u{1F5FF}",
347
- "\u{1F192}",
348
- "\u{1F498}",
349
- "\u{1F649}",
350
- "\u{1F984}",
351
- "\u{1F618}",
352
- "\u{1F48A}",
353
- "\u{1F64A}",
354
- "\u{1F60E}",
355
- "\u{1F47E}",
356
- "\u{1F937}\u200D\u2642",
357
- "\u{1F937}",
358
- "\u{1F937}\u200D\u2640",
359
- "\u{1F621}"
360
- ]);
361
- var VARIATION_SELECTOR_16 = /️/g;
362
- var stripVariationSelector = (emoji) => emoji.replace(VARIATION_SELECTOR_16, "");
363
- var isAllowedReactionEmoji = (emoji) => ALLOWED_REACTION_EMOJI.has(stripVariationSelector(emoji));
364
- var normalizeReactionEmoji = (emoji) => isAllowedReactionEmoji(emoji) ? stripVariationSelector(emoji) : emoji;
365
-
366
- // src/providers/telegram/outbound/markdown.ts
367
- import { Marked } from "marked";
368
- var markdownLexer = new Marked();
369
- var BULLET = "\u2022 ";
370
- var HR_LINE = "\u2014\u2014\u2014";
371
- var NESTED_LIST_INDENT = " ";
372
- var BLOCK_SEPARATOR = "\n\n";
373
- var TABLE_CELL_SEPARATOR = " | ";
374
- var DEFAULT_LIST_START = 1;
375
- var AMP_PATTERN = /&/g;
376
- var LT_PATTERN = /</g;
377
- var GT_PATTERN = />/g;
378
- var QUOTE_PATTERN = /"/g;
379
- var escapeHtml = (value) => value.replace(AMP_PATTERN, "&amp;").replace(LT_PATTERN, "&lt;").replace(GT_PATTERN, "&gt;");
380
- var escapeAttribute = (value) => escapeHtml(value).replace(QUOTE_PATTERN, "&quot;");
381
- var asMarkedToken = (token) => token;
382
- var checkboxPrefix = (item) => {
383
- if (!item.task) {
384
- return "";
385
- }
386
- return item.checked ? "[x] " : "[ ] ";
387
- };
388
- var listMarker = (list, index) => {
389
- if (!list.ordered) {
390
- return BULLET;
391
- }
392
- const start = list.start === "" ? DEFAULT_LIST_START : list.start;
393
- return `${start + index}. `;
394
- };
395
- var renderLink = (token) => {
396
- if (token.text === token.href) {
397
- return escapeHtml(token.href);
398
- }
399
- return `<a href="${escapeAttribute(token.href)}">${renderInlineTokens2(token.tokens)}</a>`;
400
- };
401
- var renderImage = (token) => `<a href="${escapeAttribute(token.href)}">${escapeHtml(token.text || token.href)}</a>`;
402
- var renderText = (token) => {
403
- if (token.tokens) {
404
- return renderInlineTokens2(token.tokens);
405
- }
406
- return token.escaped ? token.text : escapeHtml(token.text);
407
- };
408
- var renderInlineToken = (token) => {
409
- switch (token.type) {
410
- case "strong":
411
- return `<b>${renderInlineTokens2(token.tokens)}</b>`;
412
- case "em":
413
- return `<i>${renderInlineTokens2(token.tokens)}</i>`;
414
- case "del":
415
- return `<s>${renderInlineTokens2(token.tokens)}</s>`;
416
- case "codespan":
417
- return `<code>${escapeHtml(token.text)}</code>`;
418
- case "br":
419
- return "\n";
420
- case "link":
421
- return renderLink(token);
422
- case "image":
423
- return renderImage(token);
424
- case "escape":
425
- return escapeHtml(token.text);
426
- case "text":
427
- return renderText(token);
428
- // Raw HTML in markdown source renders literally, never passes through:
429
- // a tag outside Telegram's whitelist would 400 the whole Bot API call
430
- // (a TelegramApiError, not UnsupportedError — the plain-text fallback
431
- // would not catch it). Escaping makes invalid output impossible.
432
- case "html":
433
- return escapeHtml(token.text);
434
- // Task-item checkboxes are rendered from `ListItem.task`/`checked`.
435
- case "checkbox":
436
- return "";
437
- default:
438
- return "raw" in token ? escapeHtml(String(token.raw)) : "";
439
- }
440
- };
441
- var renderInlineTokens2 = (tokens) => {
442
- let out = "";
443
- for (const token of tokens) {
444
- out += renderInlineToken(asMarkedToken(token));
445
- }
446
- return out;
447
- };
448
- var renderCode = (token) => {
449
- if (token.lang) {
450
- return `<pre><code class="language-${escapeAttribute(token.lang)}">${escapeHtml(token.text)}</code></pre>`;
451
- }
452
- return `<pre>${escapeHtml(token.text)}</pre>`;
453
- };
454
- var renderQuoteBody = (tokens) => {
455
- const blocks = [];
456
- for (const token of tokens) {
457
- const marked = asMarkedToken(token);
458
- const rendered = marked.type === "blockquote" ? renderQuoteBody(marked.tokens) : renderBlockToken(marked);
459
- if (rendered) {
460
- blocks.push(rendered);
461
- }
462
- }
463
- return blocks.join("\n");
464
- };
465
- var renderList = (list) => {
466
- const lines = [];
467
- for (const [index, item] of list.items.entries()) {
468
- const prefix = `${listMarker(list, index)}${checkboxPrefix(item)}`;
469
- const blocks = [];
470
- for (const token of item.tokens) {
471
- const rendered = renderBlockToken(asMarkedToken(token));
472
- if (rendered) {
473
- blocks.push(rendered);
474
- }
475
- }
476
- const [first = "", ...rest] = blocks.join("\n").split("\n");
477
- lines.push(`${prefix}${first}`);
478
- for (const line of rest) {
479
- lines.push(`${NESTED_LIST_INDENT}${line}`);
480
- }
481
- }
482
- return lines.join("\n");
483
- };
484
- var renderTable = (table) => {
485
- const renderRow = (cells) => cells.map((cell) => renderInlineTokens(cell.tokens)).join(TABLE_CELL_SEPARATOR);
486
- const lines = [renderRow(table.header)];
487
- for (const row of table.rows) {
488
- lines.push(renderRow(row));
489
- }
490
- return `<pre>${escapeHtml(lines.join("\n"))}</pre>`;
491
- };
492
- var renderBlockToken = (token) => {
493
- switch (token.type) {
494
- // Telegram has no headings; bold is the conventional stand-in.
495
- case "heading":
496
- return `<b>${renderInlineTokens2(token.tokens)}</b>`;
497
- case "paragraph":
498
- return renderInlineTokens2(token.tokens);
499
- case "code":
500
- return renderCode(token);
501
- case "blockquote":
502
- return `<blockquote>${renderQuoteBody(token.tokens)}</blockquote>`;
503
- case "list":
504
- return renderList(token);
505
- case "table":
506
- return renderTable(token);
507
- case "hr":
508
- return HR_LINE;
509
- case "space":
510
- case "def":
511
- return "";
512
- default:
513
- return renderInlineToken(token);
514
- }
515
- };
516
- var markdownToTelegramHtml = (markdown) => {
517
- const blocks = [];
518
- for (const token of markdownLexer.lexer(markdown)) {
519
- const rendered = renderBlockToken(asMarkedToken(token));
520
- if (rendered) {
521
- blocks.push(rendered);
522
- }
523
- }
524
- return blocks.join(BLOCK_SEPARATOR).trim();
525
- };
526
-
527
- // src/providers/telegram/outbound/message.ts
528
- var VCARD_FILENAME = "contact.vcf";
529
- var VCARD_MIME = "text/vcard";
530
- var DEFAULT_VOICE_FILENAME = "voice.ogg";
531
- var parseMessageId = (id) => {
532
- const messageId = Number(id);
533
- if (!Number.isInteger(messageId) || messageId <= 0) {
534
- throw new Error(
535
- `Telegram message id must be a positive integer (got "${id}").`
536
- );
537
- }
538
- return messageId;
539
- };
540
- var customToSpec = (raw) => {
541
- if (typeof raw === "object" && raw !== null && typeof raw.method === "string") {
542
- const value = raw;
543
- const { params } = value;
544
- if (params !== void 0 && (typeof params !== "object" || params === null || Array.isArray(params))) {
545
- throw new Error(
546
- "Telegram custom content `raw.params` must be an object when provided."
547
- );
548
- }
549
- return {
550
- method: value.method,
551
- params: params ?? {}
552
- };
553
- }
554
- throw new Error(
555
- "Telegram custom content `raw` must be a `{ method, params }` Bot API call."
556
- );
557
- };
558
- var attachmentSpec = async (content) => {
559
- const bytes = await content.read();
560
- const file = {
561
- field: "document",
562
- filename: content.name,
563
- mimeType: content.mimeType,
564
- bytes
565
- };
566
- if (content.mimeType.startsWith("image/")) {
567
- return {
568
- method: "sendPhoto",
569
- params: {},
570
- file: { ...file, field: "photo" }
571
- };
572
- }
573
- if (content.mimeType.startsWith("video/")) {
574
- return {
575
- method: "sendVideo",
576
- params: {},
577
- file: { ...file, field: "video" }
578
- };
579
- }
580
- return { method: "sendDocument", params: {}, file };
581
- };
582
- var buildSend = async (content) => {
583
- switch (content.type) {
584
- case "text":
585
- return { method: "sendMessage", params: { text: content.text } };
586
- case "markdown":
587
- return {
588
- method: "sendMessage",
589
- params: {
590
- text: markdownToTelegramHtml(content.markdown),
591
- parse_mode: "HTML"
592
- }
593
- };
594
- case "richlink":
595
- return { method: "sendMessage", params: { text: content.url } };
596
- case "attachment":
597
- return await attachmentSpec(content);
598
- case "voice": {
599
- const bytes = await content.read();
600
- return {
601
- method: "sendVoice",
602
- params: content.duration === void 0 ? {} : { duration: content.duration },
603
- file: {
604
- field: "voice",
605
- filename: content.name ?? DEFAULT_VOICE_FILENAME,
606
- mimeType: content.mimeType,
607
- bytes
608
- }
609
- };
610
- }
611
- case "contact": {
612
- const vcf = await toVCard(content);
613
- return {
614
- method: "sendDocument",
615
- params: {},
616
- file: {
617
- field: "document",
618
- filename: VCARD_FILENAME,
619
- mimeType: VCARD_MIME,
620
- bytes: Buffer.from(vcf, "utf8")
621
- }
622
- };
623
- }
624
- case "reply": {
625
- const inner = await buildSend(content.content);
626
- return {
627
- ...inner,
628
- params: {
629
- ...inner.params,
630
- reply_parameters: { message_id: parseMessageId(content.target.id) }
631
- }
632
- };
633
- }
634
- case "custom":
635
- return customToSpec(content.raw);
636
- default:
637
- throw UnsupportedError.content(content.type, TELEGRAM_PLATFORM);
638
- }
639
- };
640
-
641
- // src/providers/telegram/outbound/stream-text.ts
642
- import { sendMessageDraft } from "@photon-ai/telegram-ts";
643
- var MILLIS_PER_SECOND2 = 1e3;
644
- var DRAFT_THROTTLE_MS = 500;
645
- var nextDraftId = 1;
646
- var sendStreamText = async (client, space, content) => {
647
- const chatId = Number(space.id);
648
- if (!(Number.isInteger(chatId) && chatId > 0)) {
649
- throw UnsupportedError.content(
650
- "streamText",
651
- TELEGRAM_PLATFORM,
652
- `message drafts work only in private chats (got chat id "${space.id}").`
653
- );
654
- }
655
- const draftId = nextDraftId;
656
- nextDraftId += 1;
657
- const renderBody = (text) => content.format === "markdown" ? { text: markdownToTelegramHtml(text), parse_mode: "HTML" } : { text };
658
- let lastDraftText;
659
- let lastDraftAt = 0;
660
- let draftsAvailable = true;
661
- const updateDraft = async (text) => {
662
- if (!draftsAvailable || text === lastDraftText) {
663
- return;
664
- }
665
- try {
666
- await sendMessageDraft({
667
- body: { chat_id: chatId, draft_id: draftId, ...renderBody(text) },
668
- client
669
- });
670
- lastDraftText = text;
671
- lastDraftAt = Date.now();
672
- } catch {
673
- draftsAvailable = false;
674
- }
675
- };
676
- await updateDraft("");
677
- let full = "";
678
- for await (const delta of content.stream()) {
679
- full += delta;
680
- if (Date.now() - lastDraftAt >= DRAFT_THROTTLE_MS) {
681
- await updateDraft(full);
682
- }
683
- }
684
- if (!full) {
685
- throw UnsupportedError.content(
686
- "streamText",
687
- TELEGRAM_PLATFORM,
688
- "stream produced no text \u2014 nothing to send."
689
- );
690
- }
691
- const sent = await executeSpec(client, {
692
- method: "sendMessage",
693
- params: { chat_id: space.id, ...renderBody(full) }
694
- });
695
- return {
696
- id: String(sent.message_id),
697
- content: content.format === "markdown" ? asMarkdown(full) : asText(full),
698
- space: { id: space.id },
699
- timestamp: new Date(sent.date * MILLIS_PER_SECOND2)
700
- };
701
- };
702
-
703
- // src/providers/telegram/outbound/send.ts
704
- var MILLIS_PER_SECOND3 = 1e3;
705
- var sendContent = async (client, space, content) => {
706
- const spec = await buildSend(content);
707
- const sent = await executeSpec(client, {
708
- ...spec,
709
- params: { chat_id: space.id, ...spec.params }
710
- });
711
- return {
712
- id: String(sent.message_id),
713
- content,
714
- space: { id: space.id },
715
- timestamp: new Date(sent.date * MILLIS_PER_SECOND3)
716
- };
717
- };
718
- var sendGroup = async (client, space, items) => {
719
- let last;
720
- for (const item of items) {
721
- last = await sendContent(client, space, item.content);
722
- }
723
- return last;
724
- };
725
- var sendReaction = async (client, space, content) => {
726
- const messageId = parseMessageId(content.target.id);
727
- const emoji = normalizeReactionEmoji(content.emoji);
728
- if (!isAllowedReactionEmoji(emoji)) {
729
- throw UnsupportedError.content(
730
- "reaction",
731
- TELEGRAM_PLATFORM,
732
- `"${content.emoji}" is not an allowed Telegram reaction emoji.`
733
- );
734
- }
735
- await setMessageReaction({
736
- body: {
737
- chat_id: space.id,
738
- message_id: messageId,
739
- // `emoji` is runtime-validated above; cast to photon's allowed-emoji union.
740
- reaction: [{ emoji, type: "emoji" }]
741
- },
742
- client
743
- });
744
- const timestamp = /* @__PURE__ */ new Date();
745
- const unixSeconds = Math.floor(timestamp.getTime() / MILLIS_PER_SECOND3);
746
- return {
747
- id: `reaction:${space.id}:${messageId}:${unixSeconds}:bot:${emoji}`,
748
- content,
749
- space: { id: space.id },
750
- timestamp
751
- };
752
- };
753
- var sendTyping = async (client, space, state) => {
754
- if (state === "start") {
755
- await sendChatAction({
756
- body: { action: "typing", chat_id: space.id },
757
- client
758
- });
759
- }
760
- return;
761
- };
762
- var sendEdit = async (client, space, content) => {
763
- const inner = content.content;
764
- if (inner.type !== "text" && inner.type !== "markdown") {
765
- throw UnsupportedError.content(
766
- "edit",
767
- TELEGRAM_PLATFORM,
768
- `only text and markdown content can be edited (got "${inner.type}").`
769
- );
770
- }
771
- const body = inner.type === "markdown" ? {
772
- text: markdownToTelegramHtml(inner.markdown),
773
- parse_mode: "HTML"
774
- } : { text: inner.text };
775
- await editMessageText({
776
- body: {
777
- chat_id: space.id,
778
- message_id: parseMessageId(content.target.id),
779
- ...body
780
- },
781
- client
782
- });
783
- return;
784
- };
785
- var send = async ({
786
- space,
787
- content,
788
- config
789
- }) => {
790
- const client = telegramClient(config);
791
- switch (content.type) {
792
- case "reaction":
793
- return await sendReaction(client, space, content);
794
- case "typing":
795
- return await sendTyping(client, space, content.state);
796
- case "read":
797
- return;
798
- case "edit":
799
- return await sendEdit(client, space, content);
800
- case "group":
801
- return await sendGroup(client, space, content.items);
802
- case "streamText":
803
- return await sendStreamText(client, space, content);
804
- case "poll":
805
- case "poll_option":
806
- case "effect":
807
- case "rename":
808
- case "avatar":
809
- throw UnsupportedError.content(content.type, TELEGRAM_PLATFORM);
810
- default:
811
- return await sendContent(client, space, content);
812
- }
813
- };
814
-
815
- // src/providers/telegram/space.ts
816
- var resolveUser = ({
817
- input
818
- }) => Promise.resolve({ id: input.userID });
819
- var createSpace = ({
820
- input
821
- }) => {
822
- const [first, ...rest] = input.users;
823
- if (first && rest.length === 0) {
824
- return Promise.resolve({ id: first.id });
825
- }
826
- if (!first) {
827
- throw new Error("Telegram space creation requires a recipient user.");
828
- }
829
- throw new Error(
830
- "Telegram bots cannot create group chats \u2014 use space.get(chatId) for an existing chat, or create a space with a single user (their private chat)."
831
- );
832
- };
833
-
834
- // src/providers/telegram/verify.ts
835
- import { timingSafeEqual } from "crypto";
836
- var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
837
- var safeEqual = (a, b) => {
838
- const left = Buffer.from(a, "utf8");
839
- const right = Buffer.from(b, "utf8");
840
- if (left.length === 0 || left.length !== right.length) {
841
- return false;
842
- }
843
- return timingSafeEqual(left, right);
844
- };
845
- var verifySecret = (headers, secret) => {
846
- const provided = headers[SECRET_TOKEN_HEADER];
847
- if (!provided) {
848
- throw new Error("Telegram webhook is missing the secret token header");
849
- }
850
- if (!safeEqual(provided, secret)) {
851
- throw new Error("Telegram webhook secret token mismatch");
852
- }
853
- };
854
- var isUpdate = (value) => typeof value === "object" && value !== null && "update_id" in value && typeof value.update_id === "number";
855
- var parseUpdate = (bodyText) => {
856
- let json;
857
- try {
858
- json = JSON.parse(bodyText);
859
- } catch {
860
- throw new Error("Telegram webhook body is not valid JSON");
861
- }
862
- if (!isUpdate(json)) {
863
- throw new Error("Telegram webhook payload is missing a numeric update_id");
864
- }
865
- return json;
866
- };
867
- var verify = (config) => (req) => {
868
- if (config.webhookSecret) {
869
- verifySecret(req.headers, config.webhookSecret);
870
- }
871
- return parseUpdate(new TextDecoder().decode(req.rawBody));
872
- };
873
-
874
- // src/providers/telegram/webhook.ts
875
- import { getWebhookInfo, setWebhook } from "@photon-ai/telegram-ts";
876
- var DEFAULT_SUPER_WEBHOOK_DOMAIN = "spctrm.dev";
877
- var webhookUrl = (slug) => {
878
- const domain = process.env.SPECTRUM_SUPER_WEBHOOK ?? DEFAULT_SUPER_WEBHOOK_DOMAIN;
879
- return `https://${slug}.${domain}/${TELEGRAM_PLATFORM}`;
880
- };
881
- var ensureWebhook = async (config, slug) => {
882
- const client = telegramClient(config);
883
- const url = webhookUrl(slug);
884
- try {
885
- const info = await getWebhookInfo({ client, throwOnError: true });
886
- if (info.result?.url === url) {
887
- return;
888
- }
889
- await setWebhook({
890
- body: {
891
- url,
892
- ...config.webhookSecret ? { secret_token: config.webhookSecret } : {}
893
- },
894
- client,
895
- throwOnError: true
896
- });
897
- } catch (error) {
898
- throw new Error(`Telegram webhook registration failed for ${url}`, {
899
- cause: error
900
- });
901
- }
902
- };
903
-
904
- // src/providers/telegram/index.ts
905
- var telegram = definePlatform(TELEGRAM_PLATFORM, {
906
- config: configSchema,
907
- lifecycle: {
908
- // Annotate the return so overload selection sees the `FusorClient` brand
909
- // without deferring this (context-sensitive) arrow — picks the fusor overload.
910
- createClient: async ({
911
- config,
912
- projectConfig
913
- }) => {
914
- const slug = projectConfig?.slug;
915
- if (slug) {
916
- await ensureWebhook(config, slug);
917
- }
918
- return fusor(TELEGRAM_PLATFORM, verify(config));
919
- }
920
- },
921
- user: { resolve: resolveUser },
922
- space: { create: createSpace },
923
- messages: handleMessages,
924
- send
925
- });
926
-
927
- export {
928
- telegram
929
- };