@llblab/pi-telegram 0.46.0 → 0.48.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/AGENTS.md +4 -2
- package/BACKLOG.md +0 -6
- package/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1604 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +115 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1008 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +6 -4
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +3 -3
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +7 -3
- package/lib/bus-leader.ts +44 -19
- package/lib/bus.ts +4 -1
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +7 -0
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +20 -6
- package/lib/outbound-markup.ts +16 -7
- package/lib/status.ts +15 -12
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-display.ts +96 -18
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/show-me/references/telegram-surfaces.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram reply delivery helpers
|
|
3
|
+
* Zones: telegram outbound, native rich markdown, UI/compat rendering transport
|
|
4
|
+
* Owns native assistant replies, rendered UI delivery, guest placeholder rotation, reply transport wiring, and plain text replies
|
|
5
|
+
*/
|
|
6
|
+
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.js";
|
|
7
|
+
import { getTelegramTargetThreadParams, } from "./target.js";
|
|
8
|
+
import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
|
|
9
|
+
import { renderTelegramMessage, } from "./rendering.js";
|
|
10
|
+
export { renderTelegramMessage, };
|
|
11
|
+
export function renderTelegramMarkdownToHtmlDraft(markdown) {
|
|
12
|
+
return renderTelegramMessage(markdown, { mode: "markdown" })
|
|
13
|
+
.map((chunk) => chunk.text)
|
|
14
|
+
.join("\n");
|
|
15
|
+
}
|
|
16
|
+
export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
|
|
17
|
+
export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
|
|
18
|
+
export function createReplyDedupRuntime() {
|
|
19
|
+
const replied = new Map();
|
|
20
|
+
return {
|
|
21
|
+
shouldReply(promptMessageId) {
|
|
22
|
+
if (replied.has(promptMessageId))
|
|
23
|
+
return false;
|
|
24
|
+
replied.set(promptMessageId, true);
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
reset() {
|
|
28
|
+
replied.clear();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// --- Transport-level dedup ---
|
|
33
|
+
const lastRepliedToMessageIdByTarget = new Map();
|
|
34
|
+
let replyDedupGeneration = 0;
|
|
35
|
+
function getReplyDedupTargetKey(chatId, target) {
|
|
36
|
+
const threadId = target?.threadId;
|
|
37
|
+
return typeof threadId === "number"
|
|
38
|
+
? `${chatId}:thread:${threadId}`
|
|
39
|
+
: `${chatId}:private`;
|
|
40
|
+
}
|
|
41
|
+
export function resetTransportReplyDedup() {
|
|
42
|
+
replyDedupGeneration += 1;
|
|
43
|
+
lastRepliedToMessageIdByTarget.clear();
|
|
44
|
+
}
|
|
45
|
+
export function buildTelegramReplyParameters(chatId, messageId, target) {
|
|
46
|
+
if (messageId === undefined || messageId <= 0)
|
|
47
|
+
return undefined;
|
|
48
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
49
|
+
if (lastRepliedToMessageIdByTarget.get(key) === messageId) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
lastRepliedToMessageIdByTarget.set(key, messageId);
|
|
53
|
+
return {
|
|
54
|
+
message_id: messageId,
|
|
55
|
+
allow_sending_without_reply: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Answer publications are caller-serialized. A rejected send releases its anchor;
|
|
59
|
+
// an uncertain ACK retains it because Telegram may already have delivered it.
|
|
60
|
+
export async function withTelegramReplyParameters(chatId, messageId, target, send) {
|
|
61
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
62
|
+
const generation = replyDedupGeneration;
|
|
63
|
+
const previous = lastRepliedToMessageIdByTarget.get(key);
|
|
64
|
+
const parameters = buildTelegramReplyParameters(chatId, messageId, target);
|
|
65
|
+
try {
|
|
66
|
+
return await send(parameters);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (parameters && !isTelegramApiCommitUnknownError(error)
|
|
70
|
+
&& generation === replyDedupGeneration
|
|
71
|
+
&& lastRepliedToMessageIdByTarget.get(key) === messageId) {
|
|
72
|
+
if (previous === undefined)
|
|
73
|
+
lastRepliedToMessageIdByTarget.delete(key);
|
|
74
|
+
else
|
|
75
|
+
lastRepliedToMessageIdByTarget.set(key, previous);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function getAgentMessageField(message, field) {
|
|
81
|
+
if (typeof message !== "object" || message === null || !(field in message)) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return Reflect.get(message, field);
|
|
85
|
+
}
|
|
86
|
+
export function isAssistantAgentMessage(message) {
|
|
87
|
+
return getAgentMessageField(message, "role") === "assistant";
|
|
88
|
+
}
|
|
89
|
+
function extractAgentTextContent(content) {
|
|
90
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
91
|
+
return blocks
|
|
92
|
+
.filter((block) => typeof block === "object" && block !== null && "type" in block)
|
|
93
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
94
|
+
.map((block) => block.text)
|
|
95
|
+
.join("")
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
|
98
|
+
export function getAgentMessageText(message) {
|
|
99
|
+
return extractAgentTextContent(getAgentMessageField(message, "content"));
|
|
100
|
+
}
|
|
101
|
+
export function extractLatestAssistantMessageText(messages) {
|
|
102
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
103
|
+
const message = messages[i];
|
|
104
|
+
if (!message || !isAssistantAgentMessage(message))
|
|
105
|
+
continue;
|
|
106
|
+
const rawStopReason = getAgentMessageField(message, "stopReason");
|
|
107
|
+
const rawErrorMessage = getAgentMessageField(message, "errorMessage");
|
|
108
|
+
const stopReason = typeof rawStopReason === "string" ? rawStopReason : undefined;
|
|
109
|
+
const errorMessage = typeof rawErrorMessage === "string" ? rawErrorMessage : undefined;
|
|
110
|
+
const text = getAgentMessageText(message);
|
|
111
|
+
return { text: text || undefined, stopReason, errorMessage };
|
|
112
|
+
}
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract the run's answer without trusting an empty final assistant message.
|
|
117
|
+
* A low-level run may end with a completed assistant message whose content was
|
|
118
|
+
* suppressed (a companion extension preserving an earlier draft, for example
|
|
119
|
+
* State Flow's fallback final:true patch turn). In that case the run's answer
|
|
120
|
+
* is the latest earlier completed assistant message that carries text;
|
|
121
|
+
* tool-use prefaces, errors, and aborts stay excluded.
|
|
122
|
+
*/
|
|
123
|
+
export function extractRunAssistantMessage(messages) {
|
|
124
|
+
const latest = extractLatestAssistantMessageText(messages);
|
|
125
|
+
if (latest.text || latest.stopReason !== "stop")
|
|
126
|
+
return latest;
|
|
127
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
128
|
+
const message = messages[i];
|
|
129
|
+
if (!message || !isAssistantAgentMessage(message))
|
|
130
|
+
continue;
|
|
131
|
+
const rawStopReason = getAgentMessageField(message, "stopReason");
|
|
132
|
+
if (rawStopReason === "toolUse" ||
|
|
133
|
+
rawStopReason === "error" ||
|
|
134
|
+
rawStopReason === "aborted") {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const text = getAgentMessageText(message);
|
|
138
|
+
if (!text)
|
|
139
|
+
continue;
|
|
140
|
+
const rawErrorMessage = getAgentMessageField(message, "errorMessage");
|
|
141
|
+
return {
|
|
142
|
+
text,
|
|
143
|
+
stopReason: typeof rawStopReason === "string" ? rawStopReason : undefined,
|
|
144
|
+
errorMessage: typeof rawErrorMessage === "string" ? rawErrorMessage : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return latest;
|
|
148
|
+
}
|
|
149
|
+
export function buildTelegramReplyTransport(deps) {
|
|
150
|
+
return {
|
|
151
|
+
sendRenderedChunks: async (chatId, chunks, options) => {
|
|
152
|
+
return sendTelegramRenderedChunks(chatId, chunks, deps, options);
|
|
153
|
+
},
|
|
154
|
+
editRenderedMessage: async (chatId, messageId, chunks, options) => {
|
|
155
|
+
return editTelegramRenderedMessage(chatId, messageId, chunks, deps, options);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export async function sendTelegramRenderedChunks(chatId, chunks, deps, options) {
|
|
160
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
161
|
+
let lastMessageId;
|
|
162
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
163
|
+
const sent = await withTelegramReplyParameters(chatId, index === 0 ? options?.replyToMessageId : undefined, options?.target, (replyParameters) => deps.sendMessage({
|
|
164
|
+
chat_id: chatId,
|
|
165
|
+
text: chunk.text,
|
|
166
|
+
parse_mode: chunk.parseMode,
|
|
167
|
+
reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
168
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
169
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
170
|
+
}));
|
|
171
|
+
lastMessageId = sent.message_id;
|
|
172
|
+
deps.recordOwnership?.({
|
|
173
|
+
chatId,
|
|
174
|
+
messageId: sent.message_id,
|
|
175
|
+
target: options?.target,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return lastMessageId;
|
|
179
|
+
}
|
|
180
|
+
export async function editTelegramRenderedMessage(chatId, messageId, chunks, deps, options) {
|
|
181
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
182
|
+
if (chunks.length === 0)
|
|
183
|
+
return messageId;
|
|
184
|
+
const [firstChunk, ...remainingChunks] = chunks;
|
|
185
|
+
deps.recordOwnership?.({ chatId, messageId, target: options?.target });
|
|
186
|
+
await deps.editMessage({
|
|
187
|
+
chat_id: chatId,
|
|
188
|
+
message_id: messageId,
|
|
189
|
+
text: firstChunk.text,
|
|
190
|
+
parse_mode: firstChunk.parseMode,
|
|
191
|
+
reply_markup: remainingChunks.length === 0 ? options?.replyMarkup : undefined,
|
|
192
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
193
|
+
});
|
|
194
|
+
if (remainingChunks.length > 0) {
|
|
195
|
+
return sendTelegramRenderedChunks(chatId, remainingChunks, deps, {
|
|
196
|
+
replyMarkup: options?.replyMarkup,
|
|
197
|
+
target: options?.target,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return messageId;
|
|
201
|
+
}
|
|
202
|
+
export async function sendTelegramPlainReply(text, deps, options) {
|
|
203
|
+
const chunks = deps.renderTelegramMessage(text, {
|
|
204
|
+
mode: options?.parseMode === "HTML" ? "html" : "plain",
|
|
205
|
+
});
|
|
206
|
+
return deps.sendRenderedChunks(chunks, {
|
|
207
|
+
target: options?.target,
|
|
208
|
+
replyToMessageId: options?.replyToMessageId,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function normalizeIndentedTelegramNativeMarkdownList(line) {
|
|
212
|
+
return line.replace(/^( +|\t+)([-*+] |\d+\. )/, (_match, indent, marker) => {
|
|
213
|
+
const visibleIndent = indent
|
|
214
|
+
.replace(/ /g, "\u00A0")
|
|
215
|
+
.replace(/\t/g, "\u00A0\u00A0");
|
|
216
|
+
return `${visibleIndent}${marker}`;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function normalizeTelegramNativeMarkdownLine(line) {
|
|
220
|
+
let result = normalizeIndentedTelegramNativeMarkdownList(line.replace(/^( {0,3}>)[ \t]/, "$1"));
|
|
221
|
+
const codeSpans = [];
|
|
222
|
+
result = result.replace(/`+[^`]*`+/g, (code) => {
|
|
223
|
+
const token = `\u0000${codeSpans.length}\u0000`;
|
|
224
|
+
codeSpans.push(code);
|
|
225
|
+
return token;
|
|
226
|
+
});
|
|
227
|
+
result = result.replace(/(^|[^\\$])\$([A-Z][A-Z0-9]{1,})(?!\$)(?=\b|[.,;:)/-])/g, (_match, prefix, ticker) => `${prefix}\\$${ticker}`);
|
|
228
|
+
return result.replace(/\u0000(\d+)\u0000/g, (_match, index) => codeSpans[Number(index)] ?? "");
|
|
229
|
+
}
|
|
230
|
+
function hasClosingDisplayMathDelimiter(lines, startIndex) {
|
|
231
|
+
let fence;
|
|
232
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
233
|
+
const line = lines[index] ?? "";
|
|
234
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
235
|
+
if (!fence && line.trim() === "$$")
|
|
236
|
+
return true;
|
|
237
|
+
if (!fence && fenceMatch) {
|
|
238
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
239
|
+
fence = { marker: markerText[0], length: markerText.length };
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (fence &&
|
|
243
|
+
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
|
|
244
|
+
fence = undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
export function normalizeTelegramNativeMarkdown(markdown) {
|
|
250
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
251
|
+
let fence;
|
|
252
|
+
let displayMath = false;
|
|
253
|
+
return lines
|
|
254
|
+
.map((line, index) => {
|
|
255
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
256
|
+
const inFence = fence !== undefined;
|
|
257
|
+
if (!inFence && line.trim() === "$$") {
|
|
258
|
+
if (displayMath) {
|
|
259
|
+
displayMath = false;
|
|
260
|
+
return "```";
|
|
261
|
+
}
|
|
262
|
+
if (hasClosingDisplayMathDelimiter(lines, index)) {
|
|
263
|
+
displayMath = true;
|
|
264
|
+
return "```math";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (displayMath)
|
|
268
|
+
return line;
|
|
269
|
+
if (!inFence && fenceMatch) {
|
|
270
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
271
|
+
fence = {
|
|
272
|
+
marker: markerText[0],
|
|
273
|
+
length: markerText.length,
|
|
274
|
+
};
|
|
275
|
+
return line;
|
|
276
|
+
}
|
|
277
|
+
if (inFence &&
|
|
278
|
+
new RegExp(`^ {0,3}${fence?.marker}{${fence?.length},}\\s*$`).test(line)) {
|
|
279
|
+
fence = undefined;
|
|
280
|
+
return line;
|
|
281
|
+
}
|
|
282
|
+
if (!inFence)
|
|
283
|
+
return normalizeTelegramNativeMarkdownLine(line);
|
|
284
|
+
return line;
|
|
285
|
+
})
|
|
286
|
+
.join("\n");
|
|
287
|
+
}
|
|
288
|
+
export function splitTelegramNativeMarkdown(markdown) {
|
|
289
|
+
const normalizedMarkdown = normalizeTelegramNativeMarkdown(markdown);
|
|
290
|
+
if (normalizedMarkdown.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS &&
|
|
291
|
+
countTelegramNativeMarkdownBlocks(normalizedMarkdown) <=
|
|
292
|
+
TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
293
|
+
return [normalizedMarkdown];
|
|
294
|
+
}
|
|
295
|
+
const chunks = [];
|
|
296
|
+
let current = "";
|
|
297
|
+
let currentBlockCount = 0;
|
|
298
|
+
for (const rawBlock of splitTelegramNativeMarkdownBlocks(normalizedMarkdown)) {
|
|
299
|
+
for (const block of splitTelegramNativeMarkdownCountedBlocks(rawBlock)) {
|
|
300
|
+
const blockCount = countTelegramNativeMarkdownBlocks(block);
|
|
301
|
+
const candidate = current ? `${current}\n\n${block}` : block;
|
|
302
|
+
const exceedsChars = candidate.length > TELEGRAM_RICH_MESSAGE_MAX_CHARS;
|
|
303
|
+
const exceedsBlocks = currentBlockCount + blockCount > TELEGRAM_RICH_MESSAGE_MAX_BLOCKS;
|
|
304
|
+
if (!exceedsChars && !exceedsBlocks) {
|
|
305
|
+
current = candidate;
|
|
306
|
+
currentBlockCount += blockCount;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (current)
|
|
310
|
+
chunks.push(current.trimEnd());
|
|
311
|
+
if (block.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS &&
|
|
312
|
+
blockCount <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
313
|
+
current = block;
|
|
314
|
+
currentBlockCount = blockCount;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
chunks.push(...splitTelegramNativeMarkdownLongBlock(block));
|
|
318
|
+
current = "";
|
|
319
|
+
currentBlockCount = 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (current)
|
|
323
|
+
chunks.push(current.trimEnd());
|
|
324
|
+
return chunks;
|
|
325
|
+
}
|
|
326
|
+
function splitTelegramNativeMarkdownBlocks(markdown) {
|
|
327
|
+
const blocks = [];
|
|
328
|
+
const current = [];
|
|
329
|
+
let fence;
|
|
330
|
+
const flush = () => {
|
|
331
|
+
if (current.length === 0)
|
|
332
|
+
return;
|
|
333
|
+
blocks.push(current.join("\n"));
|
|
334
|
+
current.length = 0;
|
|
335
|
+
};
|
|
336
|
+
for (const line of markdown.split("\n")) {
|
|
337
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
338
|
+
if (!fence && line.trim().length === 0) {
|
|
339
|
+
flush();
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
current.push(line);
|
|
343
|
+
if (!fence && fenceMatch) {
|
|
344
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
345
|
+
fence = { marker: markerText[0], length: markerText.length };
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (fence &&
|
|
349
|
+
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
|
|
350
|
+
fence = undefined;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
flush();
|
|
354
|
+
return blocks;
|
|
355
|
+
}
|
|
356
|
+
function splitTelegramNativeMarkdownCountedBlocks(block) {
|
|
357
|
+
if (countTelegramNativeMarkdownBlocks(block) <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
358
|
+
return [block];
|
|
359
|
+
}
|
|
360
|
+
const chunks = [];
|
|
361
|
+
let current = [];
|
|
362
|
+
let fence;
|
|
363
|
+
for (const line of block.split("\n")) {
|
|
364
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
365
|
+
if (!fence && current.length >= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
|
|
366
|
+
chunks.push(current.join("\n"));
|
|
367
|
+
current = [];
|
|
368
|
+
}
|
|
369
|
+
current.push(line);
|
|
370
|
+
if (!fence && fenceMatch) {
|
|
371
|
+
const markerText = fenceMatch[1] ?? "```";
|
|
372
|
+
fence = { marker: markerText[0], length: markerText.length };
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (fence &&
|
|
376
|
+
new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
|
|
377
|
+
fence = undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (current.length > 0)
|
|
381
|
+
chunks.push(current.join("\n"));
|
|
382
|
+
return chunks;
|
|
383
|
+
}
|
|
384
|
+
function countTelegramNativeMarkdownBlocks(block) {
|
|
385
|
+
if (/^ {0,3}(`{3,}|~{3,})/.test(block))
|
|
386
|
+
return 1;
|
|
387
|
+
const lines = block.split("\n").filter((line) => line.trim().length > 0);
|
|
388
|
+
if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))) {
|
|
389
|
+
return Math.max(1, lines.length);
|
|
390
|
+
}
|
|
391
|
+
return 1;
|
|
392
|
+
}
|
|
393
|
+
function splitTelegramNativeMarkdownLongBlock(block) {
|
|
394
|
+
return (splitTelegramNativeMarkdownLongFenceBlock(block) ??
|
|
395
|
+
splitTelegramNativeMarkdownLongWrappedInlineBlock(block) ??
|
|
396
|
+
splitTelegramNativeMarkdownLongPlainBlock(block));
|
|
397
|
+
}
|
|
398
|
+
function splitTelegramNativeMarkdownLongPlainBlock(block) {
|
|
399
|
+
const chunks = [];
|
|
400
|
+
let remaining = block;
|
|
401
|
+
while (remaining.length > TELEGRAM_RICH_MESSAGE_MAX_CHARS) {
|
|
402
|
+
const window = remaining.slice(0, TELEGRAM_RICH_MESSAGE_MAX_CHARS + 1);
|
|
403
|
+
const splitIndex = findTelegramNativeMarkdownSplitIndex(window);
|
|
404
|
+
chunks.push(remaining.slice(0, splitIndex).trimEnd());
|
|
405
|
+
remaining = remaining.slice(splitIndex).trimStart();
|
|
406
|
+
}
|
|
407
|
+
if (remaining.length > 0)
|
|
408
|
+
chunks.push(remaining);
|
|
409
|
+
return chunks;
|
|
410
|
+
}
|
|
411
|
+
function splitTelegramNativeMarkdownLongFenceBlock(block) {
|
|
412
|
+
const lines = block.split("\n");
|
|
413
|
+
const opening = lines[0] ?? "";
|
|
414
|
+
const closing = lines[lines.length - 1] ?? "";
|
|
415
|
+
const openingMatch = opening?.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
416
|
+
if (!openingMatch || !closing || lines.length < 2)
|
|
417
|
+
return undefined;
|
|
418
|
+
const markerText = openingMatch[1] ?? "```";
|
|
419
|
+
const marker = markerText[0];
|
|
420
|
+
if (!new RegExp(`^ {0,3}${marker}{${markerText.length},}\\s*$`).test(closing)) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
const maxContentLength = TELEGRAM_RICH_MESSAGE_MAX_CHARS - opening.length - closing.length - 2;
|
|
424
|
+
if (maxContentLength <= 0)
|
|
425
|
+
return undefined;
|
|
426
|
+
const content = lines.slice(1, -1).join("\n");
|
|
427
|
+
return splitTelegramNativeMarkdownWrappedContent(content, maxContentLength, (chunk) => `${opening}\n${chunk}${chunk.endsWith("\n") ? "" : "\n"}${closing}`);
|
|
428
|
+
}
|
|
429
|
+
function splitTelegramNativeMarkdownLongWrappedInlineBlock(block) {
|
|
430
|
+
const delimiter = ["**", "__", "~~", "`", "*", "_"].find((candidate) => block.startsWith(candidate) &&
|
|
431
|
+
block.endsWith(candidate) &&
|
|
432
|
+
block.length > candidate.length * 2);
|
|
433
|
+
if (!delimiter)
|
|
434
|
+
return undefined;
|
|
435
|
+
const maxContentLength = TELEGRAM_RICH_MESSAGE_MAX_CHARS - delimiter.length * 2;
|
|
436
|
+
if (maxContentLength <= 0)
|
|
437
|
+
return undefined;
|
|
438
|
+
return splitTelegramNativeMarkdownWrappedContent(block.slice(delimiter.length, -delimiter.length), maxContentLength, (chunk) => `${delimiter}${chunk}${delimiter}`);
|
|
439
|
+
}
|
|
440
|
+
function splitTelegramNativeMarkdownWrappedContent(content, maxContentLength, wrap) {
|
|
441
|
+
const chunks = [];
|
|
442
|
+
let remaining = content;
|
|
443
|
+
while (remaining.length > maxContentLength) {
|
|
444
|
+
const window = remaining.slice(0, maxContentLength + 1);
|
|
445
|
+
const splitIndex = findTelegramNativeMarkdownSplitIndex(window, maxContentLength);
|
|
446
|
+
chunks.push(wrap(remaining.slice(0, splitIndex)));
|
|
447
|
+
remaining = remaining.slice(splitIndex);
|
|
448
|
+
}
|
|
449
|
+
if (remaining.length > 0)
|
|
450
|
+
chunks.push(wrap(remaining));
|
|
451
|
+
return chunks;
|
|
452
|
+
}
|
|
453
|
+
function findTelegramNativeMarkdownSplitIndex(text, hardLimit = TELEGRAM_RICH_MESSAGE_MAX_CHARS) {
|
|
454
|
+
const paragraphIndex = text.lastIndexOf("\n\n", hardLimit);
|
|
455
|
+
if (paragraphIndex > 0)
|
|
456
|
+
return paragraphIndex + 2;
|
|
457
|
+
const lineIndex = text.lastIndexOf("\n", hardLimit);
|
|
458
|
+
if (lineIndex > 0)
|
|
459
|
+
return lineIndex + 1;
|
|
460
|
+
const spaceIndex = text.lastIndexOf(" ", hardLimit);
|
|
461
|
+
if (spaceIndex > 0)
|
|
462
|
+
return spaceIndex + 1;
|
|
463
|
+
return hardLimit;
|
|
464
|
+
}
|
|
465
|
+
export async function sendTelegramNativeMarkdownReply(chatId, replyToMessageId, markdown, deps, options) {
|
|
466
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
467
|
+
let lastMessageId;
|
|
468
|
+
const chunks = splitTelegramNativeMarkdown(markdown);
|
|
469
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
470
|
+
const sent = await withTelegramReplyParameters(chatId, index === 0 ? replyToMessageId : undefined, options?.target, (replyParameters) => deps.sendRichMessage({
|
|
471
|
+
chat_id: chatId,
|
|
472
|
+
rich_message: { markdown: chunk },
|
|
473
|
+
reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
|
|
474
|
+
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
475
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
476
|
+
}));
|
|
477
|
+
lastMessageId = sent.message_id;
|
|
478
|
+
deps.recordOwnership?.({
|
|
479
|
+
chatId,
|
|
480
|
+
messageId: sent.message_id,
|
|
481
|
+
target: options?.target,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return lastMessageId;
|
|
485
|
+
}
|
|
486
|
+
export async function sendTelegramNativeRichMessage(chatId, richMessage, deps, options) {
|
|
487
|
+
const sent = await deps.sendRichMessage({
|
|
488
|
+
chat_id: chatId,
|
|
489
|
+
rich_message: richMessage,
|
|
490
|
+
...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
|
|
491
|
+
});
|
|
492
|
+
deps.recordOwnership?.({ chatId, messageId: sent.message_id, target: options?.target });
|
|
493
|
+
return sent.message_id;
|
|
494
|
+
}
|
|
495
|
+
export function createTelegramRenderedMessageDeliveryRuntime(deps) {
|
|
496
|
+
const replyTransport = buildTelegramReplyTransport({
|
|
497
|
+
recordOwnership: deps.recordOwnership,
|
|
498
|
+
sendMessage: deps.sendMessage,
|
|
499
|
+
editMessage: deps.editMessage,
|
|
500
|
+
});
|
|
501
|
+
return {
|
|
502
|
+
replyTransport,
|
|
503
|
+
...createTelegramRenderedMessageRuntime({
|
|
504
|
+
renderTelegramMessage: deps.renderTelegramMessage ?? renderTelegramMessage,
|
|
505
|
+
replyTransport,
|
|
506
|
+
recordOwnership: deps.recordOwnership,
|
|
507
|
+
getAssistantRenderingMode: deps.getAssistantRenderingMode,
|
|
508
|
+
sendRichMessage: deps.sendRichMessage,
|
|
509
|
+
}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
export function createTelegramRenderedMessageRuntime(deps) {
|
|
513
|
+
return {
|
|
514
|
+
sendTextReply: async (chatId, replyToMessageId, text, options) => {
|
|
515
|
+
return sendTelegramPlainReply(text, {
|
|
516
|
+
renderTelegramMessage: deps.renderTelegramMessage,
|
|
517
|
+
sendRenderedChunks: (chunks, chunkOptions) => deps.replyTransport.sendRenderedChunks(chatId, chunks, {
|
|
518
|
+
target: chunkOptions?.target,
|
|
519
|
+
replyToMessageId: chunkOptions?.replyToMessageId ?? replyToMessageId,
|
|
520
|
+
}),
|
|
521
|
+
}, options);
|
|
522
|
+
},
|
|
523
|
+
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
524
|
+
const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
|
|
525
|
+
if (renderingMode === "html") {
|
|
526
|
+
return deps.replyTransport.sendRenderedChunks(chatId, deps.renderTelegramMessage(markdown, { mode: "markdown" }), {
|
|
527
|
+
replyMarkup: options?.replyMarkup,
|
|
528
|
+
target: options?.target,
|
|
529
|
+
replyToMessageId,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return sendTelegramNativeMarkdownReply(chatId, replyToMessageId, markdown, {
|
|
533
|
+
recordOwnership: deps.recordOwnership,
|
|
534
|
+
sendRichMessage: deps.sendRichMessage,
|
|
535
|
+
}, options);
|
|
536
|
+
},
|
|
537
|
+
editInteractiveMessage: async (chatId, messageId, text, mode, replyMarkup) => {
|
|
538
|
+
await deps.replyTransport.editRenderedMessage(chatId, messageId, deps.renderTelegramMessage(text, { mode }), { replyMarkup });
|
|
539
|
+
},
|
|
540
|
+
sendInteractiveMessage: async (chatId, text, mode, replyMarkup, options) => {
|
|
541
|
+
return deps.replyTransport.sendRenderedChunks(chatId, deps.renderTelegramMessage(text, { mode }), {
|
|
542
|
+
replyMarkup,
|
|
543
|
+
target: options?.target,
|
|
544
|
+
replyToMessageId: options?.replyToMessageId,
|
|
545
|
+
});
|
|
546
|
+
},
|
|
547
|
+
sendSectionRichMessage: (chatId, message, options) => sendTelegramNativeRichMessage(chatId, message, {
|
|
548
|
+
recordOwnership: deps.recordOwnership,
|
|
549
|
+
sendRichMessage: deps.sendRichMessage,
|
|
550
|
+
}, options),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// --- Dedup-wrapped Reply Wrappers ---
|
|
554
|
+
/** Wrap a sendTextReply with reply dedup so only the first message
|
|
555
|
+
* in a turn carries reply metadata. */
|
|
556
|
+
export function dedupSendTextReply(dedup, inner) {
|
|
557
|
+
return async (chatId, replyToMessageId, text, options) => {
|
|
558
|
+
const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
|
|
559
|
+
? replyToMessageId
|
|
560
|
+
: undefined;
|
|
561
|
+
return inner(chatId, effectiveReplyTo, text, options);
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Guest reply sender: answers guest queries with native Rich Markdown content.
|
|
566
|
+
* Guest queries use InlineQueryResult input_message_content rather than chat
|
|
567
|
+
* sendRichMessage, so this stays as a dedicated guest transport adapter.
|
|
568
|
+
*/
|
|
569
|
+
export function createGuestMarkdownReplySender(deps) {
|
|
570
|
+
return async (guestQueryId, markdown) => {
|
|
571
|
+
const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
|
|
572
|
+
await deps.answerGuestQuery(guestQueryId, undefined, {
|
|
573
|
+
richMessage: { markdown: richMarkdown },
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Guest reply editor: replaces the early Guest Mode placeholder ACK with
|
|
579
|
+
* native Rich Markdown content addressed by `inline_message_id` instead of a
|
|
580
|
+
* chat/message pair.
|
|
581
|
+
*/
|
|
582
|
+
export function createGuestMarkdownReplyEditor(deps) {
|
|
583
|
+
return async (inlineMessageId, markdown) => {
|
|
584
|
+
const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
|
|
585
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
586
|
+
richMessage: { markdown: richMarkdown },
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Guest Mode placeholder rotation: the early ACK is the first placeholder
|
|
592
|
+
* frame and the runtime steps the remaining frames once per interval, moving
|
|
593
|
+
* the globe every second while the trailing dots grow once every two seconds.
|
|
594
|
+
*
|
|
595
|
+
* Rotation completes whole six-frame cycles and only stops once at least
|
|
596
|
+
* `TELEGRAM_GUEST_PLACEHOLDER_MIN_MS` has elapsed, so a pending answer holds
|
|
597
|
+
* the finished cycle's last frame (`🌏 Working on it...`) instead of whatever
|
|
598
|
+
* step a hard time cap happens to cut. The `TELEGRAM_GUEST_PLACEHOLDER_MAX_MS`
|
|
599
|
+
* safety bound keeps the edit stream clear of the first flood-control
|
|
600
|
+
* rejections measured in live guest runs (+26.5 s at ~53 edits, +27.8 s at
|
|
601
|
+
* ~28 edits): rotation caps at 23 edits and never starts a frame after 26 s.
|
|
602
|
+
*/
|
|
603
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS = 1_000;
|
|
604
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_MIN_MS = 20_000;
|
|
605
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_MAX_MS = 26_000;
|
|
606
|
+
export const TELEGRAM_GUEST_PLACEHOLDER_FRAMES = [
|
|
607
|
+
"<b>🌎 Working on it.</b>",
|
|
608
|
+
"<b>🌍 Working on it.</b>",
|
|
609
|
+
"<b>🌏 Working on it..</b>",
|
|
610
|
+
"<b>🌎 Working on it..</b>",
|
|
611
|
+
"<b>🌍 Working on it...</b>",
|
|
612
|
+
"<b>🌏 Working on it...</b>",
|
|
613
|
+
];
|
|
614
|
+
export function buildTelegramGuestPlaceholderFrame(step) {
|
|
615
|
+
const frames = TELEGRAM_GUEST_PLACEHOLDER_FRAMES;
|
|
616
|
+
const index = ((step % frames.length) + frames.length) % frames.length;
|
|
617
|
+
return frames[index];
|
|
618
|
+
}
|
|
619
|
+
function getTelegramGuestPlaceholderRetryDelayMs(error, fallbackMs) {
|
|
620
|
+
const retryAfterSeconds = error?.retryAfterSeconds;
|
|
621
|
+
return typeof retryAfterSeconds === "number" && retryAfterSeconds > 0
|
|
622
|
+
? Math.max(fallbackMs, retryAfterSeconds * 1_000)
|
|
623
|
+
: fallbackMs;
|
|
624
|
+
}
|
|
625
|
+
export function createTelegramGuestPlaceholderRuntime(deps) {
|
|
626
|
+
const intervalMs = deps.intervalMs ?? TELEGRAM_GUEST_PLACEHOLDER_FRAME_MS;
|
|
627
|
+
const minMs = deps.minMs ?? TELEGRAM_GUEST_PLACEHOLDER_MIN_MS;
|
|
628
|
+
const maxMs = deps.maxMs ?? TELEGRAM_GUEST_PLACEHOLDER_MAX_MS;
|
|
629
|
+
const now = deps.now ?? Date.now;
|
|
630
|
+
const setTimer = deps.setTimer ??
|
|
631
|
+
((callback, ms) => setTimeout(callback, ms));
|
|
632
|
+
const clearTimer = deps.clearTimer ??
|
|
633
|
+
((timer) => clearTimeout(timer));
|
|
634
|
+
const sessions = new Map();
|
|
635
|
+
const finishRotation = (session, elapsedMs) => {
|
|
636
|
+
if (session.finished)
|
|
637
|
+
return;
|
|
638
|
+
session.finished = true;
|
|
639
|
+
deps.recordRuntimeEvent?.("guest", "Guest placeholder rotation reached its bound", {
|
|
640
|
+
phase: "guest-placeholder-capped",
|
|
641
|
+
minMs,
|
|
642
|
+
maxMs,
|
|
643
|
+
elapsedMs,
|
|
644
|
+
step: session.step,
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
const scheduleFrame = (inlineMessageId, session, delayMs) => {
|
|
648
|
+
if (session.stopped)
|
|
649
|
+
return;
|
|
650
|
+
const elapsedMs = now() - session.startedAtMs;
|
|
651
|
+
const frameCount = TELEGRAM_GUEST_PLACEHOLDER_FRAMES.length;
|
|
652
|
+
const cycleComplete = session.step > 0 && session.step % frameCount === frameCount - 1;
|
|
653
|
+
if (cycleComplete && elapsedMs >= minMs) {
|
|
654
|
+
finishRotation(session, elapsedMs);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (elapsedMs + delayMs > maxMs) {
|
|
658
|
+
finishRotation(session, elapsedMs);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const timer = setTimer(() => {
|
|
662
|
+
session.timer = undefined;
|
|
663
|
+
runFrame(inlineMessageId, session);
|
|
664
|
+
}, delayMs);
|
|
665
|
+
timer.unref?.();
|
|
666
|
+
session.timer = timer;
|
|
667
|
+
};
|
|
668
|
+
const runFrame = (inlineMessageId, session) => {
|
|
669
|
+
if (session.stopped)
|
|
670
|
+
return;
|
|
671
|
+
session.step += 1;
|
|
672
|
+
let nextDelayMs = intervalMs;
|
|
673
|
+
session.inflight = (async () => {
|
|
674
|
+
try {
|
|
675
|
+
await deps.editGuestInlineMessage(inlineMessageId, {
|
|
676
|
+
text: buildTelegramGuestPlaceholderFrame(session.step),
|
|
677
|
+
parseMode: "HTML",
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(error, intervalMs);
|
|
682
|
+
deps.recordRuntimeEvent?.("guest", error, {
|
|
683
|
+
phase: "guest-placeholder-edit",
|
|
684
|
+
retryAfterMs: nextDelayMs,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
session.inflight = undefined;
|
|
689
|
+
scheduleFrame(inlineMessageId, session, nextDelayMs);
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
692
|
+
};
|
|
693
|
+
const stop = async (inlineMessageId) => {
|
|
694
|
+
const session = sessions.get(inlineMessageId);
|
|
695
|
+
if (!session)
|
|
696
|
+
return;
|
|
697
|
+
sessions.delete(inlineMessageId);
|
|
698
|
+
session.stopped = true;
|
|
699
|
+
if (session.timer !== undefined) {
|
|
700
|
+
clearTimer(session.timer);
|
|
701
|
+
session.timer = undefined;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
await session.inflight;
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// Frame failures are reported as runtime events; stopping stays fail-open.
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
return {
|
|
711
|
+
start(inlineMessageId) {
|
|
712
|
+
const existing = sessions.get(inlineMessageId);
|
|
713
|
+
if (existing) {
|
|
714
|
+
existing.stopped = true;
|
|
715
|
+
if (existing.timer !== undefined)
|
|
716
|
+
clearTimer(existing.timer);
|
|
717
|
+
}
|
|
718
|
+
const session = {
|
|
719
|
+
step: 0,
|
|
720
|
+
stopped: false,
|
|
721
|
+
startedAtMs: now(),
|
|
722
|
+
finished: false,
|
|
723
|
+
};
|
|
724
|
+
sessions.set(inlineMessageId, session);
|
|
725
|
+
scheduleFrame(inlineMessageId, session, intervalMs);
|
|
726
|
+
},
|
|
727
|
+
stop,
|
|
728
|
+
stopAll() {
|
|
729
|
+
for (const session of sessions.values()) {
|
|
730
|
+
session.stopped = true;
|
|
731
|
+
if (session.timer !== undefined)
|
|
732
|
+
clearTimer(session.timer);
|
|
733
|
+
}
|
|
734
|
+
sessions.clear();
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|