teledzik 1.0.5 → 1.0.7
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 +3 -14
- package/lib/context.js +14 -32
- package/lib/core/helpers/rich-sanitizer.js +27 -45
- package/lib/telegram.js +22 -47
- package/package.json +1 -1
- package/src/context.ts +15 -54
- package/src/core/helpers/rich-sanitizer.ts +25 -53
- package/src/telegram.ts +20 -73
- package/typings/context.d.ts +2 -4
- package/typings/core/helpers/rich-sanitizer.d.ts +0 -5
- package/typings/telegram.d.ts +2 -4
package/README.md
CHANGED
|
@@ -588,9 +588,9 @@ await ctx.sendRichMessage(msg, {
|
|
|
588
588
|
|
|
589
589
|
---
|
|
590
590
|
|
|
591
|
-
###
|
|
591
|
+
### Native `editRichMessage` (Preserving 100% Rich UI)
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
Edit a rich message in-place while preserving 100% Native Rich UI layout (tables, headings, cards, collapsibles):
|
|
594
594
|
|
|
595
595
|
```ts
|
|
596
596
|
import { Telegraf, RichHTMLBuilder, Markup } from 'teledzik'
|
|
@@ -605,23 +605,12 @@ bot.action('btn:update', async (ctx) => {
|
|
|
605
605
|
['VPS-02', '7.2 GB', 'ONLINE 🟢']
|
|
606
606
|
], { bordered: true, striped: true, hasHeader: true })
|
|
607
607
|
|
|
608
|
+
// Edit in-place with native Rich UI preservation
|
|
608
609
|
await ctx.editRichMessage(rich, {
|
|
609
610
|
reply_markup: Markup.inlineKeyboard([
|
|
610
611
|
[Markup.button.primary('🔄 Refresh', 'btn:update')]
|
|
611
612
|
]).reply_markup
|
|
612
613
|
})
|
|
613
|
-
|
|
614
|
-
// 2. Or using an Object
|
|
615
|
-
await ctx.editRichMessage({
|
|
616
|
-
html: '<b>New Content</b>',
|
|
617
|
-
reply_markup: { inline_keyboard: [] }
|
|
618
|
-
})
|
|
619
|
-
|
|
620
|
-
// 3. Or using Raw HTML String
|
|
621
|
-
await ctx.editRichMessage('<b>Updated via raw string</b>')
|
|
622
|
-
|
|
623
|
-
// 4. Or specifying a custom messageId
|
|
624
|
-
await ctx.editRichMessage(12345, '<b>Edit specific message ID</b>')
|
|
625
614
|
})
|
|
626
615
|
```
|
|
627
616
|
|
package/lib/context.js
CHANGED
|
@@ -1069,39 +1069,21 @@ class Context {
|
|
|
1069
1069
|
this.assert(this.chat, 'sendRichMessageDraft');
|
|
1070
1070
|
return this.telegram.sendRichMessageDraft(this.chat.id, draftId, richMessage, extra);
|
|
1071
1071
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
(this.message && 'message_id' in this.message ? this.message.message_id : undefined) ??
|
|
1086
|
-
(this.update && 'edited_message' in this.update && this.update.edited_message
|
|
1087
|
-
? this.update.edited_message.message_id
|
|
1088
|
-
: undefined) ??
|
|
1089
|
-
this.msgId;
|
|
1090
|
-
content = a;
|
|
1091
|
-
extra = b || {};
|
|
1092
|
-
}
|
|
1093
|
-
const chatId = this.chat?.id ??
|
|
1094
|
-
(this.callbackQuery?.message && 'chat' in this.callbackQuery.message
|
|
1095
|
-
? this.callbackQuery.message.chat.id
|
|
1096
|
-
: undefined);
|
|
1097
|
-
const inlineMsgId = this.inlineMessageId ?? extra?.inline_message_id;
|
|
1098
|
-
if (!inlineMsgId && (!chatId || targetMsgId == null)) {
|
|
1099
|
-
throw new Error('[teledzik] Tidak dapat menemukan chatId atau messageId dari konteks pesan saat ini.');
|
|
1100
|
-
}
|
|
1101
|
-
if (inlineMsgId && !extra.inline_message_id) {
|
|
1102
|
-
extra.inline_message_id = inlineMsgId;
|
|
1072
|
+
/**
|
|
1073
|
+
* Context-aware shorthand for {@link Telegram.editRichMessage}.
|
|
1074
|
+
*/
|
|
1075
|
+
editRichMessage(richMessage, extra) {
|
|
1076
|
+
this.assert(this.chat, 'editRichMessage');
|
|
1077
|
+
const messageId = this.callbackQuery?.message?.message_id ||
|
|
1078
|
+
(this.message && 'message_id' in this.message ? this.message.message_id : undefined) ||
|
|
1079
|
+
(this.update && 'edited_message' in this.update && this.update.edited_message
|
|
1080
|
+
? this.update.edited_message.message_id
|
|
1081
|
+
: undefined) ||
|
|
1082
|
+
this.msgId;
|
|
1083
|
+
if (!messageId) {
|
|
1084
|
+
throw new Error('[teledzik] Tidak dapat menemukan message_id untuk diedit.');
|
|
1103
1085
|
}
|
|
1104
|
-
return this.telegram.editRichMessage(
|
|
1086
|
+
return this.telegram.editRichMessage(this.chat.id, messageId, richMessage, extra);
|
|
1105
1087
|
}
|
|
1106
1088
|
/**
|
|
1107
1089
|
* Enqueue a Telegram API call for the current chat to prevent 429 Too Many Requests errors.
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Converts Rich HTML strings (containing unsupported Telegram HTML tags like
|
|
4
|
-
* <table>, <ul>, <ol>, <li>, <details>, <h1>-<h6>, <aside>, <p>, <hr>, etc.)
|
|
5
|
-
* into standard Telegram HTML compatible with parse_mode: 'HTML' (editMessageText/sendMessage).
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
3
|
exports.sanitizeRichHtml = void 0;
|
|
9
|
-
function
|
|
4
|
+
function formatTableToCard(tableHtml) {
|
|
10
5
|
const rows = [];
|
|
11
6
|
const rowMatches = tableHtml.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
|
|
12
7
|
for (const row of rowMatches) {
|
|
@@ -21,45 +16,37 @@ function convertTableToText(tableHtml) {
|
|
|
21
16
|
}
|
|
22
17
|
if (rows.length === 0)
|
|
23
18
|
return '';
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
// Header vs Row handling (Format sebagai Kartu Elegan)
|
|
20
|
+
const isKeyValue = rows.length > 1 && rows.every((r) => r.length === 2);
|
|
21
|
+
let cardText = '';
|
|
22
|
+
if (isKeyValue) {
|
|
23
|
+
const lines = rows.slice(1).map((r) => `• <b>${r[0]}:</b> ${r[1]}`);
|
|
24
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const lines = rows.map((r, idx) => {
|
|
28
|
+
if (idx === 0)
|
|
29
|
+
return `<b>${r.join(' │ ')}</b>`;
|
|
30
|
+
return `${r.join(' │ ')}`;
|
|
30
31
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const line = r
|
|
35
|
-
.map((c, colIdx) => {
|
|
36
|
-
const textLen = c.replace(/<[^>]+>/g, '').length;
|
|
37
|
-
const pad = ' '.repeat(Math.max(0, (colWidths[colIdx] || 0) - textLen));
|
|
38
|
-
return `${c}${pad}`;
|
|
39
|
-
})
|
|
40
|
-
.join(' │ ');
|
|
41
|
-
if (rowIdx === 0 && rows.length > 1) {
|
|
42
|
-
const sep = colWidths.map((w) => '─'.repeat(w)).join('─┼─');
|
|
43
|
-
return `${line}\n${sep}`;
|
|
44
|
-
}
|
|
45
|
-
return line;
|
|
46
|
-
});
|
|
47
|
-
return `<pre>\n${formattedRows.join('\n')}\n</pre>\n\n`;
|
|
32
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`;
|
|
33
|
+
}
|
|
34
|
+
return cardText;
|
|
48
35
|
}
|
|
49
36
|
function sanitizeRichHtml(html) {
|
|
50
37
|
if (!html)
|
|
51
38
|
return html;
|
|
52
39
|
let result = html;
|
|
53
|
-
// 1. Convert Tables
|
|
54
|
-
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) =>
|
|
55
|
-
// 2. Convert Details
|
|
40
|
+
// 1. Convert Tables to Clean Modern Cards
|
|
41
|
+
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) => formatTableToCard(content));
|
|
42
|
+
// 2. Convert Details to Telegram Native Expandable Blockquotes
|
|
56
43
|
result = result.replace(/<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>([\s\S]*?)<\/details>/gi, (_, title, body) => {
|
|
57
|
-
return `<b>▶ ${title.trim()}</b>\n
|
|
44
|
+
return `<blockquote expandable><b>▶ ${title.trim()}</b>\n${body.trim()}</blockquote>\n\n`;
|
|
58
45
|
});
|
|
59
|
-
// 3.
|
|
60
|
-
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `<b>${c.trim()
|
|
46
|
+
// 3. Clean Headings
|
|
47
|
+
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`);
|
|
61
48
|
result = result.replace(/<h[2-6][^>]*>([\s\S]*?)<\/h[2-6]>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`);
|
|
62
|
-
// 4.
|
|
49
|
+
// 4. Task Lists & Bullet Lists
|
|
63
50
|
result = result.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, listContent) => {
|
|
64
51
|
return (listContent
|
|
65
52
|
.replace(/<li[^>]*>\s*<input[^>]*type="checkbox"[^>]*checked[^>]*>\s*([\s\S]*?)<\/li>/gi, '☑ $1\n')
|
|
@@ -73,29 +60,24 @@ function sanitizeRichHtml(html) {
|
|
|
73
60
|
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_match, item) => `${index++}. ${item.trim()}\n`)
|
|
74
61
|
.trim() + '\n\n');
|
|
75
62
|
});
|
|
76
|
-
// 5.
|
|
63
|
+
// 5. Clean Blockquotes & Dividers
|
|
77
64
|
result = result.replace(/<aside[^>]*>([\s\S]*?)(?:<cite[^>]*>([\s\S]*?)<\/cite>)?<\/aside>/gi, (_, quote, cite) => {
|
|
78
65
|
return `<blockquote>${quote.trim()}${cite ? `\n— <i>${cite.trim()}</i>` : ''}</blockquote>\n\n`;
|
|
79
66
|
});
|
|
80
|
-
// 6. Convert Special Bot API 10.1 tags
|
|
81
67
|
result = result.replace(/<tg-thinking[^>]*>([\s\S]*?)<\/tg-thinking>/gi, (_, c) => `<blockquote>💭 <i>${c.trim()}</i></blockquote>\n\n`);
|
|
82
68
|
result = result.replace(/<tg-math-block[^>]*>([\s\S]*?)<\/tg-math-block>/gi, (_, c) => `<pre><code>${c.trim()}</code></pre>\n\n`);
|
|
83
69
|
result = result.replace(/<tg-reference[^>]*>([\s\S]*?)<\/tg-reference>/gi, (_, c) => `<i>${c.trim()}</i>`);
|
|
84
70
|
result = result.replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, (_, c) => `<b>[${c.trim()}]</b>`);
|
|
85
|
-
// 7. Convert Figures & Captions
|
|
86
71
|
result = result.replace(/<figure[^>]*>[\s\S]*?<figcaption[^>]*>([\s\S]*?)<\/figcaption><\/figure>/gi, (_, cap) => `\n<i>📷 ${cap.trim()}</i>\n`);
|
|
87
|
-
// 8. Convert Paragraphs & Linebreaks
|
|
88
72
|
result = result.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `${c.trim()}\n\n`);
|
|
89
73
|
result = result.replace(/<footer[^>]*>([\s\S]*?)<\/footer>/gi, (_, c) => `<i>${c.trim()}</i>\n`);
|
|
90
74
|
result = result.replace(/<br\s*\/?>/gi, '\n');
|
|
91
|
-
result = result.replace(/<hr\s*\/?>/gi, '
|
|
92
|
-
//
|
|
75
|
+
result = result.replace(/<hr\s*\/?>/gi, '\n');
|
|
76
|
+
// Strip unsupported tags
|
|
93
77
|
result = result.replace(/<sub[^>]*>([\s\S]*?)<\/sub>/gi, '$1');
|
|
94
78
|
result = result.replace(/<sup[^>]*>([\s\S]*?)<\/sup>/gi, '$1');
|
|
95
79
|
result = result.replace(/<tg-(map|collage|slideshow)[^>]*>([\s\S]*?)<\/tg-\1>/gi, '$2');
|
|
96
80
|
result = result.replace(/<(img|video|audio|tg-map)[^>]*\/?>/gi, '');
|
|
97
|
-
|
|
98
|
-
result = result.replace(/\n{3,}/g, '\n\n');
|
|
99
|
-
return result.trim();
|
|
81
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
100
82
|
}
|
|
101
83
|
exports.sanitizeRichHtml = sanitizeRichHtml;
|
package/lib/telegram.js
CHANGED
|
@@ -1271,26 +1271,13 @@ class Telegram extends client_1.default {
|
|
|
1271
1271
|
...extra,
|
|
1272
1272
|
});
|
|
1273
1273
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if (typeof contentOrInline === 'string' &&
|
|
1279
|
-
(typeof contentOrExtra === 'object' || typeof contentOrExtra === 'string') &&
|
|
1280
|
-
maybeExtra !== undefined) {
|
|
1281
|
-
inlineMessageId = contentOrInline;
|
|
1282
|
-
content = contentOrExtra;
|
|
1283
|
-
extra = maybeExtra || {};
|
|
1284
|
-
}
|
|
1285
|
-
else {
|
|
1286
|
-
content = contentOrInline;
|
|
1287
|
-
extra = contentOrExtra || {};
|
|
1288
|
-
inlineMessageId = extra?.inline_message_id;
|
|
1289
|
-
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Universal in-place message editor with Modern Rich UI formatting.
|
|
1276
|
+
*/
|
|
1277
|
+
async editRichMessage(chatId, messageId, content, extra) {
|
|
1290
1278
|
let htmlText = '';
|
|
1291
1279
|
let replyMarkup = extra?.reply_markup;
|
|
1292
|
-
|
|
1293
|
-
if (content && typeof content.build === 'function') {
|
|
1280
|
+
if (typeof content?.build === 'function') {
|
|
1294
1281
|
const built = content.build();
|
|
1295
1282
|
if (typeof built === 'object' && built !== null) {
|
|
1296
1283
|
htmlText = built.html || built.text || built.caption || '';
|
|
@@ -1303,7 +1290,7 @@ class Telegram extends client_1.default {
|
|
|
1303
1290
|
}
|
|
1304
1291
|
}
|
|
1305
1292
|
else if (typeof content === 'object' && content !== null) {
|
|
1306
|
-
htmlText = content.html || content.text || content.caption ||
|
|
1293
|
+
htmlText = content.html || content.text || content.caption || '';
|
|
1307
1294
|
if (content.reply_markup) {
|
|
1308
1295
|
replyMarkup = replyMarkup || content.reply_markup;
|
|
1309
1296
|
}
|
|
@@ -1311,52 +1298,40 @@ class Telegram extends client_1.default {
|
|
|
1311
1298
|
else if (typeof content === 'string') {
|
|
1312
1299
|
htmlText = content;
|
|
1313
1300
|
}
|
|
1314
|
-
//
|
|
1301
|
+
// Format ke Telegram Modern HTML
|
|
1315
1302
|
htmlText = (0, rich_sanitizer_1.sanitizeRichHtml)(htmlText);
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1303
|
+
const payload = {
|
|
1304
|
+
chat_id: chatId,
|
|
1305
|
+
message_id: Number(messageId),
|
|
1306
|
+
text: htmlText,
|
|
1319
1307
|
parse_mode: 'HTML',
|
|
1320
1308
|
...extra,
|
|
1321
1309
|
};
|
|
1322
|
-
if (
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
else {
|
|
1326
|
-
basePayload.chat_id = chatId;
|
|
1327
|
-
basePayload.message_id = messageId != null ? Number(messageId) : undefined;
|
|
1328
|
-
}
|
|
1329
|
-
if (replyMarkup) {
|
|
1330
|
-
basePayload.reply_markup = replyMarkup;
|
|
1331
|
-
}
|
|
1332
|
-
// 3. Execution with Auto-Detect (Text -> Caption Fallback) and Silent Error Handling
|
|
1310
|
+
if (replyMarkup)
|
|
1311
|
+
payload.reply_markup = replyMarkup;
|
|
1333
1312
|
try {
|
|
1334
|
-
return await this.callApi('editMessageText',
|
|
1335
|
-
...basePayload,
|
|
1336
|
-
text: htmlText,
|
|
1337
|
-
});
|
|
1313
|
+
return await this.callApi('editMessageText', payload);
|
|
1338
1314
|
}
|
|
1339
1315
|
catch (err) {
|
|
1340
1316
|
const desc = String(err?.description || err?.message || '');
|
|
1341
|
-
// Fallback
|
|
1342
|
-
if (desc.includes('no text in the message') ||
|
|
1343
|
-
desc.includes('there is no text in the message to edit') ||
|
|
1344
|
-
desc.includes('message to edit not found')) {
|
|
1317
|
+
// Fallback jika pesan berupa Media (Foto/Video)
|
|
1318
|
+
if (desc.includes('no text in the message') || desc.includes('message to edit not found')) {
|
|
1345
1319
|
try {
|
|
1346
1320
|
return await this.callApi('editMessageCaption', {
|
|
1347
|
-
|
|
1321
|
+
chat_id: chatId,
|
|
1322
|
+
message_id: Number(messageId),
|
|
1348
1323
|
caption: htmlText,
|
|
1324
|
+
parse_mode: 'HTML',
|
|
1325
|
+
reply_markup: replyMarkup,
|
|
1326
|
+
...extra,
|
|
1349
1327
|
});
|
|
1350
1328
|
}
|
|
1351
1329
|
catch (captionErr) {
|
|
1352
|
-
|
|
1353
|
-
if (capDesc.includes('message is not modified')) {
|
|
1330
|
+
if (String(captionErr?.description).includes('message is not modified'))
|
|
1354
1331
|
return false;
|
|
1355
|
-
}
|
|
1356
1332
|
throw captionErr;
|
|
1357
1333
|
}
|
|
1358
1334
|
}
|
|
1359
|
-
// Gracefully handle 'message is not modified'
|
|
1360
1335
|
if (desc.includes('message is not modified')) {
|
|
1361
1336
|
return false;
|
|
1362
1337
|
}
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -1407,61 +1407,22 @@ export class Context<U extends Deunionize<tg.Update> = tg.Update> {
|
|
|
1407
1407
|
}
|
|
1408
1408
|
|
|
1409
1409
|
/**
|
|
1410
|
-
*
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
let content: any
|
|
1425
|
-
let extra: any = {}
|
|
1426
|
-
|
|
1427
|
-
if (
|
|
1428
|
-
typeof a === 'number' ||
|
|
1429
|
-
(typeof a === 'string' && !isNaN(Number(a)) && b !== undefined)
|
|
1430
|
-
) {
|
|
1431
|
-
targetMsgId = a
|
|
1432
|
-
content = b
|
|
1433
|
-
extra = c || {}
|
|
1434
|
-
} else {
|
|
1435
|
-
targetMsgId =
|
|
1436
|
-
this.callbackQuery?.message?.message_id ??
|
|
1437
|
-
(this.message && 'message_id' in this.message ? this.message.message_id : undefined) ??
|
|
1438
|
-
(this.update && 'edited_message' in this.update && this.update.edited_message
|
|
1439
|
-
? this.update.edited_message.message_id
|
|
1440
|
-
: undefined) ??
|
|
1441
|
-
this.msgId
|
|
1442
|
-
content = a
|
|
1443
|
-
extra = b || {}
|
|
1410
|
+
* Context-aware shorthand for {@link Telegram.editRichMessage}.
|
|
1411
|
+
*/
|
|
1412
|
+
editRichMessage(richMessage: any, extra?: any): Promise<any> {
|
|
1413
|
+
this.assert(this.chat, 'editRichMessage')
|
|
1414
|
+
const messageId =
|
|
1415
|
+
this.callbackQuery?.message?.message_id ||
|
|
1416
|
+
(this.message && 'message_id' in this.message ? this.message.message_id : undefined) ||
|
|
1417
|
+
(this.update && 'edited_message' in this.update && this.update.edited_message
|
|
1418
|
+
? this.update.edited_message.message_id
|
|
1419
|
+
: undefined) ||
|
|
1420
|
+
this.msgId
|
|
1421
|
+
|
|
1422
|
+
if (!messageId) {
|
|
1423
|
+
throw new Error('[teledzik] Tidak dapat menemukan message_id untuk diedit.')
|
|
1444
1424
|
}
|
|
1445
|
-
|
|
1446
|
-
const chatId =
|
|
1447
|
-
this.chat?.id ??
|
|
1448
|
-
(this.callbackQuery?.message && 'chat' in this.callbackQuery.message
|
|
1449
|
-
? this.callbackQuery.message.chat.id
|
|
1450
|
-
: undefined)
|
|
1451
|
-
|
|
1452
|
-
const inlineMsgId = this.inlineMessageId ?? extra?.inline_message_id
|
|
1453
|
-
|
|
1454
|
-
if (!inlineMsgId && (!chatId || targetMsgId == null)) {
|
|
1455
|
-
throw new Error(
|
|
1456
|
-
'[teledzik] Tidak dapat menemukan chatId atau messageId dari konteks pesan saat ini.'
|
|
1457
|
-
)
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
if (inlineMsgId && !extra.inline_message_id) {
|
|
1461
|
-
extra.inline_message_id = inlineMsgId
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
return this.telegram.editRichMessage(chatId, targetMsgId, content, extra)
|
|
1425
|
+
return this.telegram.editRichMessage(this.chat.id, messageId, richMessage, extra)
|
|
1465
1426
|
}
|
|
1466
1427
|
|
|
1467
1428
|
/**
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
* Converts Rich HTML strings (containing unsupported Telegram HTML tags like
|
|
3
|
-
* <table>, <ul>, <ol>, <li>, <details>, <h1>-<h6>, <aside>, <p>, <hr>, etc.)
|
|
4
|
-
* into standard Telegram HTML compatible with parse_mode: 'HTML' (editMessageText/sendMessage).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
function convertTableToText(tableHtml: string): string {
|
|
1
|
+
function formatTableToCard(tableHtml: string): string {
|
|
8
2
|
const rows: string[][] = []
|
|
9
3
|
const rowMatches = tableHtml.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || []
|
|
10
|
-
|
|
11
4
|
for (const row of rowMatches) {
|
|
12
5
|
const cells: string[] = []
|
|
13
6
|
const cellMatches = row.match(/<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi) || []
|
|
@@ -17,56 +10,41 @@ function convertTableToText(tableHtml: string): string {
|
|
|
17
10
|
}
|
|
18
11
|
if (cells.length > 0) rows.push(cells)
|
|
19
12
|
}
|
|
20
|
-
|
|
21
13
|
if (rows.length === 0) return ''
|
|
22
14
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
// Header vs Row handling (Format sebagai Kartu Elegan)
|
|
16
|
+
const isKeyValue = rows.length > 1 && rows.every((r) => r.length === 2)
|
|
17
|
+
let cardText = ''
|
|
18
|
+
if (isKeyValue) {
|
|
19
|
+
const lines = rows.slice(1).map((r) => `• <b>${r[0]}:</b> ${r[1]}`)
|
|
20
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`
|
|
21
|
+
} else {
|
|
22
|
+
const lines = rows.map((r, idx) => {
|
|
23
|
+
if (idx === 0) return `<b>${r.join(' │ ')}</b>`
|
|
24
|
+
return `${r.join(' │ ')}`
|
|
29
25
|
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const formattedRows = rows.map((r, rowIdx) => {
|
|
34
|
-
const line = r
|
|
35
|
-
.map((c, colIdx) => {
|
|
36
|
-
const textLen = c.replace(/<[^>]+>/g, '').length
|
|
37
|
-
const pad = ' '.repeat(Math.max(0, (colWidths[colIdx] || 0) - textLen))
|
|
38
|
-
return `${c}${pad}`
|
|
39
|
-
})
|
|
40
|
-
.join(' │ ')
|
|
41
|
-
|
|
42
|
-
if (rowIdx === 0 && rows.length > 1) {
|
|
43
|
-
const sep = colWidths.map((w) => '─'.repeat(w)).join('─┼─')
|
|
44
|
-
return `${line}\n${sep}`
|
|
45
|
-
}
|
|
46
|
-
return line
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
return `<pre>\n${formattedRows.join('\n')}\n</pre>\n\n`
|
|
26
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`
|
|
27
|
+
}
|
|
28
|
+
return cardText
|
|
50
29
|
}
|
|
51
30
|
|
|
52
31
|
export function sanitizeRichHtml(html: string): string {
|
|
53
32
|
if (!html) return html
|
|
54
|
-
|
|
55
33
|
let result = html
|
|
56
34
|
|
|
57
|
-
// 1. Convert Tables
|
|
58
|
-
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) =>
|
|
35
|
+
// 1. Convert Tables to Clean Modern Cards
|
|
36
|
+
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) => formatTableToCard(content))
|
|
59
37
|
|
|
60
|
-
// 2. Convert Details
|
|
38
|
+
// 2. Convert Details to Telegram Native Expandable Blockquotes
|
|
61
39
|
result = result.replace(/<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>([\s\S]*?)<\/details>/gi, (_, title, body) => {
|
|
62
|
-
return `<b>▶ ${title.trim()}</b>\n
|
|
40
|
+
return `<blockquote expandable><b>▶ ${title.trim()}</b>\n${body.trim()}</blockquote>\n\n`
|
|
63
41
|
})
|
|
64
42
|
|
|
65
|
-
// 3.
|
|
66
|
-
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `<b>${c.trim()
|
|
43
|
+
// 3. Clean Headings
|
|
44
|
+
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`)
|
|
67
45
|
result = result.replace(/<h[2-6][^>]*>([\s\S]*?)<\/h[2-6]>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`)
|
|
68
46
|
|
|
69
|
-
// 4.
|
|
47
|
+
// 4. Task Lists & Bullet Lists
|
|
70
48
|
result = result.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, listContent) => {
|
|
71
49
|
return (
|
|
72
50
|
listContent
|
|
@@ -86,34 +64,28 @@ export function sanitizeRichHtml(html: string): string {
|
|
|
86
64
|
)
|
|
87
65
|
})
|
|
88
66
|
|
|
89
|
-
// 5.
|
|
67
|
+
// 5. Clean Blockquotes & Dividers
|
|
90
68
|
result = result.replace(/<aside[^>]*>([\s\S]*?)(?:<cite[^>]*>([\s\S]*?)<\/cite>)?<\/aside>/gi, (_, quote, cite) => {
|
|
91
69
|
return `<blockquote>${quote.trim()}${cite ? `\n— <i>${cite.trim()}</i>` : ''}</blockquote>\n\n`
|
|
92
70
|
})
|
|
93
71
|
|
|
94
|
-
// 6. Convert Special Bot API 10.1 tags
|
|
95
72
|
result = result.replace(/<tg-thinking[^>]*>([\s\S]*?)<\/tg-thinking>/gi, (_, c) => `<blockquote>💭 <i>${c.trim()}</i></blockquote>\n\n`)
|
|
96
73
|
result = result.replace(/<tg-math-block[^>]*>([\s\S]*?)<\/tg-math-block>/gi, (_, c) => `<pre><code>${c.trim()}</code></pre>\n\n`)
|
|
97
74
|
result = result.replace(/<tg-reference[^>]*>([\s\S]*?)<\/tg-reference>/gi, (_, c) => `<i>${c.trim()}</i>`)
|
|
98
75
|
result = result.replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, (_, c) => `<b>[${c.trim()}]</b>`)
|
|
99
76
|
|
|
100
|
-
// 7. Convert Figures & Captions
|
|
101
77
|
result = result.replace(/<figure[^>]*>[\s\S]*?<figcaption[^>]*>([\s\S]*?)<\/figcaption><\/figure>/gi, (_, cap) => `\n<i>📷 ${cap.trim()}</i>\n`)
|
|
102
78
|
|
|
103
|
-
// 8. Convert Paragraphs & Linebreaks
|
|
104
79
|
result = result.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `${c.trim()}\n\n`)
|
|
105
80
|
result = result.replace(/<footer[^>]*>([\s\S]*?)<\/footer>/gi, (_, c) => `<i>${c.trim()}</i>\n`)
|
|
106
81
|
result = result.replace(/<br\s*\/?>/gi, '\n')
|
|
107
|
-
result = result.replace(/<hr\s*\/?>/gi, '
|
|
82
|
+
result = result.replace(/<hr\s*\/?>/gi, '\n')
|
|
108
83
|
|
|
109
|
-
//
|
|
84
|
+
// Strip unsupported tags
|
|
110
85
|
result = result.replace(/<sub[^>]*>([\s\S]*?)<\/sub>/gi, '$1')
|
|
111
86
|
result = result.replace(/<sup[^>]*>([\s\S]*?)<\/sup>/gi, '$1')
|
|
112
87
|
result = result.replace(/<tg-(map|collage|slideshow)[^>]*>([\s\S]*?)<\/tg-\1>/gi, '$2')
|
|
113
88
|
result = result.replace(/<(img|video|audio|tg-map)[^>]*\/?>/gi, '')
|
|
114
89
|
|
|
115
|
-
|
|
116
|
-
result = result.replace(/\n{3,}/g, '\n\n')
|
|
117
|
-
|
|
118
|
-
return result.trim()
|
|
90
|
+
return result.replace(/\n{3,}/g, '\n\n').trim()
|
|
119
91
|
}
|
package/src/telegram.ts
CHANGED
|
@@ -1680,52 +1680,18 @@ export class Telegram extends ApiClient {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
|
|
1682
1682
|
/**
|
|
1683
|
-
* Universal
|
|
1684
|
-
* Auto-detects Text vs Media Caption and gracefully handles 'message is not modified'.
|
|
1683
|
+
* Universal in-place message editor with Modern Rich UI formatting.
|
|
1685
1684
|
*/
|
|
1686
1685
|
async editRichMessage(
|
|
1687
|
-
chatId: number | string
|
|
1688
|
-
messageId: number | string
|
|
1689
|
-
content: any,
|
|
1690
|
-
extra?: any
|
|
1691
|
-
): Promise<any>
|
|
1692
|
-
async editRichMessage(
|
|
1693
|
-
chatId: number | string | undefined,
|
|
1694
|
-
messageId: number | string | undefined,
|
|
1695
|
-
inlineMessageId: string | undefined,
|
|
1686
|
+
chatId: number | string,
|
|
1687
|
+
messageId: number | string,
|
|
1696
1688
|
content: any,
|
|
1697
1689
|
extra?: any
|
|
1698
|
-
): Promise<any>
|
|
1699
|
-
async editRichMessage(
|
|
1700
|
-
chatId: number | string | undefined,
|
|
1701
|
-
messageId: number | string | undefined,
|
|
1702
|
-
contentOrInline: any,
|
|
1703
|
-
contentOrExtra?: any,
|
|
1704
|
-
maybeExtra?: any
|
|
1705
1690
|
): Promise<any> {
|
|
1706
|
-
let inlineMessageId: string | undefined
|
|
1707
|
-
let content: any
|
|
1708
|
-
let extra: any = {}
|
|
1709
|
-
|
|
1710
|
-
if (
|
|
1711
|
-
typeof contentOrInline === 'string' &&
|
|
1712
|
-
(typeof contentOrExtra === 'object' || typeof contentOrExtra === 'string') &&
|
|
1713
|
-
maybeExtra !== undefined
|
|
1714
|
-
) {
|
|
1715
|
-
inlineMessageId = contentOrInline
|
|
1716
|
-
content = contentOrExtra
|
|
1717
|
-
extra = maybeExtra || {}
|
|
1718
|
-
} else {
|
|
1719
|
-
content = contentOrInline
|
|
1720
|
-
extra = contentOrExtra || {}
|
|
1721
|
-
inlineMessageId = extra?.inline_message_id
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
1691
|
let htmlText = ''
|
|
1725
1692
|
let replyMarkup = extra?.reply_markup
|
|
1726
1693
|
|
|
1727
|
-
|
|
1728
|
-
if (content && typeof content.build === 'function') {
|
|
1694
|
+
if (typeof content?.build === 'function') {
|
|
1729
1695
|
const built = content.build()
|
|
1730
1696
|
if (typeof built === 'object' && built !== null) {
|
|
1731
1697
|
htmlText = built.html || built.text || built.caption || ''
|
|
@@ -1736,7 +1702,7 @@ export class Telegram extends ApiClient {
|
|
|
1736
1702
|
htmlText = String(built || '')
|
|
1737
1703
|
}
|
|
1738
1704
|
} else if (typeof content === 'object' && content !== null) {
|
|
1739
|
-
htmlText = content.html || content.text || content.caption ||
|
|
1705
|
+
htmlText = content.html || content.text || content.caption || ''
|
|
1740
1706
|
if (content.reply_markup) {
|
|
1741
1707
|
replyMarkup = replyMarkup || content.reply_markup
|
|
1742
1708
|
}
|
|
@@ -1744,61 +1710,42 @@ export class Telegram extends ApiClient {
|
|
|
1744
1710
|
htmlText = content
|
|
1745
1711
|
}
|
|
1746
1712
|
|
|
1747
|
-
//
|
|
1713
|
+
// Format ke Telegram Modern HTML
|
|
1748
1714
|
htmlText = sanitizeRichHtml(htmlText)
|
|
1749
1715
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1716
|
+
const payload: Record<string, any> = {
|
|
1717
|
+
chat_id: chatId,
|
|
1718
|
+
message_id: Number(messageId),
|
|
1719
|
+
text: htmlText,
|
|
1753
1720
|
parse_mode: 'HTML',
|
|
1754
1721
|
...extra,
|
|
1755
1722
|
}
|
|
1756
1723
|
|
|
1757
|
-
if (
|
|
1758
|
-
basePayload.inline_message_id = inlineMessageId || extra.inline_message_id
|
|
1759
|
-
} else {
|
|
1760
|
-
basePayload.chat_id = chatId
|
|
1761
|
-
basePayload.message_id = messageId != null ? Number(messageId) : undefined
|
|
1762
|
-
}
|
|
1724
|
+
if (replyMarkup) payload.reply_markup = replyMarkup
|
|
1763
1725
|
|
|
1764
|
-
if (replyMarkup) {
|
|
1765
|
-
basePayload.reply_markup = replyMarkup
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
// 3. Execution with Auto-Detect (Text -> Caption Fallback) and Silent Error Handling
|
|
1769
1726
|
try {
|
|
1770
|
-
return await this.callApi('editMessageText' as never,
|
|
1771
|
-
...basePayload,
|
|
1772
|
-
text: htmlText,
|
|
1773
|
-
} as never)
|
|
1727
|
+
return await this.callApi('editMessageText' as never, payload as never)
|
|
1774
1728
|
} catch (err: any) {
|
|
1775
1729
|
const desc = String(err?.description || err?.message || '')
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
if (
|
|
1779
|
-
desc.includes('no text in the message') ||
|
|
1780
|
-
desc.includes('there is no text in the message to edit') ||
|
|
1781
|
-
desc.includes('message to edit not found')
|
|
1782
|
-
) {
|
|
1730
|
+
// Fallback jika pesan berupa Media (Foto/Video)
|
|
1731
|
+
if (desc.includes('no text in the message') || desc.includes('message to edit not found')) {
|
|
1783
1732
|
try {
|
|
1784
1733
|
return await this.callApi('editMessageCaption' as never, {
|
|
1785
|
-
|
|
1734
|
+
chat_id: chatId,
|
|
1735
|
+
message_id: Number(messageId),
|
|
1786
1736
|
caption: htmlText,
|
|
1737
|
+
parse_mode: 'HTML',
|
|
1738
|
+
reply_markup: replyMarkup,
|
|
1739
|
+
...extra,
|
|
1787
1740
|
} as never)
|
|
1788
1741
|
} catch (captionErr: any) {
|
|
1789
|
-
|
|
1790
|
-
if (capDesc.includes('message is not modified')) {
|
|
1791
|
-
return false
|
|
1792
|
-
}
|
|
1742
|
+
if (String(captionErr?.description).includes('message is not modified')) return false
|
|
1793
1743
|
throw captionErr
|
|
1794
1744
|
}
|
|
1795
1745
|
}
|
|
1796
|
-
|
|
1797
|
-
// Gracefully handle 'message is not modified'
|
|
1798
1746
|
if (desc.includes('message is not modified')) {
|
|
1799
1747
|
return false
|
|
1800
1748
|
}
|
|
1801
|
-
|
|
1802
1749
|
throw err
|
|
1803
1750
|
}
|
|
1804
1751
|
}
|
package/typings/context.d.ts
CHANGED
|
@@ -591,11 +591,9 @@ export declare class Context<U extends Deunionize<tg.Update> = tg.Update> {
|
|
|
591
591
|
*/
|
|
592
592
|
sendRichMessageDraft(draftId: number, richMessage: tg.InputRichMessage, extra?: tt.ExtraSendRichMessageDraft): Promise<never>;
|
|
593
593
|
/**
|
|
594
|
-
*
|
|
595
|
-
* auto-detection of Text vs Media Caption, and silent 'not modified' error handling.
|
|
594
|
+
* Context-aware shorthand for {@link Telegram.editRichMessage}.
|
|
596
595
|
*/
|
|
597
|
-
editRichMessage(
|
|
598
|
-
editRichMessage(messageId: number | string, content: tg.RichContentInput, extra?: tg.EditRichOptions): Promise<any>;
|
|
596
|
+
editRichMessage(richMessage: any, extra?: any): Promise<any>;
|
|
599
597
|
/**
|
|
600
598
|
* Enqueue a Telegram API call for the current chat to prevent 429 Too Many Requests errors.
|
|
601
599
|
*/
|
|
@@ -1,6 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Converts Rich HTML strings (containing unsupported Telegram HTML tags like
|
|
3
|
-
* <table>, <ul>, <ol>, <li>, <details>, <h1>-<h6>, <aside>, <p>, <hr>, etc.)
|
|
4
|
-
* into standard Telegram HTML compatible with parse_mode: 'HTML' (editMessageText/sendMessage).
|
|
5
|
-
*/
|
|
6
1
|
export declare function sanitizeRichHtml(html: string): string;
|
package/typings/telegram.d.ts
CHANGED
|
@@ -690,10 +690,8 @@ export declare class Telegram extends ApiClient {
|
|
|
690
690
|
*/
|
|
691
691
|
sendRichMessageDraft(chatId: number, draftId: number, richMessage: tg.InputRichMessage, extra?: tt.ExtraSendRichMessageDraft): Promise<never>;
|
|
692
692
|
/**
|
|
693
|
-
* Universal
|
|
694
|
-
* Auto-detects Text vs Media Caption and gracefully handles 'message is not modified'.
|
|
693
|
+
* Universal in-place message editor with Modern Rich UI formatting.
|
|
695
694
|
*/
|
|
696
|
-
editRichMessage(chatId: number | string
|
|
697
|
-
editRichMessage(chatId: number | string | undefined, messageId: number | string | undefined, inlineMessageId: string | undefined, content: any, extra?: any): Promise<any>;
|
|
695
|
+
editRichMessage(chatId: number | string, messageId: number | string, content: any, extra?: any): Promise<any>;
|
|
698
696
|
}
|
|
699
697
|
export default Telegram;
|