spectrum-ts 1.17.1 → 2.0.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/README.md +11 -1
- package/dist/{attachment-DfWSZS5L.d.ts → attachment-B4nSrKVd.d.ts} +1 -1
- package/dist/{authoring-C9uDdZ2F.d.ts → authoring-BjE5BvlO.d.ts} +2 -2
- package/dist/authoring.d.ts +3 -3
- package/dist/authoring.js +6 -3
- package/dist/chunk-34FQGGD7.js +34 -0
- package/dist/chunk-3B4QH4JG.js +35 -0
- package/dist/chunk-3GEJYGZK.js +84 -0
- package/dist/chunk-5LT5J3NR.js +695 -0
- package/dist/{chunk-MC6ZKFSG.js → chunk-5XEFJBN2.js} +25 -103
- package/dist/{chunk-JQN6CRSC.js → chunk-6BI4PFTP.js} +10 -39
- package/dist/{chunk-QGJFZMD5.js → chunk-6UZFVXQF.js} +17 -101
- package/dist/{chunk-YJMPSD3S.js → chunk-ATNAE7OR.js} +196 -47
- package/dist/{chunk-IPOFBAIM.js → chunk-NGC4DJIX.js} +23 -19
- package/dist/{chunk-5TIF3FIE.js → chunk-Q537JPTG.js} +8 -6
- package/dist/{chunk-5BKZJMZV.js → chunk-U3LXXT3W.js} +61 -32
- package/dist/chunk-U7AWXDH6.js +91 -0
- package/dist/{chunk-3OTECDNH.js → chunk-WXY5QP3M.js} +5 -3
- package/dist/index.d.ts +71 -126
- package/dist/index.js +350 -90
- package/dist/manifest.json +6 -0
- package/dist/providers/imessage/index.d.ts +75 -3
- package/dist/providers/imessage/index.js +10 -5
- package/dist/providers/index.d.ts +5 -2
- package/dist/providers/index.js +16 -8
- package/dist/providers/slack/index.d.ts +1 -1
- package/dist/providers/slack/index.js +4 -3
- package/dist/providers/telegram/index.d.ts +47 -0
- package/dist/providers/telegram/index.js +13 -0
- package/dist/providers/terminal/index.d.ts +17 -419
- package/dist/providers/terminal/index.js +5 -3
- package/dist/providers/whatsapp-business/index.d.ts +1 -1
- package/dist/providers/whatsapp-business/index.js +6 -4
- package/dist/types-BD0-kKyv.d.ts +82 -0
- package/dist/{types-DcQ5a7PK.d.ts → types-Bje8aq1k.d.ts} +34 -4
- package/package.json +3 -2
|
@@ -0,0 +1,695 @@
|
|
|
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
|
+
asGroup
|
|
7
|
+
} from "./chunk-3B4QH4JG.js";
|
|
8
|
+
import {
|
|
9
|
+
asVoice
|
|
10
|
+
} from "./chunk-NNY6LMSC.js";
|
|
11
|
+
import {
|
|
12
|
+
toVCard
|
|
13
|
+
} from "./chunk-6UZFVXQF.js";
|
|
14
|
+
import {
|
|
15
|
+
UnsupportedError,
|
|
16
|
+
definePlatform
|
|
17
|
+
} from "./chunk-NGC4DJIX.js";
|
|
18
|
+
import {
|
|
19
|
+
asAttachment,
|
|
20
|
+
asCustom,
|
|
21
|
+
asReaction,
|
|
22
|
+
asText
|
|
23
|
+
} from "./chunk-2ILTJC35.js";
|
|
24
|
+
|
|
25
|
+
// src/providers/telegram/config.ts
|
|
26
|
+
import z from "zod";
|
|
27
|
+
var TELEGRAM_PLATFORM = "telegram";
|
|
28
|
+
var DEFAULT_BASE_URL = "https://api.telegram.org";
|
|
29
|
+
var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
|
|
30
|
+
var BOT_TOKEN_PATTERN = /^\d+:[A-Za-z0-9_-]+$/;
|
|
31
|
+
var configSchema = z.object({
|
|
32
|
+
/** Bot token from @BotFather (outbound API calls + media downloads). */
|
|
33
|
+
botToken: z.string().regex(BOT_TOKEN_PATTERN, "botToken must be in the form '<id>:<token>'"),
|
|
34
|
+
/**
|
|
35
|
+
* The `secret_token` passed to `setWebhook`. When present, inbound webhooks
|
|
36
|
+
* are verified against the `X-Telegram-Bot-Api-Secret-Token` header; when
|
|
37
|
+
* omitted, the check is skipped. Telegram does not HMAC-sign the body, so
|
|
38
|
+
* this shared token is the only inbound authentication.
|
|
39
|
+
*/
|
|
40
|
+
webhookSecret: z.string().regex(SECRET_TOKEN_PATTERN).optional(),
|
|
41
|
+
/** Override the Bot API base URL. Defaults to `https://api.telegram.org`. */
|
|
42
|
+
baseUrl: z.url().default(DEFAULT_BASE_URL)
|
|
43
|
+
});
|
|
44
|
+
var botIdFromToken = (botToken) => botToken.split(":")[0] ?? "";
|
|
45
|
+
|
|
46
|
+
// src/providers/telegram/client.ts
|
|
47
|
+
import { createTelegramClient, getFile } from "@photon-ai/telegram-ts";
|
|
48
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
49
|
+
var TRAILING_SLASHES = /\/+$/;
|
|
50
|
+
var telegramClient = (config) => createTelegramClient({ token: config.botToken, baseUrl: config.baseUrl });
|
|
51
|
+
var appendFormValue = (form, key, value) => {
|
|
52
|
+
if (typeof value === "string" || value instanceof Blob) {
|
|
53
|
+
form.append(key, value);
|
|
54
|
+
} else if (value instanceof Date) {
|
|
55
|
+
form.append(key, value.toISOString());
|
|
56
|
+
} else {
|
|
57
|
+
form.append(key, JSON.stringify(value));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var toFormData = (body) => {
|
|
61
|
+
const form = new FormData();
|
|
62
|
+
for (const [key, value] of Object.entries(body)) {
|
|
63
|
+
if (value === void 0 || value === null) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
appendFormValue(form, key, value);
|
|
67
|
+
}
|
|
68
|
+
return form;
|
|
69
|
+
};
|
|
70
|
+
var executeSpec = async (client, spec) => {
|
|
71
|
+
const url = `/${spec.method}`;
|
|
72
|
+
if (spec.file) {
|
|
73
|
+
const file = new File(
|
|
74
|
+
[new Uint8Array(spec.file.bytes)],
|
|
75
|
+
spec.file.filename,
|
|
76
|
+
{ type: spec.file.mimeType }
|
|
77
|
+
);
|
|
78
|
+
const res2 = await client.post({
|
|
79
|
+
body: { ...spec.params, [spec.file.field]: file },
|
|
80
|
+
bodySerializer: toFormData,
|
|
81
|
+
headers: { "Content-Type": null },
|
|
82
|
+
throwOnError: true,
|
|
83
|
+
url
|
|
84
|
+
});
|
|
85
|
+
return res2.data.result;
|
|
86
|
+
}
|
|
87
|
+
const res = await client.post({ body: spec.params, throwOnError: true, url });
|
|
88
|
+
return res.data.result;
|
|
89
|
+
};
|
|
90
|
+
var downloadFile = async (config, fileId) => {
|
|
91
|
+
const client = telegramClient(config);
|
|
92
|
+
const meta = await getFile({
|
|
93
|
+
body: { file_id: fileId },
|
|
94
|
+
client,
|
|
95
|
+
throwOnError: true
|
|
96
|
+
});
|
|
97
|
+
const filePath = meta.result?.file_path;
|
|
98
|
+
if (!filePath) {
|
|
99
|
+
throw new Error(`Telegram getFile returned no file_path for ${fileId}`);
|
|
100
|
+
}
|
|
101
|
+
const base = config.baseUrl.replace(TRAILING_SLASHES, "");
|
|
102
|
+
const res = await fetch(`${base}/file/bot${config.botToken}/${filePath}`, {
|
|
103
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Telegram media download failed: ${res.status} ${res.statusText}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return Buffer.from(await res.arrayBuffer());
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/providers/telegram/inbound/media.ts
|
|
114
|
+
var DEFAULT_VIDEO_MIME = "video/mp4";
|
|
115
|
+
var DEFAULT_AUDIO_MIME = "audio/mpeg";
|
|
116
|
+
var DEFAULT_VOICE_MIME = "audio/ogg";
|
|
117
|
+
var DEFAULT_DOC_MIME = "application/octet-stream";
|
|
118
|
+
var pixelArea = (photo) => photo.file_size ?? photo.width * photo.height;
|
|
119
|
+
var pickLargestPhoto = (photos) => photos.reduce(
|
|
120
|
+
(best, next) => pixelArea(next) > pixelArea(best) ? next : best
|
|
121
|
+
);
|
|
122
|
+
var lazyRead = (config, fileId) => () => downloadFile(config, fileId);
|
|
123
|
+
var fileAttachment = (config, file, fallbackExt, fallbackMime) => asAttachment({
|
|
124
|
+
id: file.file_id,
|
|
125
|
+
name: file.file_name ?? `${file.file_unique_id}.${fallbackExt}`,
|
|
126
|
+
mimeType: file.mime_type ?? fallbackMime,
|
|
127
|
+
size: file.file_size,
|
|
128
|
+
read: lazyRead(config, file.file_id)
|
|
129
|
+
});
|
|
130
|
+
var stickerAttachment = (config, sticker) => {
|
|
131
|
+
let ext = "webp";
|
|
132
|
+
let mimeType = "image/webp";
|
|
133
|
+
if (sticker.is_animated) {
|
|
134
|
+
ext = "tgs";
|
|
135
|
+
mimeType = "application/x-tgsticker";
|
|
136
|
+
} else if (sticker.is_video) {
|
|
137
|
+
ext = "webm";
|
|
138
|
+
mimeType = "video/webm";
|
|
139
|
+
}
|
|
140
|
+
return asAttachment({
|
|
141
|
+
id: sticker.file_id,
|
|
142
|
+
name: `${sticker.file_unique_id}.${ext}`,
|
|
143
|
+
mimeType,
|
|
144
|
+
size: sticker.file_size,
|
|
145
|
+
read: lazyRead(config, sticker.file_id)
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
var mediaToContent = (msg, config) => {
|
|
149
|
+
if (msg.voice) {
|
|
150
|
+
return asVoice({
|
|
151
|
+
mimeType: msg.voice.mime_type ?? DEFAULT_VOICE_MIME,
|
|
152
|
+
duration: msg.voice.duration,
|
|
153
|
+
size: msg.voice.file_size,
|
|
154
|
+
read: lazyRead(config, msg.voice.file_id)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (msg.video_note) {
|
|
158
|
+
return fileAttachment(config, msg.video_note, "mp4", DEFAULT_VIDEO_MIME);
|
|
159
|
+
}
|
|
160
|
+
if (msg.animation) {
|
|
161
|
+
return fileAttachment(config, msg.animation, "mp4", DEFAULT_VIDEO_MIME);
|
|
162
|
+
}
|
|
163
|
+
if (msg.video) {
|
|
164
|
+
return fileAttachment(config, msg.video, "mp4", DEFAULT_VIDEO_MIME);
|
|
165
|
+
}
|
|
166
|
+
if (msg.audio) {
|
|
167
|
+
return fileAttachment(config, msg.audio, "mp3", DEFAULT_AUDIO_MIME);
|
|
168
|
+
}
|
|
169
|
+
if (msg.document) {
|
|
170
|
+
return fileAttachment(config, msg.document, "bin", DEFAULT_DOC_MIME);
|
|
171
|
+
}
|
|
172
|
+
if (msg.photo && msg.photo.length > 0) {
|
|
173
|
+
return fileAttachment(
|
|
174
|
+
config,
|
|
175
|
+
pickLargestPhoto(msg.photo),
|
|
176
|
+
"jpg",
|
|
177
|
+
"image/jpeg"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (msg.sticker) {
|
|
181
|
+
return stickerAttachment(config, msg.sticker);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
};
|
|
185
|
+
var messageToContent = (msg, config) => {
|
|
186
|
+
const media = mediaToContent(msg, config);
|
|
187
|
+
if (media) {
|
|
188
|
+
const caption = msg.caption?.trim();
|
|
189
|
+
return caption ? [asText(caption), media] : [media];
|
|
190
|
+
}
|
|
191
|
+
const text = msg.text;
|
|
192
|
+
if (text !== void 0 && text.length > 0) {
|
|
193
|
+
return [asText(text)];
|
|
194
|
+
}
|
|
195
|
+
return [];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// src/providers/telegram/inbound/messages.ts
|
|
199
|
+
var MILLIS_PER_SECOND = 1e3;
|
|
200
|
+
var stubMessage = (id, content) => ({ id, content });
|
|
201
|
+
var senderRef = (user) => ({
|
|
202
|
+
id: String(user.id),
|
|
203
|
+
...user.username ? { handle: user.username } : {},
|
|
204
|
+
isMe: false
|
|
205
|
+
});
|
|
206
|
+
var toRecordContent = (contents, messageId) => {
|
|
207
|
+
if (contents.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (contents.length === 1) {
|
|
211
|
+
return contents[0];
|
|
212
|
+
}
|
|
213
|
+
return asGroup({
|
|
214
|
+
items: contents.map(
|
|
215
|
+
(content, index) => stubMessage(`${messageId}:${index}`, content)
|
|
216
|
+
)
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
var fromMessage = (msg, config) => {
|
|
220
|
+
if (msg.from && String(msg.from.id) === botIdFromToken(config.botToken)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const content = toRecordContent(
|
|
224
|
+
messageToContent(msg, config),
|
|
225
|
+
String(msg.message_id)
|
|
226
|
+
);
|
|
227
|
+
if (!content) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
id: String(msg.message_id),
|
|
232
|
+
content,
|
|
233
|
+
...msg.from ? { sender: senderRef(msg.from) } : {},
|
|
234
|
+
space: { id: String(msg.chat.id) },
|
|
235
|
+
timestamp: new Date(msg.date * MILLIS_PER_SECOND)
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
var emojiReactions = (reactions) => reactions.filter((r) => r.type === "emoji").map((r) => r.emoji);
|
|
239
|
+
var fromReaction = (reaction) => {
|
|
240
|
+
const added = emojiReactions(reaction.new_reaction);
|
|
241
|
+
if (added.length === 0) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const previous = new Set(emojiReactions(reaction.old_reaction));
|
|
245
|
+
const emoji = added.find((e) => !previous.has(e)) ?? added[0];
|
|
246
|
+
if (!emoji) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const target = stubMessage(
|
|
250
|
+
String(reaction.message_id),
|
|
251
|
+
asCustom({ telegram: "reaction-target" })
|
|
252
|
+
);
|
|
253
|
+
const actorId = reaction.user ? String(reaction.user.id) : "anonymous";
|
|
254
|
+
return {
|
|
255
|
+
id: `reaction:${reaction.chat.id}:${reaction.message_id}:${reaction.date}:${actorId}:${emoji}`,
|
|
256
|
+
content: asReaction({ emoji, target }),
|
|
257
|
+
...reaction.user ? { sender: senderRef(reaction.user) } : {},
|
|
258
|
+
space: { id: String(reaction.chat.id) },
|
|
259
|
+
timestamp: new Date(reaction.date * MILLIS_PER_SECOND)
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
var handleMessages = ({
|
|
263
|
+
payload: update,
|
|
264
|
+
config
|
|
265
|
+
}) => {
|
|
266
|
+
const message = update.message ?? update.channel_post;
|
|
267
|
+
if (message) {
|
|
268
|
+
return fromMessage(message, config);
|
|
269
|
+
}
|
|
270
|
+
if (update.message_reaction) {
|
|
271
|
+
return fromReaction(update.message_reaction);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/providers/telegram/outbound/send.ts
|
|
277
|
+
import {
|
|
278
|
+
editMessageText,
|
|
279
|
+
sendChatAction,
|
|
280
|
+
setMessageReaction
|
|
281
|
+
} from "@photon-ai/telegram-ts";
|
|
282
|
+
|
|
283
|
+
// src/providers/telegram/reactions.ts
|
|
284
|
+
var ALLOWED_REACTION_EMOJI = /* @__PURE__ */ new Set([
|
|
285
|
+
"\u{1F44D}",
|
|
286
|
+
"\u{1F44E}",
|
|
287
|
+
"\u2764",
|
|
288
|
+
"\u{1F525}",
|
|
289
|
+
"\u{1F970}",
|
|
290
|
+
"\u{1F44F}",
|
|
291
|
+
"\u{1F601}",
|
|
292
|
+
"\u{1F914}",
|
|
293
|
+
"\u{1F92F}",
|
|
294
|
+
"\u{1F631}",
|
|
295
|
+
"\u{1F92C}",
|
|
296
|
+
"\u{1F622}",
|
|
297
|
+
"\u{1F389}",
|
|
298
|
+
"\u{1F929}",
|
|
299
|
+
"\u{1F92E}",
|
|
300
|
+
"\u{1F4A9}",
|
|
301
|
+
"\u{1F64F}",
|
|
302
|
+
"\u{1F44C}",
|
|
303
|
+
"\u{1F54A}",
|
|
304
|
+
"\u{1F921}",
|
|
305
|
+
"\u{1F971}",
|
|
306
|
+
"\u{1F974}",
|
|
307
|
+
"\u{1F60D}",
|
|
308
|
+
"\u{1F433}",
|
|
309
|
+
"\u2764\u200D\u{1F525}",
|
|
310
|
+
"\u{1F31A}",
|
|
311
|
+
"\u{1F32D}",
|
|
312
|
+
"\u{1F4AF}",
|
|
313
|
+
"\u{1F923}",
|
|
314
|
+
"\u26A1",
|
|
315
|
+
"\u{1F34C}",
|
|
316
|
+
"\u{1F3C6}",
|
|
317
|
+
"\u{1F494}",
|
|
318
|
+
"\u{1F928}",
|
|
319
|
+
"\u{1F610}",
|
|
320
|
+
"\u{1F353}",
|
|
321
|
+
"\u{1F37E}",
|
|
322
|
+
"\u{1F48B}",
|
|
323
|
+
"\u{1F595}",
|
|
324
|
+
"\u{1F608}",
|
|
325
|
+
"\u{1F634}",
|
|
326
|
+
"\u{1F62D}",
|
|
327
|
+
"\u{1F913}",
|
|
328
|
+
"\u{1F47B}",
|
|
329
|
+
"\u{1F468}\u200D\u{1F4BB}",
|
|
330
|
+
"\u{1F440}",
|
|
331
|
+
"\u{1F383}",
|
|
332
|
+
"\u{1F648}",
|
|
333
|
+
"\u{1F607}",
|
|
334
|
+
"\u{1F628}",
|
|
335
|
+
"\u{1F91D}",
|
|
336
|
+
"\u270D",
|
|
337
|
+
"\u{1F917}",
|
|
338
|
+
"\u{1FAE1}",
|
|
339
|
+
"\u{1F385}",
|
|
340
|
+
"\u{1F384}",
|
|
341
|
+
"\u2603",
|
|
342
|
+
"\u{1F485}",
|
|
343
|
+
"\u{1F92A}",
|
|
344
|
+
"\u{1F5FF}",
|
|
345
|
+
"\u{1F192}",
|
|
346
|
+
"\u{1F498}",
|
|
347
|
+
"\u{1F649}",
|
|
348
|
+
"\u{1F984}",
|
|
349
|
+
"\u{1F618}",
|
|
350
|
+
"\u{1F48A}",
|
|
351
|
+
"\u{1F64A}",
|
|
352
|
+
"\u{1F60E}",
|
|
353
|
+
"\u{1F47E}",
|
|
354
|
+
"\u{1F937}\u200D\u2642",
|
|
355
|
+
"\u{1F937}",
|
|
356
|
+
"\u{1F937}\u200D\u2640",
|
|
357
|
+
"\u{1F621}"
|
|
358
|
+
]);
|
|
359
|
+
var VARIATION_SELECTOR_16 = /️/g;
|
|
360
|
+
var stripVariationSelector = (emoji) => emoji.replace(VARIATION_SELECTOR_16, "");
|
|
361
|
+
var isAllowedReactionEmoji = (emoji) => ALLOWED_REACTION_EMOJI.has(stripVariationSelector(emoji));
|
|
362
|
+
var normalizeReactionEmoji = (emoji) => isAllowedReactionEmoji(emoji) ? stripVariationSelector(emoji) : emoji;
|
|
363
|
+
|
|
364
|
+
// src/providers/telegram/outbound/message.ts
|
|
365
|
+
var VCARD_FILENAME = "contact.vcf";
|
|
366
|
+
var VCARD_MIME = "text/vcard";
|
|
367
|
+
var DEFAULT_VOICE_FILENAME = "voice.ogg";
|
|
368
|
+
var parseMessageId = (id) => {
|
|
369
|
+
const messageId = Number(id);
|
|
370
|
+
if (!Number.isInteger(messageId) || messageId <= 0) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Telegram message id must be a positive integer (got "${id}").`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return messageId;
|
|
376
|
+
};
|
|
377
|
+
var customToSpec = (raw) => {
|
|
378
|
+
if (typeof raw === "object" && raw !== null && typeof raw.method === "string") {
|
|
379
|
+
const value = raw;
|
|
380
|
+
const { params } = value;
|
|
381
|
+
if (params !== void 0 && (typeof params !== "object" || params === null || Array.isArray(params))) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
"Telegram custom content `raw.params` must be an object when provided."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
method: value.method,
|
|
388
|
+
params: params ?? {}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
throw new Error(
|
|
392
|
+
"Telegram custom content `raw` must be a `{ method, params }` Bot API call."
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
var attachmentSpec = async (content) => {
|
|
396
|
+
const bytes = await content.read();
|
|
397
|
+
const file = {
|
|
398
|
+
field: "document",
|
|
399
|
+
filename: content.name,
|
|
400
|
+
mimeType: content.mimeType,
|
|
401
|
+
bytes
|
|
402
|
+
};
|
|
403
|
+
if (content.mimeType.startsWith("image/")) {
|
|
404
|
+
return {
|
|
405
|
+
method: "sendPhoto",
|
|
406
|
+
params: {},
|
|
407
|
+
file: { ...file, field: "photo" }
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (content.mimeType.startsWith("video/")) {
|
|
411
|
+
return {
|
|
412
|
+
method: "sendVideo",
|
|
413
|
+
params: {},
|
|
414
|
+
file: { ...file, field: "video" }
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return { method: "sendDocument", params: {}, file };
|
|
418
|
+
};
|
|
419
|
+
var buildSend = async (content) => {
|
|
420
|
+
switch (content.type) {
|
|
421
|
+
case "text":
|
|
422
|
+
return { method: "sendMessage", params: { text: content.text } };
|
|
423
|
+
case "richlink":
|
|
424
|
+
return { method: "sendMessage", params: { text: content.url } };
|
|
425
|
+
case "attachment":
|
|
426
|
+
return await attachmentSpec(content);
|
|
427
|
+
case "voice": {
|
|
428
|
+
const bytes = await content.read();
|
|
429
|
+
return {
|
|
430
|
+
method: "sendVoice",
|
|
431
|
+
params: content.duration === void 0 ? {} : { duration: content.duration },
|
|
432
|
+
file: {
|
|
433
|
+
field: "voice",
|
|
434
|
+
filename: content.name ?? DEFAULT_VOICE_FILENAME,
|
|
435
|
+
mimeType: content.mimeType,
|
|
436
|
+
bytes
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
case "contact": {
|
|
441
|
+
const vcf = await toVCard(content);
|
|
442
|
+
return {
|
|
443
|
+
method: "sendDocument",
|
|
444
|
+
params: {},
|
|
445
|
+
file: {
|
|
446
|
+
field: "document",
|
|
447
|
+
filename: VCARD_FILENAME,
|
|
448
|
+
mimeType: VCARD_MIME,
|
|
449
|
+
bytes: Buffer.from(vcf, "utf8")
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
case "reply": {
|
|
454
|
+
const inner = await buildSend(content.content);
|
|
455
|
+
return {
|
|
456
|
+
...inner,
|
|
457
|
+
params: {
|
|
458
|
+
...inner.params,
|
|
459
|
+
reply_parameters: { message_id: parseMessageId(content.target.id) }
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
case "custom":
|
|
464
|
+
return customToSpec(content.raw);
|
|
465
|
+
default:
|
|
466
|
+
throw UnsupportedError.content(content.type, TELEGRAM_PLATFORM);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/providers/telegram/outbound/send.ts
|
|
471
|
+
var MILLIS_PER_SECOND2 = 1e3;
|
|
472
|
+
var sendContent = async (client, space, content) => {
|
|
473
|
+
const spec = await buildSend(content);
|
|
474
|
+
const sent = await executeSpec(client, {
|
|
475
|
+
...spec,
|
|
476
|
+
params: { chat_id: space.id, ...spec.params }
|
|
477
|
+
});
|
|
478
|
+
return {
|
|
479
|
+
id: String(sent.message_id),
|
|
480
|
+
content,
|
|
481
|
+
space: { id: space.id },
|
|
482
|
+
timestamp: new Date(sent.date * MILLIS_PER_SECOND2)
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
var sendGroup = async (client, space, items) => {
|
|
486
|
+
let last;
|
|
487
|
+
for (const item of items) {
|
|
488
|
+
last = await sendContent(client, space, item.content);
|
|
489
|
+
}
|
|
490
|
+
return last;
|
|
491
|
+
};
|
|
492
|
+
var sendReaction = async (client, space, content) => {
|
|
493
|
+
const messageId = parseMessageId(content.target.id);
|
|
494
|
+
const emoji = normalizeReactionEmoji(content.emoji);
|
|
495
|
+
if (!isAllowedReactionEmoji(emoji)) {
|
|
496
|
+
throw UnsupportedError.content(
|
|
497
|
+
"reaction",
|
|
498
|
+
TELEGRAM_PLATFORM,
|
|
499
|
+
`"${content.emoji}" is not an allowed Telegram reaction emoji.`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
await setMessageReaction({
|
|
503
|
+
body: {
|
|
504
|
+
chat_id: space.id,
|
|
505
|
+
message_id: messageId,
|
|
506
|
+
// `emoji` is runtime-validated above; cast to photon's allowed-emoji union.
|
|
507
|
+
reaction: [{ emoji, type: "emoji" }]
|
|
508
|
+
},
|
|
509
|
+
client
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
};
|
|
513
|
+
var sendTyping = async (client, space, state) => {
|
|
514
|
+
if (state === "start") {
|
|
515
|
+
await sendChatAction({
|
|
516
|
+
body: { action: "typing", chat_id: space.id },
|
|
517
|
+
client
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
};
|
|
522
|
+
var sendEdit = async (client, space, content) => {
|
|
523
|
+
if (content.content.type !== "text") {
|
|
524
|
+
throw UnsupportedError.content(
|
|
525
|
+
"edit",
|
|
526
|
+
TELEGRAM_PLATFORM,
|
|
527
|
+
`only text content can be edited (got "${content.content.type}").`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
await editMessageText({
|
|
531
|
+
body: {
|
|
532
|
+
chat_id: space.id,
|
|
533
|
+
message_id: parseMessageId(content.target.id),
|
|
534
|
+
text: content.content.text
|
|
535
|
+
},
|
|
536
|
+
client
|
|
537
|
+
});
|
|
538
|
+
return;
|
|
539
|
+
};
|
|
540
|
+
var send = async ({
|
|
541
|
+
space,
|
|
542
|
+
content,
|
|
543
|
+
config
|
|
544
|
+
}) => {
|
|
545
|
+
const client = telegramClient(config);
|
|
546
|
+
switch (content.type) {
|
|
547
|
+
case "reaction":
|
|
548
|
+
return await sendReaction(client, space, content);
|
|
549
|
+
case "typing":
|
|
550
|
+
return await sendTyping(client, space, content.state);
|
|
551
|
+
case "edit":
|
|
552
|
+
return await sendEdit(client, space, content);
|
|
553
|
+
case "group":
|
|
554
|
+
return await sendGroup(client, space, content.items);
|
|
555
|
+
case "poll":
|
|
556
|
+
case "poll_option":
|
|
557
|
+
case "effect":
|
|
558
|
+
case "rename":
|
|
559
|
+
case "avatar":
|
|
560
|
+
throw UnsupportedError.content(content.type, TELEGRAM_PLATFORM);
|
|
561
|
+
default:
|
|
562
|
+
return await sendContent(client, space, content);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// src/providers/telegram/space.ts
|
|
567
|
+
import z2 from "zod";
|
|
568
|
+
var spaceParamsSchema = z2.object({
|
|
569
|
+
/**
|
|
570
|
+
* Target a chat directly by id. Telegram chat ids are numbers in the wire
|
|
571
|
+
* format (negative for groups/supergroups); accept either form and store as
|
|
572
|
+
* a string. For a private chat the id equals the user's id.
|
|
573
|
+
*/
|
|
574
|
+
chatId: z2.union([z2.string().min(1), z2.number()]).optional()
|
|
575
|
+
});
|
|
576
|
+
var resolveUser = ({
|
|
577
|
+
input
|
|
578
|
+
}) => Promise.resolve({ id: input.userID });
|
|
579
|
+
var resolveSpace = ({
|
|
580
|
+
input
|
|
581
|
+
}) => {
|
|
582
|
+
const chatId = input.params?.chatId;
|
|
583
|
+
if (chatId !== void 0) {
|
|
584
|
+
return Promise.resolve({ id: String(chatId) });
|
|
585
|
+
}
|
|
586
|
+
const [first, ...rest] = input.users;
|
|
587
|
+
if (first && rest.length === 0) {
|
|
588
|
+
return Promise.resolve({ id: first.id });
|
|
589
|
+
}
|
|
590
|
+
if (!first) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
"Telegram space creation requires params.chatId or a single recipient user."
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
throw new Error(
|
|
596
|
+
"Telegram bots cannot create group chats \u2014 pass params.chatId for an existing chat, or resolve a single user (their private chat)."
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/providers/telegram/verify.ts
|
|
601
|
+
import { timingSafeEqual } from "crypto";
|
|
602
|
+
var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
|
|
603
|
+
var safeEqual = (a, b) => {
|
|
604
|
+
const left = Buffer.from(a, "utf8");
|
|
605
|
+
const right = Buffer.from(b, "utf8");
|
|
606
|
+
if (left.length === 0 || left.length !== right.length) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
return timingSafeEqual(left, right);
|
|
610
|
+
};
|
|
611
|
+
var verifySecret = (headers, secret) => {
|
|
612
|
+
const provided = headers[SECRET_TOKEN_HEADER];
|
|
613
|
+
if (!provided) {
|
|
614
|
+
throw new Error("Telegram webhook is missing the secret token header");
|
|
615
|
+
}
|
|
616
|
+
if (!safeEqual(provided, secret)) {
|
|
617
|
+
throw new Error("Telegram webhook secret token mismatch");
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
var isUpdate = (value) => typeof value === "object" && value !== null && "update_id" in value && typeof value.update_id === "number";
|
|
621
|
+
var parseUpdate = (bodyText) => {
|
|
622
|
+
let json;
|
|
623
|
+
try {
|
|
624
|
+
json = JSON.parse(bodyText);
|
|
625
|
+
} catch {
|
|
626
|
+
throw new Error("Telegram webhook body is not valid JSON");
|
|
627
|
+
}
|
|
628
|
+
if (!isUpdate(json)) {
|
|
629
|
+
throw new Error("Telegram webhook payload is missing a numeric update_id");
|
|
630
|
+
}
|
|
631
|
+
return json;
|
|
632
|
+
};
|
|
633
|
+
var verify = (config) => (req) => {
|
|
634
|
+
if (config.webhookSecret) {
|
|
635
|
+
verifySecret(req.headers, config.webhookSecret);
|
|
636
|
+
}
|
|
637
|
+
return parseUpdate(new TextDecoder().decode(req.rawBody));
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// src/providers/telegram/webhook.ts
|
|
641
|
+
import { getWebhookInfo, setWebhook } from "@photon-ai/telegram-ts";
|
|
642
|
+
var DEFAULT_SUPER_WEBHOOK_DOMAIN = "spctrm.dev";
|
|
643
|
+
var webhookUrl = (slug) => {
|
|
644
|
+
const domain = process.env.SPECTRUM_SUPER_WEBHOOK ?? DEFAULT_SUPER_WEBHOOK_DOMAIN;
|
|
645
|
+
return `https://${slug}.${domain}/${TELEGRAM_PLATFORM}`;
|
|
646
|
+
};
|
|
647
|
+
var ensureWebhook = async (config, slug) => {
|
|
648
|
+
const client = telegramClient(config);
|
|
649
|
+
const url = webhookUrl(slug);
|
|
650
|
+
try {
|
|
651
|
+
const info = await getWebhookInfo({ client, throwOnError: true });
|
|
652
|
+
if (info.result?.url === url) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
await setWebhook({
|
|
656
|
+
body: {
|
|
657
|
+
url,
|
|
658
|
+
...config.webhookSecret ? { secret_token: config.webhookSecret } : {}
|
|
659
|
+
},
|
|
660
|
+
client,
|
|
661
|
+
throwOnError: true
|
|
662
|
+
});
|
|
663
|
+
} catch (error) {
|
|
664
|
+
throw new Error(`Telegram webhook registration failed for ${url}`, {
|
|
665
|
+
cause: error
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/providers/telegram/index.ts
|
|
671
|
+
var telegram = definePlatform(TELEGRAM_PLATFORM, {
|
|
672
|
+
config: configSchema,
|
|
673
|
+
lifecycle: {
|
|
674
|
+
// Annotate the return so overload selection sees the `FusorClient` brand
|
|
675
|
+
// without deferring this (context-sensitive) arrow — picks the fusor overload.
|
|
676
|
+
createClient: async ({
|
|
677
|
+
config,
|
|
678
|
+
projectConfig
|
|
679
|
+
}) => {
|
|
680
|
+
const slug = projectConfig?.slug;
|
|
681
|
+
if (slug) {
|
|
682
|
+
await ensureWebhook(config, slug);
|
|
683
|
+
}
|
|
684
|
+
return fusor(TELEGRAM_PLATFORM, verify(config));
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
user: { resolve: resolveUser },
|
|
688
|
+
space: { params: spaceParamsSchema, resolve: resolveSpace },
|
|
689
|
+
messages: handleMessages,
|
|
690
|
+
send
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
export {
|
|
694
|
+
telegram
|
|
695
|
+
};
|