lazy-gravity 0.2.0 → 0.3.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 +76 -15
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +353 -147
- package/dist/bot/telegramCommands.js +428 -0
- package/dist/bot/telegramMessageHandler.js +304 -0
- package/dist/bot/telegramProjectCommand.js +137 -0
- package/dist/bot/workspaceQueue.js +61 -0
- package/dist/commands/joinCommandHandler.js +4 -1
- package/dist/database/telegramBindingRepository.js +97 -0
- package/dist/database/userPreferenceRepository.js +46 -1
- package/dist/events/interactionCreateHandler.js +36 -0
- package/dist/events/messageCreateHandler.js +11 -7
- package/dist/handlers/approvalButtonAction.js +99 -0
- package/dist/handlers/autoAcceptButtonAction.js +43 -0
- package/dist/handlers/buttonHandler.js +55 -0
- package/dist/handlers/commandHandler.js +44 -0
- package/dist/handlers/errorPopupButtonAction.js +137 -0
- package/dist/handlers/messageHandler.js +70 -0
- package/dist/handlers/modeSelectAction.js +63 -0
- package/dist/handlers/modelButtonAction.js +102 -0
- package/dist/handlers/planningButtonAction.js +118 -0
- package/dist/handlers/selectHandler.js +41 -0
- package/dist/handlers/templateButtonAction.js +54 -0
- package/dist/platform/adapter.js +8 -0
- package/dist/platform/discord/discordAdapter.js +99 -0
- package/dist/platform/discord/index.js +15 -0
- package/dist/platform/discord/wrappers.js +331 -0
- package/dist/platform/index.js +18 -0
- package/dist/platform/richContentBuilder.js +76 -0
- package/dist/platform/telegram/index.js +16 -0
- package/dist/platform/telegram/telegramAdapter.js +195 -0
- package/dist/platform/telegram/telegramFormatter.js +134 -0
- package/dist/platform/telegram/wrappers.js +329 -0
- package/dist/platform/types.js +28 -0
- package/dist/services/approvalDetector.js +15 -2
- package/dist/services/cdpBridgeManager.js +91 -146
- package/dist/services/defaultModelApplicator.js +54 -0
- package/dist/services/modeService.js +16 -1
- package/dist/services/modelService.js +57 -16
- package/dist/services/notificationSender.js +149 -0
- package/dist/services/responseMonitor.js +1 -2
- package/dist/ui/autoAcceptUi.js +37 -0
- package/dist/ui/modeUi.js +38 -1
- package/dist/ui/modelsUi.js +96 -0
- package/dist/ui/outputUi.js +32 -0
- package/dist/ui/projectListUi.js +55 -0
- package/dist/ui/screenshotUi.js +26 -0
- package/dist/ui/sessionPickerUi.js +35 -1
- package/dist/ui/templateUi.js +41 -0
- package/dist/utils/configLoader.js +63 -12
- package/dist/utils/lockfile.js +5 -5
- package/dist/utils/logger.js +7 -0
- package/dist/utils/telegramImageHandler.js +127 -0
- package/package.json +4 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telegram HTML formatter.
|
|
4
|
+
*
|
|
5
|
+
* Converts markdown-like text and RichContent to Telegram-compatible HTML.
|
|
6
|
+
* Telegram supports a subset of HTML tags: <b>, <i>, <code>, <pre>, <s>,
|
|
7
|
+
* <a href="...">, and a few others.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.escapeHtml = escapeHtml;
|
|
11
|
+
exports.markdownToTelegramHtml = markdownToTelegramHtml;
|
|
12
|
+
exports.richContentToHtml = richContentToHtml;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// HTML escaping
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** Escape characters that are special in HTML. */
|
|
17
|
+
function escapeHtml(text) {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/&/g, '&')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
.replace(/"/g, '"')
|
|
23
|
+
.replace(/'/g, ''');
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Markdown -> Telegram HTML
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
/**
|
|
29
|
+
* Convert a limited subset of Markdown to Telegram HTML.
|
|
30
|
+
*
|
|
31
|
+
* Supported conversions:
|
|
32
|
+
* - `**bold**` -> `<b>bold</b>`
|
|
33
|
+
* - `*italic*` -> `<i>italic</i>` (only outside ** pairs)
|
|
34
|
+
* - `` `code` `` -> `<code>code</code>`
|
|
35
|
+
* - ` ```block``` ` -> `<pre>block</pre>`
|
|
36
|
+
* - `~~strike~~` -> `<s>strike</s>`
|
|
37
|
+
* - `[text](url)` -> `<a href="url">text</a>`
|
|
38
|
+
*
|
|
39
|
+
* Text outside these patterns is HTML-escaped.
|
|
40
|
+
*/
|
|
41
|
+
function markdownToTelegramHtml(text) {
|
|
42
|
+
// Process code blocks first (``` ... ```) to avoid inner transformations
|
|
43
|
+
let result = text.replace(/```(?:\w*\n)?([\s\S]*?)```/g, (_match, code) => `<pre>${escapeHtml(code.trim())}</pre>`);
|
|
44
|
+
// Markdown headings (# ... ######) -> bold text
|
|
45
|
+
// Telegram has no heading tags, so we convert to <b>bold</b>
|
|
46
|
+
result = result.replace(/^(#{1,6})\s+(.+)$/gm, (_match, _hashes, content) => `\n<b>${content}</b>`);
|
|
47
|
+
// Inline code (`...`)
|
|
48
|
+
result = result.replace(/`([^`]+)`/g, (_match, code) => `<code>${escapeHtml(code)}</code>`);
|
|
49
|
+
// Links [text](url) - must be processed before other inline markup
|
|
50
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => `<a href="${escapeHtml(url)}">${escapeHtml(linkText)}</a>`);
|
|
51
|
+
// Bold **text** (must come before italic)
|
|
52
|
+
//
|
|
53
|
+
// HTML escaping note: Content inside bold/italic/strikethrough is NOT
|
|
54
|
+
// escaped here. Earlier regex passes (code blocks, inline code, links)
|
|
55
|
+
// have already replaced their matched text with HTML tags (e.g.
|
|
56
|
+
// <code>...</code>). Since the bold regex `.+?` can span text that
|
|
57
|
+
// includes those prior HTML outputs, calling escapeHtml() would
|
|
58
|
+
// double-escape them (e.g. <code>).
|
|
59
|
+
//
|
|
60
|
+
// This is safe because Telegram's Bot API HTML parser rejects unknown
|
|
61
|
+
// tags with a parse error rather than executing them, so raw `<` / `>`
|
|
62
|
+
// in user text will cause a Telegram API error, not an XSS vector.
|
|
63
|
+
result = result.replace(/\*\*(.+?)\*\*/g, (_match, content) => `<b>${content}</b>`);
|
|
64
|
+
// Italic *text* (single asterisk, not inside bold)
|
|
65
|
+
// Same HTML escaping rationale as bold above.
|
|
66
|
+
result = result.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (_match, content) => `<i>${content}</i>`);
|
|
67
|
+
// Strikethrough ~~text~~
|
|
68
|
+
// Same HTML escaping rationale as bold above.
|
|
69
|
+
result = result.replace(/~~(.+?)~~/g, (_match, content) => `<s>${content}</s>`);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// RichContent -> Telegram HTML
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* Format a single field for Telegram display.
|
|
77
|
+
*/
|
|
78
|
+
function formatField(field) {
|
|
79
|
+
const escapedName = escapeHtml(field.name);
|
|
80
|
+
const convertedValue = markdownToTelegramHtml(field.value);
|
|
81
|
+
return `<b>${escapedName}:</b> ${convertedValue}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Group fields into inline groups and standalone fields.
|
|
85
|
+
* Consecutive inline fields are joined with " | ".
|
|
86
|
+
*/
|
|
87
|
+
function formatFields(fields) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
let inlineGroup = [];
|
|
90
|
+
for (const field of fields) {
|
|
91
|
+
if (field.inline) {
|
|
92
|
+
inlineGroup = [...inlineGroup, formatField(field)];
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
if (inlineGroup.length > 0) {
|
|
96
|
+
parts.push(inlineGroup.join(' | '));
|
|
97
|
+
inlineGroup = [];
|
|
98
|
+
}
|
|
99
|
+
parts.push(formatField(field));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Flush any remaining inline group
|
|
103
|
+
if (inlineGroup.length > 0) {
|
|
104
|
+
parts.push(inlineGroup.join(' | '));
|
|
105
|
+
}
|
|
106
|
+
return parts.join('\n');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convert a RichContent object to a single Telegram HTML string.
|
|
110
|
+
*
|
|
111
|
+
* Layout:
|
|
112
|
+
* <b>title</b>\n\n
|
|
113
|
+
* description (markdown-converted)
|
|
114
|
+
* \n<b>fieldName:</b> fieldValue (inline fields separated by " | ")
|
|
115
|
+
* \n\n<i>footer</i>
|
|
116
|
+
*/
|
|
117
|
+
function richContentToHtml(rc) {
|
|
118
|
+
const sections = [];
|
|
119
|
+
if (rc.title) {
|
|
120
|
+
sections.push(`<b>${escapeHtml(rc.title)}</b>`);
|
|
121
|
+
}
|
|
122
|
+
if (rc.description) {
|
|
123
|
+
sections.push(markdownToTelegramHtml(rc.description));
|
|
124
|
+
}
|
|
125
|
+
if (rc.fields && rc.fields.length > 0) {
|
|
126
|
+
sections.push(formatFields(rc.fields));
|
|
127
|
+
}
|
|
128
|
+
// Join title/description/fields with double newline, then append footer
|
|
129
|
+
let html = sections.join('\n\n');
|
|
130
|
+
if (rc.footer) {
|
|
131
|
+
html += `\n\n<i>${escapeHtml(rc.footer)}</i>`;
|
|
132
|
+
}
|
|
133
|
+
return html;
|
|
134
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telegram wrapper functions.
|
|
4
|
+
*
|
|
5
|
+
* Convert Telegram-specific objects to the platform-agnostic types defined
|
|
6
|
+
* in ../types.ts. Uses `TelegramBotLike` interface instead of importing
|
|
7
|
+
* grammy directly, so the code compiles without grammy installed.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.SELECT_CALLBACK_SEP = void 0;
|
|
11
|
+
exports.toTelegramPayload = toTelegramPayload;
|
|
12
|
+
exports.wrapTelegramUser = wrapTelegramUser;
|
|
13
|
+
exports.wrapTelegramChannel = wrapTelegramChannel;
|
|
14
|
+
exports.wrapTelegramMessage = wrapTelegramMessage;
|
|
15
|
+
exports.wrapTelegramCallbackQuery = wrapTelegramCallbackQuery;
|
|
16
|
+
exports.wrapTelegramSentMessage = wrapTelegramSentMessage;
|
|
17
|
+
const telegramFormatter_1 = require("./telegramFormatter");
|
|
18
|
+
function buttonDefToInline(btn) {
|
|
19
|
+
return { text: btn.label, callback_data: btn.customId };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Separator for select menu callback_data: customId + SEP + value.
|
|
23
|
+
* Uses ASCII Unit Separator (0x1F) to avoid collisions with button
|
|
24
|
+
* customIds that legitimately contain colons (e.g. "approve_action:proj:ch").
|
|
25
|
+
*/
|
|
26
|
+
exports.SELECT_CALLBACK_SEP = '\x1f';
|
|
27
|
+
function selectMenuToInlineRows(menu) {
|
|
28
|
+
return menu.options.map((opt) => [
|
|
29
|
+
{ text: opt.label, callback_data: `${menu.customId}${exports.SELECT_CALLBACK_SEP}${opt.value}` },
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
function componentRowsToInlineKeyboard(rows) {
|
|
33
|
+
const keyboard = [];
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
let buttons = [];
|
|
36
|
+
for (const comp of row.components) {
|
|
37
|
+
if (comp.type === 'button') {
|
|
38
|
+
buttons = [...buttons, buttonDefToInline(comp)];
|
|
39
|
+
}
|
|
40
|
+
else if (comp.type === 'selectMenu') {
|
|
41
|
+
// A select menu becomes multiple rows (one per option)
|
|
42
|
+
const menuRows = selectMenuToInlineRows(comp);
|
|
43
|
+
// Flush any accumulated buttons first
|
|
44
|
+
if (buttons.length > 0) {
|
|
45
|
+
keyboard.push([...buttons]);
|
|
46
|
+
buttons = [];
|
|
47
|
+
}
|
|
48
|
+
for (const menuRow of menuRows) {
|
|
49
|
+
keyboard.push(menuRow);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (buttons.length > 0) {
|
|
54
|
+
keyboard.push(buttons);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return keyboard;
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// toTelegramPayload
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/**
|
|
63
|
+
* Convert a platform-agnostic MessagePayload to Telegram send options.
|
|
64
|
+
*
|
|
65
|
+
* - RichContent is rendered to HTML via richContentToHtml
|
|
66
|
+
* - ComponentRow[] become inline_keyboard
|
|
67
|
+
* - text + richContent are combined into one HTML message
|
|
68
|
+
*/
|
|
69
|
+
function toTelegramPayload(payload) {
|
|
70
|
+
const parts = [];
|
|
71
|
+
if (payload.text) {
|
|
72
|
+
parts.push((0, telegramFormatter_1.markdownToTelegramHtml)(payload.text));
|
|
73
|
+
}
|
|
74
|
+
if (payload.richContent) {
|
|
75
|
+
parts.push((0, telegramFormatter_1.richContentToHtml)(payload.richContent));
|
|
76
|
+
}
|
|
77
|
+
const text = parts.join('\n\n') || ' ';
|
|
78
|
+
const options = {
|
|
79
|
+
text,
|
|
80
|
+
parse_mode: 'HTML',
|
|
81
|
+
};
|
|
82
|
+
if (payload.components !== undefined) {
|
|
83
|
+
if (payload.components.length > 0) {
|
|
84
|
+
const keyboard = componentRowsToInlineKeyboard(payload.components);
|
|
85
|
+
if (keyboard.length > 0) {
|
|
86
|
+
return {
|
|
87
|
+
...options,
|
|
88
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Explicitly empty components array => remove existing keyboard
|
|
93
|
+
return {
|
|
94
|
+
...options,
|
|
95
|
+
reply_markup: { inline_keyboard: [] },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Entity wrappers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/** Wrap a Telegram user object to a PlatformUser. */
|
|
104
|
+
function wrapTelegramUser(from) {
|
|
105
|
+
const displayParts = [from.first_name];
|
|
106
|
+
if (from.last_name) {
|
|
107
|
+
displayParts.push(from.last_name);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
id: String(from.id),
|
|
111
|
+
platform: 'telegram',
|
|
112
|
+
username: from.username ?? String(from.id),
|
|
113
|
+
displayName: displayParts.join(' '),
|
|
114
|
+
isBot: from.is_bot,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Try to send a file attachment via Telegram photo/document API.
|
|
119
|
+
* Returns the sent message, or null if file sending is not available.
|
|
120
|
+
*
|
|
121
|
+
* @param toInputFile - Optional converter that wraps Buffer for the Telegram API.
|
|
122
|
+
* grammY requires Buffer wrapped in InputFile; pass `bot.toInputFile` here.
|
|
123
|
+
*/
|
|
124
|
+
async function trySendFile(api, chatId, file, caption, extraOptions, toInputFile) {
|
|
125
|
+
const isImage = file.contentType?.startsWith('image/') || file.name.match(/\.(png|jpe?g|gif|webp)$/i);
|
|
126
|
+
// grammY requires Buffer wrapped in InputFile; use toInputFile if available.
|
|
127
|
+
const inputFile = toInputFile ? toInputFile(file.data, file.name) : file.data;
|
|
128
|
+
if (isImage && api.sendPhoto) {
|
|
129
|
+
return api.sendPhoto(chatId, inputFile, {
|
|
130
|
+
caption,
|
|
131
|
+
parse_mode: caption ? 'HTML' : undefined,
|
|
132
|
+
...extraOptions,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (api.sendDocument) {
|
|
136
|
+
return api.sendDocument(chatId, inputFile, {
|
|
137
|
+
caption,
|
|
138
|
+
parse_mode: caption ? 'HTML' : undefined,
|
|
139
|
+
...extraOptions,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/** Wrap a Telegram chat as a PlatformChannel. */
|
|
145
|
+
function wrapTelegramChannel(api, chatId, toInputFile) {
|
|
146
|
+
const chatIdStr = String(chatId);
|
|
147
|
+
return {
|
|
148
|
+
id: chatIdStr,
|
|
149
|
+
platform: 'telegram',
|
|
150
|
+
name: undefined,
|
|
151
|
+
async send(payload) {
|
|
152
|
+
// Handle file attachments (e.g., screenshots)
|
|
153
|
+
if (payload.files && payload.files.length > 0) {
|
|
154
|
+
const file = payload.files[0];
|
|
155
|
+
const opts = payload.text || payload.richContent
|
|
156
|
+
? toTelegramPayload({ text: payload.text, richContent: payload.richContent })
|
|
157
|
+
: null;
|
|
158
|
+
const caption = opts?.text;
|
|
159
|
+
const sent = await trySendFile(api, chatId, file, caption, undefined, toInputFile);
|
|
160
|
+
if (sent) {
|
|
161
|
+
return wrapTelegramSentMessage(sent, api, chatId);
|
|
162
|
+
}
|
|
163
|
+
// Fallback to text-only if file sending not supported
|
|
164
|
+
}
|
|
165
|
+
const opts = toTelegramPayload(payload);
|
|
166
|
+
const { text, ...rest } = opts;
|
|
167
|
+
const sent = await api.sendMessage(chatId, text, rest);
|
|
168
|
+
return wrapTelegramSentMessage(sent, api, chatId);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build PlatformAttachment[] from a Telegram photo message.
|
|
174
|
+
* Uses the largest photo size (last in the array) and constructs
|
|
175
|
+
* the download URL from the bot token and file_id.
|
|
176
|
+
*/
|
|
177
|
+
function buildPhotoAttachments(photo, botToken) {
|
|
178
|
+
if (photo.length === 0)
|
|
179
|
+
return [];
|
|
180
|
+
// Telegram sends multiple sizes; last is the largest
|
|
181
|
+
const largest = photo[photo.length - 1];
|
|
182
|
+
// URL is constructed later during download via getFile API.
|
|
183
|
+
// Store file_id as the URL so the download utility can resolve it.
|
|
184
|
+
const url = botToken
|
|
185
|
+
? `telegram-file://${largest.file_id}`
|
|
186
|
+
: `telegram-file://${largest.file_id}`;
|
|
187
|
+
return [{
|
|
188
|
+
name: `photo-${largest.file_unique_id}.jpg`,
|
|
189
|
+
contentType: 'image/jpeg',
|
|
190
|
+
url,
|
|
191
|
+
size: largest.file_size ?? 0,
|
|
192
|
+
}];
|
|
193
|
+
}
|
|
194
|
+
/** Wrap a Telegram message as a PlatformMessage. */
|
|
195
|
+
function wrapTelegramMessage(msg, api, toInputFile, botToken) {
|
|
196
|
+
const author = msg.from
|
|
197
|
+
? wrapTelegramUser(msg.from)
|
|
198
|
+
: {
|
|
199
|
+
id: '0',
|
|
200
|
+
platform: 'telegram',
|
|
201
|
+
username: 'unknown',
|
|
202
|
+
displayName: 'Unknown',
|
|
203
|
+
isBot: false,
|
|
204
|
+
};
|
|
205
|
+
const channel = wrapTelegramChannel(api, msg.chat.id, toInputFile);
|
|
206
|
+
// Photo messages: use caption as content, build attachments from photo array
|
|
207
|
+
const content = msg.text ?? msg.caption ?? '';
|
|
208
|
+
const attachments = msg.photo
|
|
209
|
+
? buildPhotoAttachments(msg.photo, botToken)
|
|
210
|
+
: [];
|
|
211
|
+
return {
|
|
212
|
+
id: String(msg.message_id),
|
|
213
|
+
platform: 'telegram',
|
|
214
|
+
content,
|
|
215
|
+
author,
|
|
216
|
+
channel,
|
|
217
|
+
attachments,
|
|
218
|
+
createdAt: new Date(msg.date * 1000),
|
|
219
|
+
async react(emoji) {
|
|
220
|
+
// Telegram Bot API 7.0+ setMessageReaction — limited to 79 emoji.
|
|
221
|
+
// Silently ignore failures (unsupported emoji, old API, etc.).
|
|
222
|
+
if (api.setMessageReaction) {
|
|
223
|
+
await api.setMessageReaction(msg.chat.id, msg.message_id, [{ type: 'emoji', emoji }]).catch(() => { });
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async reply(payload) {
|
|
227
|
+
// Handle file attachments (e.g., screenshots)
|
|
228
|
+
if (payload.files && payload.files.length > 0) {
|
|
229
|
+
const file = payload.files[0];
|
|
230
|
+
const opts = payload.text || payload.richContent
|
|
231
|
+
? toTelegramPayload({ text: payload.text, richContent: payload.richContent })
|
|
232
|
+
: null;
|
|
233
|
+
const caption = opts?.text;
|
|
234
|
+
const sent = await trySendFile(api, msg.chat.id, file, caption, { reply_to_message_id: msg.message_id }, toInputFile);
|
|
235
|
+
if (sent) {
|
|
236
|
+
return wrapTelegramSentMessage(sent, api, msg.chat.id);
|
|
237
|
+
}
|
|
238
|
+
// Fallback to text-only if file sending not supported
|
|
239
|
+
}
|
|
240
|
+
const opts = toTelegramPayload(payload);
|
|
241
|
+
const { text, ...rest } = opts;
|
|
242
|
+
const sent = await api.sendMessage(msg.chat.id, text, {
|
|
243
|
+
...rest,
|
|
244
|
+
reply_to_message_id: msg.message_id,
|
|
245
|
+
});
|
|
246
|
+
return wrapTelegramSentMessage(sent, api, msg.chat.id);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Validate that a chatId is usable for sending messages.
|
|
252
|
+
* Throws a descriptive error if the chatId is synthetic (0).
|
|
253
|
+
*/
|
|
254
|
+
function assertValidChatId(chatId) {
|
|
255
|
+
if (chatId === 0 || chatId === '0') {
|
|
256
|
+
throw new Error('Cannot send message: callback query has no associated chat (chatId is 0). ' +
|
|
257
|
+
'Use answerCallbackQuery instead.');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Wrap a Telegram callback query as a PlatformButtonInteraction. */
|
|
261
|
+
function wrapTelegramCallbackQuery(query, api) {
|
|
262
|
+
const user = wrapTelegramUser(query.from);
|
|
263
|
+
const chatId = query.message?.chat.id ?? 0;
|
|
264
|
+
const channel = wrapTelegramChannel(api, chatId);
|
|
265
|
+
const messageId = query.message ? String(query.message.message_id) : '0';
|
|
266
|
+
const callbackQueryId = query.id;
|
|
267
|
+
return {
|
|
268
|
+
id: query.id,
|
|
269
|
+
platform: 'telegram',
|
|
270
|
+
customId: query.data ?? '',
|
|
271
|
+
user,
|
|
272
|
+
channel,
|
|
273
|
+
messageId,
|
|
274
|
+
async deferUpdate() {
|
|
275
|
+
// Acknowledge the callback query to dismiss the loading indicator
|
|
276
|
+
await api.answerCallbackQuery(callbackQueryId);
|
|
277
|
+
},
|
|
278
|
+
async reply(payload) {
|
|
279
|
+
assertValidChatId(chatId);
|
|
280
|
+
const opts = toTelegramPayload(payload);
|
|
281
|
+
const { text, ...rest } = opts;
|
|
282
|
+
await api.sendMessage(chatId, text, rest);
|
|
283
|
+
},
|
|
284
|
+
async update(payload) {
|
|
285
|
+
if (!query.message)
|
|
286
|
+
return;
|
|
287
|
+
assertValidChatId(chatId);
|
|
288
|
+
const opts = toTelegramPayload(payload);
|
|
289
|
+
const { text, ...rest } = opts;
|
|
290
|
+
await api.editMessageText(chatId, query.message.message_id, text, rest);
|
|
291
|
+
},
|
|
292
|
+
async editReply(payload) {
|
|
293
|
+
if (!query.message)
|
|
294
|
+
return;
|
|
295
|
+
assertValidChatId(chatId);
|
|
296
|
+
const opts = toTelegramPayload(payload);
|
|
297
|
+
const { text, ...rest } = opts;
|
|
298
|
+
await api.editMessageText(chatId, query.message.message_id, text, rest);
|
|
299
|
+
},
|
|
300
|
+
async followUp(payload) {
|
|
301
|
+
assertValidChatId(chatId);
|
|
302
|
+
const opts = toTelegramPayload(payload);
|
|
303
|
+
const { text, ...rest } = opts;
|
|
304
|
+
const sent = await api.sendMessage(chatId, text, rest);
|
|
305
|
+
return wrapTelegramSentMessage(sent, api, chatId);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Sent message wrapper
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
/** Wrap a Telegram API send result as a PlatformSentMessage. */
|
|
313
|
+
function wrapTelegramSentMessage(msg, api, chatId) {
|
|
314
|
+
const msgId = String(msg.message_id ?? msg.id ?? '0');
|
|
315
|
+
return {
|
|
316
|
+
id: msgId,
|
|
317
|
+
platform: 'telegram',
|
|
318
|
+
channelId: String(chatId),
|
|
319
|
+
async edit(payload) {
|
|
320
|
+
const opts = toTelegramPayload(payload);
|
|
321
|
+
const { text, ...rest } = opts;
|
|
322
|
+
const edited = await api.editMessageText(chatId, Number(msgId), text, rest);
|
|
323
|
+
return wrapTelegramSentMessage(edited, api, chatId);
|
|
324
|
+
},
|
|
325
|
+
async delete() {
|
|
326
|
+
await api.deleteMessage(chatId, Number(msgId));
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform abstraction types for multi-platform support.
|
|
4
|
+
*
|
|
5
|
+
* These types provide a common interface layer between Discord, Telegram,
|
|
6
|
+
* and future messaging platforms. All platform-specific code should convert
|
|
7
|
+
* to/from these types at the adapter boundary.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.toPlatformKey = toPlatformKey;
|
|
11
|
+
exports.fromPlatformKey = fromPlatformKey;
|
|
12
|
+
/** Encode a PlatformId to a "platform:id" string key. */
|
|
13
|
+
function toPlatformKey(pid) {
|
|
14
|
+
return `${pid.platform}:${pid.id}`;
|
|
15
|
+
}
|
|
16
|
+
/** Decode a "platform:id" string key to a PlatformId. Returns null on invalid input. */
|
|
17
|
+
function fromPlatformKey(key) {
|
|
18
|
+
const idx = key.indexOf(':');
|
|
19
|
+
if (idx <= 0)
|
|
20
|
+
return null;
|
|
21
|
+
const platform = key.slice(0, idx);
|
|
22
|
+
if (platform !== 'discord' && platform !== 'telegram')
|
|
23
|
+
return null;
|
|
24
|
+
const id = key.slice(idx + 1);
|
|
25
|
+
if (!id)
|
|
26
|
+
return null;
|
|
27
|
+
return { platform: platform, id };
|
|
28
|
+
}
|
|
@@ -192,7 +192,13 @@ function buildClickScript(buttonText) {
|
|
|
192
192
|
ariaLabel.includes(wanted);
|
|
193
193
|
});
|
|
194
194
|
if (!target) return { ok: false, error: 'Button not found: ' + text };
|
|
195
|
-
target.
|
|
195
|
+
const rect = target.getBoundingClientRect();
|
|
196
|
+
const x = rect.left + rect.width / 2;
|
|
197
|
+
const y = rect.top + rect.height / 2;
|
|
198
|
+
const eventInit = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
|
|
199
|
+
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
200
|
+
target.dispatchEvent(new PointerEvent(type, { ...eventInit, pointerId: 1 }));
|
|
201
|
+
}
|
|
196
202
|
return { ok: true };
|
|
197
203
|
})()`;
|
|
198
204
|
}
|
|
@@ -359,7 +365,14 @@ class ApprovalDetector {
|
|
|
359
365
|
*/
|
|
360
366
|
async clickButton(buttonText) {
|
|
361
367
|
try {
|
|
362
|
-
const
|
|
368
|
+
const script = buildClickScript(buttonText);
|
|
369
|
+
const result = await this.runEvaluateScript(script);
|
|
370
|
+
if (result?.ok !== true) {
|
|
371
|
+
logger_1.logger.warn(`[ApprovalDetector] Click failed for "${buttonText}":`, result?.error ?? 'unknown');
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
logger_1.logger.debug(`[ApprovalDetector] Click OK for "${buttonText}"`);
|
|
375
|
+
}
|
|
363
376
|
return result?.ok === true;
|
|
364
377
|
}
|
|
365
378
|
catch (error) {
|