@llblab/pi-telegram 0.2.9 → 0.2.10
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/AGENTS.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +12 -9
- package/docs/architecture.md +16 -12
- package/index.ts +191 -246
- package/lib/api.ts +277 -42
- package/lib/commands.ts +87 -0
- package/lib/media.ts +70 -1
- package/lib/polling.ts +25 -5
- package/lib/preview.ts +31 -4
- package/lib/rendering.ts +105 -5
- package/lib/turns.ts +86 -0
- package/lib/types.ts +137 -0
- package/lib/updates.ts +64 -2
- package/package.json +1 -1
- package/tests/api.test.ts +243 -1
- package/tests/commands.test.ts +85 -0
- package/tests/media.test.ts +90 -1
- package/tests/polling.test.ts +73 -0
- package/tests/preview.test.ts +39 -0
- package/tests/rendering.test.ts +51 -0
- package/tests/turns.test.ts +115 -0
- package/tests/updates.test.ts +62 -3
package/lib/preview.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface TelegramPreviewStateLike {
|
|
|
23
23
|
|
|
24
24
|
export interface TelegramPreviewRuntimeState extends TelegramPreviewStateLike {
|
|
25
25
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
26
|
+
flushPromise?: Promise<void>;
|
|
27
|
+
flushRequested?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface TelegramSentPreviewMessageLike {
|
|
@@ -105,13 +107,11 @@ export async function clearTelegramPreview(
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
async function performTelegramPreviewFlush(
|
|
109
111
|
chatId: number,
|
|
112
|
+
state: TelegramPreviewRuntimeState,
|
|
110
113
|
deps: TelegramPreviewRuntimeDeps,
|
|
111
114
|
): Promise<void> {
|
|
112
|
-
const state = deps.getState();
|
|
113
|
-
if (!state) return;
|
|
114
|
-
state.flushTimer = undefined;
|
|
115
115
|
const snapshot = buildTelegramPreviewSnapshot({
|
|
116
116
|
state,
|
|
117
117
|
maxMessageLength: deps.maxMessageLength,
|
|
@@ -166,6 +166,33 @@ export async function flushTelegramPreview(
|
|
|
166
166
|
state.lastSentStrategy = snapshot.strategy;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export async function flushTelegramPreview(
|
|
170
|
+
chatId: number,
|
|
171
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const state = deps.getState();
|
|
174
|
+
if (!state) return;
|
|
175
|
+
if (state.flushPromise) {
|
|
176
|
+
state.flushRequested = true;
|
|
177
|
+
await state.flushPromise;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
state.flushTimer = undefined;
|
|
181
|
+
state.flushPromise = (async () => {
|
|
182
|
+
do {
|
|
183
|
+
state.flushRequested = false;
|
|
184
|
+
await performTelegramPreviewFlush(chatId, state, deps);
|
|
185
|
+
} while (deps.getState() === state && state.flushRequested);
|
|
186
|
+
})();
|
|
187
|
+
try {
|
|
188
|
+
await state.flushPromise;
|
|
189
|
+
} finally {
|
|
190
|
+
if (deps.getState() === state) {
|
|
191
|
+
state.flushPromise = undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
export async function finalizeTelegramPreview(
|
|
170
197
|
chatId: number,
|
|
171
198
|
deps: TelegramPreviewRuntimeDeps,
|
package/lib/rendering.ts
CHANGED
|
@@ -14,6 +14,10 @@ function escapeHtml(text: string): string {
|
|
|
14
14
|
.replace(/>/g, ">");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function escapeHtmlAttribute(text: string): string {
|
|
18
|
+
return escapeHtml(text).replace(/"/g, """).replace(/'/g, "'");
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
// --- Plain Preview Rendering ---
|
|
18
22
|
|
|
19
23
|
function splitPlainMarkdownLine(line: string, maxLength = 1500): string[] {
|
|
@@ -746,12 +750,12 @@ function renderInlineMarkdown(text: string): string {
|
|
|
746
750
|
const renderedLabel =
|
|
747
751
|
plainLabel.length > 0 ? plainLabel : link.destination;
|
|
748
752
|
return makeToken(
|
|
749
|
-
`<a href="${
|
|
753
|
+
`<a href="${escapeHtmlAttribute(link.destination)}">${escapeHtml(renderedLabel)}</a>`,
|
|
750
754
|
);
|
|
751
755
|
},
|
|
752
756
|
renderAutolink: (link) => {
|
|
753
757
|
return makeToken(
|
|
754
|
-
`<a href="${
|
|
758
|
+
`<a href="${escapeHtmlAttribute(link.destination)}">${escapeHtml(link.destination)}</a>`,
|
|
755
759
|
);
|
|
756
760
|
},
|
|
757
761
|
});
|
|
@@ -877,9 +881,14 @@ function renderMarkdownTextLines(block: string): string[] {
|
|
|
877
881
|
return rendered;
|
|
878
882
|
}
|
|
879
883
|
|
|
884
|
+
function sanitizeTelegramCodeLanguage(language: string): string {
|
|
885
|
+
return language.split(/\s+/)[0]?.replace(/[^A-Za-z0-9_+.-]/g, "") ?? "";
|
|
886
|
+
}
|
|
887
|
+
|
|
880
888
|
function renderMarkdownCodeBlock(code: string, language?: string): string[] {
|
|
881
|
-
const
|
|
882
|
-
|
|
889
|
+
const safeLanguage = language ? sanitizeTelegramCodeLanguage(language) : "";
|
|
890
|
+
const open = safeLanguage
|
|
891
|
+
? `<pre><code class="language-${escapeHtmlAttribute(safeLanguage)}">`
|
|
883
892
|
: "<pre><code>";
|
|
884
893
|
const close = "</code></pre>";
|
|
885
894
|
const maxContentLength = MAX_MESSAGE_LENGTH - open.length - close.length;
|
|
@@ -1236,6 +1245,94 @@ function chunkParagraphs(text: string): string[] {
|
|
|
1236
1245
|
return chunks;
|
|
1237
1246
|
}
|
|
1238
1247
|
|
|
1248
|
+
interface OpenHtmlTag {
|
|
1249
|
+
name: string;
|
|
1250
|
+
openTag: string;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const TELEGRAM_VOID_HTML_TAGS = new Set(["br", "hr"]);
|
|
1254
|
+
|
|
1255
|
+
function getHtmlTagName(tag: string): string | undefined {
|
|
1256
|
+
return tag.match(/^<\/?\s*([a-zA-Z][\w-]*)/)?.[1]?.toLowerCase();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function isHtmlClosingTag(tag: string): boolean {
|
|
1260
|
+
return /^<\//.test(tag);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function isHtmlSelfClosingTag(tag: string): boolean {
|
|
1264
|
+
return /\/\s*>$/.test(tag);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function getHtmlClosingTags(openTags: OpenHtmlTag[]): string {
|
|
1268
|
+
return [...openTags]
|
|
1269
|
+
.reverse()
|
|
1270
|
+
.map((tag) => `</${tag.name}>`)
|
|
1271
|
+
.join("");
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function getHtmlOpeningTags(openTags: OpenHtmlTag[]): string {
|
|
1275
|
+
return openTags.map((tag) => tag.openTag).join("");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function updateOpenHtmlTags(tag: string, openTags: OpenHtmlTag[]): void {
|
|
1279
|
+
const name = getHtmlTagName(tag);
|
|
1280
|
+
if (!name || TELEGRAM_VOID_HTML_TAGS.has(name)) return;
|
|
1281
|
+
if (isHtmlClosingTag(tag)) {
|
|
1282
|
+
const index = openTags.map((openTag) => openTag.name).lastIndexOf(name);
|
|
1283
|
+
if (index !== -1) openTags.splice(index, 1);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (isHtmlSelfClosingTag(tag)) return;
|
|
1287
|
+
openTags.push({ name, openTag: tag });
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function chunkHtmlPreservingTags(html: string): string[] {
|
|
1291
|
+
if (html.length <= MAX_MESSAGE_LENGTH) return [html];
|
|
1292
|
+
const chunks: string[] = [];
|
|
1293
|
+
const openTags: OpenHtmlTag[] = [];
|
|
1294
|
+
const tagPattern = /<\/?[a-zA-Z][^>]*>/g;
|
|
1295
|
+
let current = "";
|
|
1296
|
+
let index = 0;
|
|
1297
|
+
const flushCurrent = (): void => {
|
|
1298
|
+
if (current.length === 0) return;
|
|
1299
|
+
chunks.push(`${current}${getHtmlClosingTags(openTags)}`);
|
|
1300
|
+
current = getHtmlOpeningTags(openTags);
|
|
1301
|
+
};
|
|
1302
|
+
const appendText = (text: string): void => {
|
|
1303
|
+
let remaining = text;
|
|
1304
|
+
while (remaining.length > 0) {
|
|
1305
|
+
const closingTags = getHtmlClosingTags(openTags);
|
|
1306
|
+
const available =
|
|
1307
|
+
MAX_MESSAGE_LENGTH - current.length - closingTags.length;
|
|
1308
|
+
if (available <= 0) {
|
|
1309
|
+
flushCurrent();
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const slice = remaining.slice(0, available);
|
|
1313
|
+
current += slice;
|
|
1314
|
+
remaining = remaining.slice(slice.length);
|
|
1315
|
+
if (remaining.length > 0) flushCurrent();
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
const appendTag = (tag: string): void => {
|
|
1319
|
+
const closingTags = getHtmlClosingTags(openTags);
|
|
1320
|
+
if (current.length + tag.length + closingTags.length > MAX_MESSAGE_LENGTH) {
|
|
1321
|
+
flushCurrent();
|
|
1322
|
+
}
|
|
1323
|
+
current += tag;
|
|
1324
|
+
updateOpenHtmlTags(tag, openTags);
|
|
1325
|
+
};
|
|
1326
|
+
for (const match of html.matchAll(tagPattern)) {
|
|
1327
|
+
appendText(html.slice(index, match.index));
|
|
1328
|
+
appendTag(match[0]);
|
|
1329
|
+
index = match.index + match[0].length;
|
|
1330
|
+
}
|
|
1331
|
+
appendText(html.slice(index));
|
|
1332
|
+
if (current.length > 0) chunks.push(current);
|
|
1333
|
+
return chunks;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1239
1336
|
export function renderTelegramMessage(
|
|
1240
1337
|
text: string,
|
|
1241
1338
|
options?: { mode?: TelegramRenderMode },
|
|
@@ -1245,7 +1342,10 @@ export function renderTelegramMessage(
|
|
|
1245
1342
|
return chunkParagraphs(text).map((chunk) => ({ text: chunk }));
|
|
1246
1343
|
}
|
|
1247
1344
|
if (mode === "html") {
|
|
1248
|
-
return
|
|
1345
|
+
return chunkHtmlPreservingTags(text).map((chunk) => ({
|
|
1346
|
+
text: chunk,
|
|
1347
|
+
parseMode: "HTML",
|
|
1348
|
+
}));
|
|
1249
1349
|
}
|
|
1250
1350
|
return renderMarkdownToTelegramHtmlChunks(text).map((chunk) => ({
|
|
1251
1351
|
text: chunk,
|
package/lib/turns.ts
CHANGED
|
@@ -89,6 +89,92 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
89
89
|
return prompt;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
93
|
+
promptWithoutAttachments: string;
|
|
94
|
+
attachmentSuffix: string;
|
|
95
|
+
attachmentFiles: DownloadedTelegramTurnFileLike[];
|
|
96
|
+
} {
|
|
97
|
+
const marker = "\n\nTelegram attachments were saved locally:";
|
|
98
|
+
const markerIndex = prompt.indexOf(marker);
|
|
99
|
+
if (markerIndex === -1) {
|
|
100
|
+
return {
|
|
101
|
+
promptWithoutAttachments: prompt,
|
|
102
|
+
attachmentSuffix: "",
|
|
103
|
+
attachmentFiles: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const promptWithoutAttachments = prompt.slice(0, markerIndex);
|
|
107
|
+
const attachmentSuffix = prompt.slice(markerIndex);
|
|
108
|
+
const attachmentFiles = attachmentSuffix
|
|
109
|
+
.split("\n")
|
|
110
|
+
.map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
|
|
111
|
+
.filter((path): path is string => !!path)
|
|
112
|
+
.map((path) => ({ path, fileName: basename(path), isImage: false }));
|
|
113
|
+
return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildEditedTelegramPromptText(options: {
|
|
117
|
+
existingPrompt: string;
|
|
118
|
+
telegramPrefix: string;
|
|
119
|
+
rawText: string;
|
|
120
|
+
}): { text: string; attachmentFiles: DownloadedTelegramTurnFileLike[] } {
|
|
121
|
+
const { promptWithoutAttachments, attachmentSuffix, attachmentFiles } =
|
|
122
|
+
splitTelegramPromptAttachmentSuffix(options.existingPrompt);
|
|
123
|
+
const currentMessageMarker = "Current Telegram message:";
|
|
124
|
+
const currentMessageIndex = promptWithoutAttachments.lastIndexOf(
|
|
125
|
+
currentMessageMarker,
|
|
126
|
+
);
|
|
127
|
+
if (currentMessageIndex !== -1) {
|
|
128
|
+
const prefix = promptWithoutAttachments.slice(
|
|
129
|
+
0,
|
|
130
|
+
currentMessageIndex + currentMessageMarker.length,
|
|
131
|
+
);
|
|
132
|
+
const separator = options.rawText.length > 0 ? "\n" : "";
|
|
133
|
+
return {
|
|
134
|
+
text: `${prefix}${separator}${options.rawText}${attachmentSuffix}`,
|
|
135
|
+
attachmentFiles,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const promptText =
|
|
139
|
+
options.rawText.length > 0
|
|
140
|
+
? `${options.telegramPrefix} ${options.rawText}`
|
|
141
|
+
: options.telegramPrefix;
|
|
142
|
+
return {
|
|
143
|
+
text: `${promptText}${attachmentSuffix}`,
|
|
144
|
+
attachmentFiles,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function updateTelegramPromptTurnText(options: {
|
|
149
|
+
turn: PendingTelegramTurn;
|
|
150
|
+
telegramPrefix: string;
|
|
151
|
+
rawText: string;
|
|
152
|
+
}): PendingTelegramTurn {
|
|
153
|
+
let attachmentFiles: DownloadedTelegramTurnFileLike[] = [];
|
|
154
|
+
const nextContent = options.turn.content.map((block, index) => {
|
|
155
|
+
if (index !== 0 || block.type !== "text") return block;
|
|
156
|
+
const updated = buildEditedTelegramPromptText({
|
|
157
|
+
existingPrompt: block.text,
|
|
158
|
+
telegramPrefix: options.telegramPrefix,
|
|
159
|
+
rawText: options.rawText,
|
|
160
|
+
});
|
|
161
|
+
attachmentFiles = updated.attachmentFiles;
|
|
162
|
+
return {
|
|
163
|
+
...block,
|
|
164
|
+
text: updated.text,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
...options.turn,
|
|
169
|
+
content: nextContent,
|
|
170
|
+
historyText: formatTelegramHistoryText(options.rawText, attachmentFiles),
|
|
171
|
+
statusSummary: formatTelegramTurnStatusSummary(
|
|
172
|
+
options.rawText,
|
|
173
|
+
attachmentFiles,
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
92
178
|
export async function buildTelegramPromptTurn(options: {
|
|
93
179
|
telegramPrefix: string;
|
|
94
180
|
messages: TelegramTurnMessageLike[];
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Bot API transport shape types
|
|
3
|
+
* Centralizes Telegram update, message, callback, reaction, and response shapes used by the runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TelegramApiResponse<T> {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
result?: T;
|
|
9
|
+
description?: string;
|
|
10
|
+
error_code?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TelegramUser {
|
|
14
|
+
id: number;
|
|
15
|
+
is_bot: boolean;
|
|
16
|
+
first_name: string;
|
|
17
|
+
username?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TelegramChat {
|
|
21
|
+
id: number;
|
|
22
|
+
type: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TelegramPhotoSize {
|
|
26
|
+
file_id: string;
|
|
27
|
+
file_size?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TelegramDocument {
|
|
31
|
+
file_id: string;
|
|
32
|
+
file_name?: string;
|
|
33
|
+
mime_type?: string;
|
|
34
|
+
file_size?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TelegramVideo {
|
|
38
|
+
file_id: string;
|
|
39
|
+
file_name?: string;
|
|
40
|
+
mime_type?: string;
|
|
41
|
+
file_size?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TelegramAudio {
|
|
45
|
+
file_id: string;
|
|
46
|
+
file_name?: string;
|
|
47
|
+
mime_type?: string;
|
|
48
|
+
file_size?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TelegramVoice {
|
|
52
|
+
file_id: string;
|
|
53
|
+
mime_type?: string;
|
|
54
|
+
file_size?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TelegramAnimation {
|
|
58
|
+
file_id: string;
|
|
59
|
+
file_name?: string;
|
|
60
|
+
mime_type?: string;
|
|
61
|
+
file_size?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TelegramSticker {
|
|
65
|
+
file_id: string;
|
|
66
|
+
emoji?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TelegramMessage {
|
|
70
|
+
message_id: number;
|
|
71
|
+
chat: TelegramChat;
|
|
72
|
+
from?: TelegramUser;
|
|
73
|
+
text?: string;
|
|
74
|
+
caption?: string;
|
|
75
|
+
media_group_id?: string;
|
|
76
|
+
photo?: TelegramPhotoSize[];
|
|
77
|
+
document?: TelegramDocument;
|
|
78
|
+
video?: TelegramVideo;
|
|
79
|
+
audio?: TelegramAudio;
|
|
80
|
+
voice?: TelegramVoice;
|
|
81
|
+
animation?: TelegramAnimation;
|
|
82
|
+
sticker?: TelegramSticker;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TelegramCallbackQuery {
|
|
86
|
+
id: string;
|
|
87
|
+
from: TelegramUser;
|
|
88
|
+
message?: TelegramMessage;
|
|
89
|
+
data?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TelegramReactionTypeEmoji {
|
|
93
|
+
type: "emoji";
|
|
94
|
+
emoji: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface TelegramReactionTypeCustomEmoji {
|
|
98
|
+
type: "custom_emoji";
|
|
99
|
+
custom_emoji_id: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TelegramReactionTypePaid {
|
|
103
|
+
type: "paid";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type TelegramReactionType =
|
|
107
|
+
| TelegramReactionTypeEmoji
|
|
108
|
+
| TelegramReactionTypeCustomEmoji
|
|
109
|
+
| TelegramReactionTypePaid;
|
|
110
|
+
|
|
111
|
+
export interface TelegramMessageReactionUpdated {
|
|
112
|
+
chat: TelegramChat;
|
|
113
|
+
message_id: number;
|
|
114
|
+
user?: TelegramUser;
|
|
115
|
+
actor_chat?: TelegramChat;
|
|
116
|
+
old_reaction: TelegramReactionType[];
|
|
117
|
+
new_reaction: TelegramReactionType[];
|
|
118
|
+
date: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TelegramUpdate {
|
|
122
|
+
update_id: number;
|
|
123
|
+
message?: TelegramMessage;
|
|
124
|
+
edited_message?: TelegramMessage;
|
|
125
|
+
callback_query?: TelegramCallbackQuery;
|
|
126
|
+
message_reaction?: TelegramMessageReactionUpdated;
|
|
127
|
+
deleted_business_messages?: { message_ids?: unknown };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface TelegramSentMessage {
|
|
131
|
+
message_id: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface TelegramBotCommand {
|
|
135
|
+
command: string;
|
|
136
|
+
description: string;
|
|
137
|
+
}
|
package/lib/updates.ts
CHANGED
|
@@ -119,7 +119,22 @@ export function getAuthorizedTelegramCallbackQuery(
|
|
|
119
119
|
export function getAuthorizedTelegramMessage(
|
|
120
120
|
update: TelegramUpdateRoutingLike,
|
|
121
121
|
): TelegramMessageLike | undefined {
|
|
122
|
-
const message = update.message
|
|
122
|
+
const message = update.message;
|
|
123
|
+
if (
|
|
124
|
+
!message ||
|
|
125
|
+
message.chat.type !== "private" ||
|
|
126
|
+
!message.from ||
|
|
127
|
+
message.from.is_bot
|
|
128
|
+
) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return message;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getAuthorizedTelegramEditedMessage(
|
|
135
|
+
update: TelegramUpdateRoutingLike,
|
|
136
|
+
): TelegramMessageLike | undefined {
|
|
137
|
+
const message = update.edited_message;
|
|
123
138
|
if (
|
|
124
139
|
!message ||
|
|
125
140
|
message.chat.type !== "private" ||
|
|
@@ -156,6 +171,11 @@ export type TelegramUpdateFlowAction =
|
|
|
156
171
|
kind: "message";
|
|
157
172
|
message: TelegramMessageLike & { from: TelegramUserLike };
|
|
158
173
|
authorization: TelegramAuthorizationState;
|
|
174
|
+
}
|
|
175
|
+
| {
|
|
176
|
+
kind: "edited-message";
|
|
177
|
+
message: TelegramMessageLike & { from: TelegramUserLike };
|
|
178
|
+
authorization: TelegramAuthorizationState;
|
|
159
179
|
};
|
|
160
180
|
|
|
161
181
|
export function buildTelegramUpdateFlowAction(
|
|
@@ -191,6 +211,19 @@ export function buildTelegramUpdateFlowAction(
|
|
|
191
211
|
),
|
|
192
212
|
};
|
|
193
213
|
}
|
|
214
|
+
const editedMessage = getAuthorizedTelegramEditedMessage(update);
|
|
215
|
+
if (editedMessage?.from) {
|
|
216
|
+
return {
|
|
217
|
+
kind: "edited-message",
|
|
218
|
+
message: editedMessage as TelegramMessageLike & {
|
|
219
|
+
from: TelegramUserLike;
|
|
220
|
+
},
|
|
221
|
+
authorization: getTelegramAuthorizationState(
|
|
222
|
+
editedMessage.from.id,
|
|
223
|
+
allowedUserId,
|
|
224
|
+
),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
194
227
|
return { kind: "ignore" };
|
|
195
228
|
}
|
|
196
229
|
|
|
@@ -215,6 +248,12 @@ export type TelegramUpdateExecutionPlan =
|
|
|
215
248
|
shouldPair: boolean;
|
|
216
249
|
shouldNotifyPaired: boolean;
|
|
217
250
|
shouldDeny: boolean;
|
|
251
|
+
}
|
|
252
|
+
| {
|
|
253
|
+
kind: "edited-message";
|
|
254
|
+
message: TelegramMessageLike & { from: TelegramUserLike };
|
|
255
|
+
shouldPair: boolean;
|
|
256
|
+
shouldDeny: boolean;
|
|
218
257
|
};
|
|
219
258
|
|
|
220
259
|
export function buildTelegramUpdateExecutionPlan(
|
|
@@ -242,6 +281,13 @@ export function buildTelegramUpdateExecutionPlan(
|
|
|
242
281
|
shouldNotifyPaired: action.authorization.kind === "pair",
|
|
243
282
|
shouldDeny: action.authorization.kind === "deny",
|
|
244
283
|
};
|
|
284
|
+
case "edited-message":
|
|
285
|
+
return {
|
|
286
|
+
kind: "edited-message",
|
|
287
|
+
message: action.message,
|
|
288
|
+
shouldPair: action.authorization.kind === "pair",
|
|
289
|
+
shouldDeny: action.authorization.kind === "deny",
|
|
290
|
+
};
|
|
245
291
|
}
|
|
246
292
|
}
|
|
247
293
|
|
|
@@ -296,6 +342,13 @@ export interface TelegramUpdateRuntimeDeps {
|
|
|
296
342
|
>["message"],
|
|
297
343
|
ctx: ExtensionContext,
|
|
298
344
|
) => Promise<void>;
|
|
345
|
+
handleAuthorizedTelegramEditedMessage: (
|
|
346
|
+
message: Extract<
|
|
347
|
+
TelegramUpdateExecutionPlan,
|
|
348
|
+
{ kind: "edited-message" }
|
|
349
|
+
>["message"],
|
|
350
|
+
ctx: ExtensionContext,
|
|
351
|
+
) => Promise<void>;
|
|
299
352
|
}
|
|
300
353
|
|
|
301
354
|
function getTelegramCallbackQueryId(
|
|
@@ -368,7 +421,12 @@ export async function executeTelegramUpdatePlan(
|
|
|
368
421
|
? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx)
|
|
369
422
|
: false;
|
|
370
423
|
const replyTarget = getTelegramMessageReplyTarget(plan.message);
|
|
371
|
-
if (
|
|
424
|
+
if (
|
|
425
|
+
plan.kind === "message" &&
|
|
426
|
+
pairedNow &&
|
|
427
|
+
plan.shouldNotifyPaired &&
|
|
428
|
+
replyTarget
|
|
429
|
+
) {
|
|
372
430
|
await deps.sendTextReply(
|
|
373
431
|
replyTarget.chatId,
|
|
374
432
|
replyTarget.messageId,
|
|
@@ -385,5 +443,9 @@ export async function executeTelegramUpdatePlan(
|
|
|
385
443
|
}
|
|
386
444
|
return;
|
|
387
445
|
}
|
|
446
|
+
if (plan.kind === "edited-message") {
|
|
447
|
+
await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
388
450
|
await deps.handleAuthorizedTelegramMessage(plan.message, deps.ctx);
|
|
389
451
|
}
|