teledzik 1.0.6 ā 1.0.8
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 +33 -18
- package/lib/core/helpers/rich-sanitizer.js +31 -45
- package/lib/telegram.js +65 -7
- package/package.json +1 -1
- package/src/core/helpers/rich-sanitizer.ts +28 -53
- package/src/telegram.ts +62 -6
- package/typings/core/helpers/rich-sanitizer.d.ts +0 -5
- package/typings/telegram.d.ts +2 -3
package/README.md
CHANGED
|
@@ -8,21 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
<div align="center">
|
|
10
10
|
<img src="docs/assets/logo.svg" alt="logo" height="90" align="center">
|
|
11
|
-
<h1 align="center">
|
|
11
|
+
<h1 align="center">teledzik</h1>
|
|
12
12
|
|
|
13
|
-
<p>Modern Telegram Bot API framework for Node.js</p>
|
|
13
|
+
<p>Modern Telegram Bot API framework for Node.js (with Native Rich Messages & Full In-Place Edit)</p>
|
|
14
14
|
|
|
15
|
-
<a href="https://
|
|
16
|
-
<img src="https://img.shields.io/
|
|
15
|
+
<a href="https://www.npmjs.com/package/teledzik">
|
|
16
|
+
<img src="https://img.shields.io/npm/v/teledzik.svg?style=flat-square" alt="npm version" />
|
|
17
17
|
</a>
|
|
18
|
-
<a href="https://
|
|
19
|
-
<img src="https://
|
|
18
|
+
<a href="https://core.telegram.org/bots/api">
|
|
19
|
+
<img src="https://img.shields.io/badge/Bot%20API-v10.1+-f36caf.svg?style=flat-square" alt="Bot API Version" />
|
|
20
20
|
</a>
|
|
21
|
-
<a href="https://
|
|
22
|
-
<img src="https://
|
|
21
|
+
<a href="https://packagephobia.com/result?p=teledzik">
|
|
22
|
+
<img src="https://flat.badgen.net/packagephobia/install/teledzik" alt="install size" />
|
|
23
23
|
</a>
|
|
24
|
-
<a href="https://
|
|
25
|
-
<img src="https://img.shields.io/badge/
|
|
24
|
+
<a href="https://t.me/lorddzik">
|
|
25
|
+
<img src="https://img.shields.io/badge/Telegram-@lorddzik-blue?style=flat-square&logo=telegram" alt="Maintainer Telegram" />
|
|
26
26
|
</a>
|
|
27
27
|
</div>
|
|
28
28
|
|
|
@@ -99,14 +99,16 @@ process.once('SIGTERM', () => bot.stop('SIGTERM'))
|
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
|
-
### Sending Methods
|
|
102
|
+
### Sending & Editing Methods
|
|
103
103
|
|
|
104
104
|
| Method | Description |
|
|
105
105
|
|---|---|
|
|
106
106
|
| `ctx.sendRichMessage(msg, extra?)` | Send a rich message to the current chat |
|
|
107
107
|
| `ctx.replyWithRichMessageContent(msg, extra?)` | Send a rich message quoting the current message |
|
|
108
|
+
| `ctx.editRichMessage(msg, extra?)` | In-place edit of message with native Rich UI preservation (v1.0.8+) |
|
|
108
109
|
| `ctx.sendRichMessageDraft(draftId, msg, extra?)` | Stream a partial draft (private chats only) |
|
|
109
|
-
| `ctx.telegram.sendRichMessage(chatId, msg, extra?)` | Explicit
|
|
110
|
+
| `ctx.telegram.sendRichMessage(chatId, msg, extra?)` | Explicit send with chat ID |
|
|
111
|
+
| `ctx.telegram.editRichMessage(chatId, messageId, msg, extra?)` | Explicit edit with chat ID and message ID |
|
|
110
112
|
| `ctx.telegram.sendRichMessageDraft(chatId, draftId, msg, extra?)` | Explicit streaming with chat ID |
|
|
111
113
|
|
|
112
114
|
**`extra` options for `sendRichMessage`:**
|
|
@@ -645,17 +647,30 @@ bot.on('inline_query', async (ctx) => {
|
|
|
645
647
|
|
|
646
648
|
## Exclusive Teledzik Enhancements
|
|
647
649
|
|
|
648
|
-
###
|
|
650
|
+
### Native `editRichMessage` & Auto-Sanitizer (v1.0.8+)
|
|
649
651
|
|
|
650
|
-
Standard
|
|
652
|
+
Standard `editMessageText` (`parse_mode: 'HTML'`) fails or degrades layout when editing complex structured blocks. In `teledzik` v1.0.8+, `ctx.editRichMessage` passes the payload natively via Telegram's `rich_message` parameter:
|
|
651
653
|
|
|
652
|
-
|
|
654
|
+
- **Preserves Full Rich UI**: In-place edits keep native `<table>`, `<h1>`-`<h6>`, collapsible cards (`<details>` / `<blockquote expandable>`), and styled elements without flattening them into plain text.
|
|
655
|
+
- **Smart Key-Value Table Handling**: 2-column tables correctly retain all rows and headers without accidental truncation.
|
|
656
|
+
- **Resilient Media & No-Op Handling**: Automatically falls back to `editMessageCaption` if editing a photo or video message, and silently resolves `message is not modified` instead of throwing unhandled exceptions.
|
|
653
657
|
|
|
654
658
|
```ts
|
|
655
|
-
//
|
|
656
|
-
|
|
659
|
+
// 1. Edit with RichHTMLBuilder
|
|
660
|
+
const rich = new RichHTMLBuilder()
|
|
661
|
+
.heading(2, 'Order Confirmed')
|
|
662
|
+
.table([
|
|
663
|
+
['Product', 'Status'],
|
|
664
|
+
['VPS-01', 'ACTIVE š¢']
|
|
665
|
+
])
|
|
666
|
+
|
|
667
|
+
await ctx.editRichMessage(rich, {
|
|
668
|
+
reply_markup: Markup.inlineKeyboard([
|
|
669
|
+
[Markup.button.callback('Back to Menu', 'menu')]
|
|
670
|
+
]).reply_markup
|
|
671
|
+
})
|
|
657
672
|
|
|
658
|
-
//
|
|
673
|
+
// 2. Edit with raw HTML / Rich Object
|
|
659
674
|
await ctx.editRichMessage({ html: '<h1>Title</h1><p>Body</p>' })
|
|
660
675
|
```
|
|
661
676
|
|
|
@@ -1,15 +1,13 @@
|
|
|
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 = [];
|
|
6
|
+
let hasHeader = false;
|
|
11
7
|
const rowMatches = tableHtml.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
|
|
12
8
|
for (const row of rowMatches) {
|
|
9
|
+
if (/<th[^>]*>/i.test(row))
|
|
10
|
+
hasHeader = true;
|
|
13
11
|
const cells = [];
|
|
14
12
|
const cellMatches = row.match(/<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi) || [];
|
|
15
13
|
for (const cell of cellMatches) {
|
|
@@ -21,45 +19,38 @@ function convertTableToText(tableHtml) {
|
|
|
21
19
|
}
|
|
22
20
|
if (rows.length === 0)
|
|
23
21
|
return '';
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
// Header vs Row handling (Format sebagai Kartu Elegan)
|
|
23
|
+
const isKeyValue = rows.length > 0 && rows.every((r) => r.length === 2);
|
|
24
|
+
let cardText = '';
|
|
25
|
+
if (isKeyValue) {
|
|
26
|
+
const dataRows = hasHeader ? rows.slice(1) : rows;
|
|
27
|
+
const lines = dataRows.map((r) => `⢠<b>${r[0]}:</b> ${r[1]}`);
|
|
28
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const lines = rows.map((r, idx) => {
|
|
32
|
+
if (idx === 0 && hasHeader)
|
|
33
|
+
return `<b>${r.join(' ā ')}</b>`;
|
|
34
|
+
return `${r.join(' ā ')}`;
|
|
30
35
|
});
|
|
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`;
|
|
36
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`;
|
|
37
|
+
}
|
|
38
|
+
return cardText;
|
|
48
39
|
}
|
|
49
40
|
function sanitizeRichHtml(html) {
|
|
50
41
|
if (!html)
|
|
51
42
|
return html;
|
|
52
43
|
let result = html;
|
|
53
|
-
// 1. Convert Tables
|
|
54
|
-
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) =>
|
|
55
|
-
// 2. Convert Details
|
|
44
|
+
// 1. Convert Tables to Clean Modern Cards
|
|
45
|
+
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) => formatTableToCard(content));
|
|
46
|
+
// 2. Convert Details to Telegram Native Expandable Blockquotes
|
|
56
47
|
result = result.replace(/<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>([\s\S]*?)<\/details>/gi, (_, title, body) => {
|
|
57
|
-
return `<b>ā¶ ${title.trim()}</b>\n
|
|
48
|
+
return `<blockquote expandable><b>ā¶ ${title.trim()}</b>\n${body.trim()}</blockquote>\n\n`;
|
|
58
49
|
});
|
|
59
|
-
// 3.
|
|
60
|
-
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `<b>${c.trim()
|
|
50
|
+
// 3. Clean Headings
|
|
51
|
+
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`);
|
|
61
52
|
result = result.replace(/<h[2-6][^>]*>([\s\S]*?)<\/h[2-6]>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`);
|
|
62
|
-
// 4.
|
|
53
|
+
// 4. Task Lists & Bullet Lists
|
|
63
54
|
result = result.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, listContent) => {
|
|
64
55
|
return (listContent
|
|
65
56
|
.replace(/<li[^>]*>\s*<input[^>]*type="checkbox"[^>]*checked[^>]*>\s*([\s\S]*?)<\/li>/gi, 'ā $1\n')
|
|
@@ -73,29 +64,24 @@ function sanitizeRichHtml(html) {
|
|
|
73
64
|
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_match, item) => `${index++}. ${item.trim()}\n`)
|
|
74
65
|
.trim() + '\n\n');
|
|
75
66
|
});
|
|
76
|
-
// 5.
|
|
67
|
+
// 5. Clean Blockquotes & Dividers
|
|
77
68
|
result = result.replace(/<aside[^>]*>([\s\S]*?)(?:<cite[^>]*>([\s\S]*?)<\/cite>)?<\/aside>/gi, (_, quote, cite) => {
|
|
78
69
|
return `<blockquote>${quote.trim()}${cite ? `\nā <i>${cite.trim()}</i>` : ''}</blockquote>\n\n`;
|
|
79
70
|
});
|
|
80
|
-
// 6. Convert Special Bot API 10.1 tags
|
|
81
71
|
result = result.replace(/<tg-thinking[^>]*>([\s\S]*?)<\/tg-thinking>/gi, (_, c) => `<blockquote>š <i>${c.trim()}</i></blockquote>\n\n`);
|
|
82
72
|
result = result.replace(/<tg-math-block[^>]*>([\s\S]*?)<\/tg-math-block>/gi, (_, c) => `<pre><code>${c.trim()}</code></pre>\n\n`);
|
|
83
73
|
result = result.replace(/<tg-reference[^>]*>([\s\S]*?)<\/tg-reference>/gi, (_, c) => `<i>${c.trim()}</i>`);
|
|
84
74
|
result = result.replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, (_, c) => `<b>[${c.trim()}]</b>`);
|
|
85
|
-
// 7. Convert Figures & Captions
|
|
86
75
|
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
76
|
result = result.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `${c.trim()}\n\n`);
|
|
89
77
|
result = result.replace(/<footer[^>]*>([\s\S]*?)<\/footer>/gi, (_, c) => `<i>${c.trim()}</i>\n`);
|
|
90
78
|
result = result.replace(/<br\s*\/?>/gi, '\n');
|
|
91
|
-
result = result.replace(/<hr\s*\/?>/gi, '
|
|
92
|
-
//
|
|
79
|
+
result = result.replace(/<hr\s*\/?>/gi, '\n');
|
|
80
|
+
// Strip unsupported tags
|
|
93
81
|
result = result.replace(/<sub[^>]*>([\s\S]*?)<\/sub>/gi, '$1');
|
|
94
82
|
result = result.replace(/<sup[^>]*>([\s\S]*?)<\/sup>/gi, '$1');
|
|
95
83
|
result = result.replace(/<tg-(map|collage|slideshow)[^>]*>([\s\S]*?)<\/tg-\1>/gi, '$2');
|
|
96
84
|
result = result.replace(/<(img|video|audio|tg-map)[^>]*\/?>/gi, '');
|
|
97
|
-
|
|
98
|
-
result = result.replace(/\n{3,}/g, '\n\n');
|
|
99
|
-
return result.trim();
|
|
85
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
100
86
|
}
|
|
101
87
|
exports.sanitizeRichHtml = sanitizeRichHtml;
|
package/lib/telegram.js
CHANGED
|
@@ -1272,25 +1272,83 @@ class Telegram extends client_1.default {
|
|
|
1272
1272
|
});
|
|
1273
1273
|
}
|
|
1274
1274
|
/**
|
|
1275
|
-
*
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1275
|
+
* Universal in-place message editor with Modern Rich UI formatting.
|
|
1276
|
+
*/
|
|
1277
|
+
async editRichMessage(chatId, messageId, content, extra) {
|
|
1278
|
+
let richPayload = content;
|
|
1279
|
+
let htmlText = '';
|
|
1280
|
+
let replyMarkup = extra?.reply_markup;
|
|
1281
|
+
if (typeof content?.build === 'function') {
|
|
1282
|
+
const built = content.build();
|
|
1283
|
+
richPayload = built;
|
|
1284
|
+
if (typeof built === 'object' && built !== null) {
|
|
1285
|
+
htmlText = built.html || built.text || built.caption || '';
|
|
1286
|
+
if (built.reply_markup) {
|
|
1287
|
+
replyMarkup = replyMarkup || built.reply_markup;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
else {
|
|
1291
|
+
htmlText = String(built || '');
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
else if (typeof content === 'object' && content !== null) {
|
|
1295
|
+
richPayload = content;
|
|
1296
|
+
htmlText = content.html || content.text || content.caption || '';
|
|
1297
|
+
if (content.reply_markup) {
|
|
1298
|
+
replyMarkup = replyMarkup || content.reply_markup;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
else if (typeof content === 'string') {
|
|
1302
|
+
htmlText = content;
|
|
1303
|
+
richPayload = content.startsWith('{') || content.includes('<') ? content : { text: content };
|
|
1304
|
+
}
|
|
1279
1305
|
const payload = {
|
|
1280
1306
|
chat_id: chatId,
|
|
1281
1307
|
message_id: Number(messageId),
|
|
1282
|
-
rich_message:
|
|
1308
|
+
rich_message: richPayload,
|
|
1283
1309
|
...extra,
|
|
1284
1310
|
};
|
|
1311
|
+
if (replyMarkup)
|
|
1312
|
+
payload.reply_markup = replyMarkup;
|
|
1285
1313
|
try {
|
|
1286
|
-
return await this.callApi('
|
|
1314
|
+
return await this.callApi('editMessageText', payload);
|
|
1287
1315
|
}
|
|
1288
1316
|
catch (err) {
|
|
1289
1317
|
const desc = String(err?.description || err?.message || '');
|
|
1290
|
-
//
|
|
1318
|
+
// Ignore if message is unchanged
|
|
1291
1319
|
if (desc.includes('message is not modified')) {
|
|
1292
1320
|
return false;
|
|
1293
1321
|
}
|
|
1322
|
+
const fallbackHtml = (0, rich_sanitizer_1.sanitizeRichHtml)(htmlText);
|
|
1323
|
+
// Fallback jika pesan berupa Media (Foto/Video)
|
|
1324
|
+
if (desc.includes('no text in the message') || desc.includes('message to edit not found')) {
|
|
1325
|
+
try {
|
|
1326
|
+
return await this.callApi('editMessageCaption', {
|
|
1327
|
+
chat_id: chatId,
|
|
1328
|
+
message_id: Number(messageId),
|
|
1329
|
+
caption: fallbackHtml,
|
|
1330
|
+
parse_mode: 'HTML',
|
|
1331
|
+
reply_markup: replyMarkup,
|
|
1332
|
+
...extra,
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
catch (captionErr) {
|
|
1336
|
+
if (String(captionErr?.description).includes('message is not modified'))
|
|
1337
|
+
return false;
|
|
1338
|
+
throw captionErr;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// Fallback untuk Bot API versi lama tanpa parameter rich_message pada editMessageText
|
|
1342
|
+
if (desc.includes('rich_message') || desc.includes('not supported') || desc.includes('wrong type')) {
|
|
1343
|
+
return await this.callApi('editMessageText', {
|
|
1344
|
+
chat_id: chatId,
|
|
1345
|
+
message_id: Number(messageId),
|
|
1346
|
+
text: fallbackHtml,
|
|
1347
|
+
parse_mode: 'HTML',
|
|
1348
|
+
reply_markup: replyMarkup,
|
|
1349
|
+
...extra,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1294
1352
|
throw err;
|
|
1295
1353
|
}
|
|
1296
1354
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
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[][] = []
|
|
3
|
+
let hasHeader = false
|
|
9
4
|
const rowMatches = tableHtml.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || []
|
|
10
|
-
|
|
11
5
|
for (const row of rowMatches) {
|
|
6
|
+
if (/<th[^>]*>/i.test(row)) hasHeader = true
|
|
12
7
|
const cells: string[] = []
|
|
13
8
|
const cellMatches = row.match(/<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi) || []
|
|
14
9
|
for (const cell of cellMatches) {
|
|
@@ -17,56 +12,42 @@ function convertTableToText(tableHtml: string): string {
|
|
|
17
12
|
}
|
|
18
13
|
if (cells.length > 0) rows.push(cells)
|
|
19
14
|
}
|
|
20
|
-
|
|
21
15
|
if (rows.length === 0) return ''
|
|
22
16
|
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
// Header vs Row handling (Format sebagai Kartu Elegan)
|
|
18
|
+
const isKeyValue = rows.length > 0 && rows.every((r) => r.length === 2)
|
|
19
|
+
let cardText = ''
|
|
20
|
+
if (isKeyValue) {
|
|
21
|
+
const dataRows = hasHeader ? rows.slice(1) : rows
|
|
22
|
+
const lines = dataRows.map((r) => `⢠<b>${r[0]}:</b> ${r[1]}`)
|
|
23
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`
|
|
24
|
+
} else {
|
|
25
|
+
const lines = rows.map((r, idx) => {
|
|
26
|
+
if (idx === 0 && hasHeader) return `<b>${r.join(' ā ')}</b>`
|
|
27
|
+
return `${r.join(' ā ')}`
|
|
29
28
|
})
|
|
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`
|
|
29
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`
|
|
30
|
+
}
|
|
31
|
+
return cardText
|
|
50
32
|
}
|
|
51
33
|
|
|
52
34
|
export function sanitizeRichHtml(html: string): string {
|
|
53
35
|
if (!html) return html
|
|
54
|
-
|
|
55
36
|
let result = html
|
|
56
37
|
|
|
57
|
-
// 1. Convert Tables
|
|
58
|
-
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) =>
|
|
38
|
+
// 1. Convert Tables to Clean Modern Cards
|
|
39
|
+
result = result.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, content) => formatTableToCard(content))
|
|
59
40
|
|
|
60
|
-
// 2. Convert Details
|
|
41
|
+
// 2. Convert Details to Telegram Native Expandable Blockquotes
|
|
61
42
|
result = result.replace(/<details[^>]*>\s*<summary[^>]*>(.*?)<\/summary>([\s\S]*?)<\/details>/gi, (_, title, body) => {
|
|
62
|
-
return `<b>ā¶ ${title.trim()}</b>\n
|
|
43
|
+
return `<blockquote expandable><b>ā¶ ${title.trim()}</b>\n${body.trim()}</blockquote>\n\n`
|
|
63
44
|
})
|
|
64
45
|
|
|
65
|
-
// 3.
|
|
66
|
-
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`)
|
|
67
48
|
result = result.replace(/<h[2-6][^>]*>([\s\S]*?)<\/h[2-6]>/gi, (_, c) => `<b>${c.trim()}</b>\n\n`)
|
|
68
49
|
|
|
69
|
-
// 4.
|
|
50
|
+
// 4. Task Lists & Bullet Lists
|
|
70
51
|
result = result.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, listContent) => {
|
|
71
52
|
return (
|
|
72
53
|
listContent
|
|
@@ -86,34 +67,28 @@ export function sanitizeRichHtml(html: string): string {
|
|
|
86
67
|
)
|
|
87
68
|
})
|
|
88
69
|
|
|
89
|
-
// 5.
|
|
70
|
+
// 5. Clean Blockquotes & Dividers
|
|
90
71
|
result = result.replace(/<aside[^>]*>([\s\S]*?)(?:<cite[^>]*>([\s\S]*?)<\/cite>)?<\/aside>/gi, (_, quote, cite) => {
|
|
91
72
|
return `<blockquote>${quote.trim()}${cite ? `\nā <i>${cite.trim()}</i>` : ''}</blockquote>\n\n`
|
|
92
73
|
})
|
|
93
74
|
|
|
94
|
-
// 6. Convert Special Bot API 10.1 tags
|
|
95
75
|
result = result.replace(/<tg-thinking[^>]*>([\s\S]*?)<\/tg-thinking>/gi, (_, c) => `<blockquote>š <i>${c.trim()}</i></blockquote>\n\n`)
|
|
96
76
|
result = result.replace(/<tg-math-block[^>]*>([\s\S]*?)<\/tg-math-block>/gi, (_, c) => `<pre><code>${c.trim()}</code></pre>\n\n`)
|
|
97
77
|
result = result.replace(/<tg-reference[^>]*>([\s\S]*?)<\/tg-reference>/gi, (_, c) => `<i>${c.trim()}</i>`)
|
|
98
78
|
result = result.replace(/<mark[^>]*>([\s\S]*?)<\/mark>/gi, (_, c) => `<b>[${c.trim()}]</b>`)
|
|
99
79
|
|
|
100
|
-
// 7. Convert Figures & Captions
|
|
101
80
|
result = result.replace(/<figure[^>]*>[\s\S]*?<figcaption[^>]*>([\s\S]*?)<\/figcaption><\/figure>/gi, (_, cap) => `\n<i>š· ${cap.trim()}</i>\n`)
|
|
102
81
|
|
|
103
|
-
// 8. Convert Paragraphs & Linebreaks
|
|
104
82
|
result = result.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `${c.trim()}\n\n`)
|
|
105
83
|
result = result.replace(/<footer[^>]*>([\s\S]*?)<\/footer>/gi, (_, c) => `<i>${c.trim()}</i>\n`)
|
|
106
84
|
result = result.replace(/<br\s*\/?>/gi, '\n')
|
|
107
|
-
result = result.replace(/<hr\s*\/?>/gi, '
|
|
85
|
+
result = result.replace(/<hr\s*\/?>/gi, '\n')
|
|
108
86
|
|
|
109
|
-
//
|
|
87
|
+
// Strip unsupported tags
|
|
110
88
|
result = result.replace(/<sub[^>]*>([\s\S]*?)<\/sub>/gi, '$1')
|
|
111
89
|
result = result.replace(/<sup[^>]*>([\s\S]*?)<\/sup>/gi, '$1')
|
|
112
90
|
result = result.replace(/<tg-(map|collage|slideshow)[^>]*>([\s\S]*?)<\/tg-\1>/gi, '$2')
|
|
113
91
|
result = result.replace(/<(img|video|audio|tg-map)[^>]*\/?>/gi, '')
|
|
114
92
|
|
|
115
|
-
|
|
116
|
-
result = result.replace(/\n{3,}/g, '\n\n')
|
|
117
|
-
|
|
118
|
-
return result.trim()
|
|
93
|
+
return result.replace(/\n{3,}/g, '\n\n').trim()
|
|
119
94
|
}
|
package/src/telegram.ts
CHANGED
|
@@ -1680,29 +1680,85 @@ export class Telegram extends ApiClient {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
|
|
1682
1682
|
/**
|
|
1683
|
-
*
|
|
1684
|
-
* @see https://core.telegram.org/bots/api#editrichmessage
|
|
1683
|
+
* Universal in-place message editor with Modern Rich UI formatting.
|
|
1685
1684
|
*/
|
|
1686
1685
|
async editRichMessage(
|
|
1687
1686
|
chatId: number | string,
|
|
1688
1687
|
messageId: number | string,
|
|
1689
|
-
|
|
1688
|
+
content: any,
|
|
1690
1689
|
extra?: any
|
|
1691
1690
|
): Promise<any> {
|
|
1691
|
+
let richPayload: any = content
|
|
1692
|
+
let htmlText = ''
|
|
1693
|
+
let replyMarkup = extra?.reply_markup
|
|
1694
|
+
|
|
1695
|
+
if (typeof content?.build === 'function') {
|
|
1696
|
+
const built = content.build()
|
|
1697
|
+
richPayload = built
|
|
1698
|
+
if (typeof built === 'object' && built !== null) {
|
|
1699
|
+
htmlText = built.html || built.text || built.caption || ''
|
|
1700
|
+
if (built.reply_markup) {
|
|
1701
|
+
replyMarkup = replyMarkup || built.reply_markup
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
htmlText = String(built || '')
|
|
1705
|
+
}
|
|
1706
|
+
} else if (typeof content === 'object' && content !== null) {
|
|
1707
|
+
richPayload = content
|
|
1708
|
+
htmlText = content.html || content.text || content.caption || ''
|
|
1709
|
+
if (content.reply_markup) {
|
|
1710
|
+
replyMarkup = replyMarkup || content.reply_markup
|
|
1711
|
+
}
|
|
1712
|
+
} else if (typeof content === 'string') {
|
|
1713
|
+
htmlText = content
|
|
1714
|
+
richPayload = content.startsWith('{') || content.includes('<') ? content : { text: content }
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1692
1717
|
const payload: Record<string, any> = {
|
|
1693
1718
|
chat_id: chatId,
|
|
1694
1719
|
message_id: Number(messageId),
|
|
1695
|
-
rich_message:
|
|
1720
|
+
rich_message: richPayload,
|
|
1696
1721
|
...extra,
|
|
1697
1722
|
}
|
|
1723
|
+
|
|
1724
|
+
if (replyMarkup) payload.reply_markup = replyMarkup
|
|
1725
|
+
|
|
1698
1726
|
try {
|
|
1699
|
-
return await this.callApi('
|
|
1727
|
+
return await this.callApi('editMessageText' as never, payload as never)
|
|
1700
1728
|
} catch (err: any) {
|
|
1701
1729
|
const desc = String(err?.description || err?.message || '')
|
|
1702
|
-
//
|
|
1730
|
+
// Ignore if message is unchanged
|
|
1703
1731
|
if (desc.includes('message is not modified')) {
|
|
1704
1732
|
return false
|
|
1705
1733
|
}
|
|
1734
|
+
const fallbackHtml = sanitizeRichHtml(htmlText)
|
|
1735
|
+
// Fallback jika pesan berupa Media (Foto/Video)
|
|
1736
|
+
if (desc.includes('no text in the message') || desc.includes('message to edit not found')) {
|
|
1737
|
+
try {
|
|
1738
|
+
return await this.callApi('editMessageCaption' as never, {
|
|
1739
|
+
chat_id: chatId,
|
|
1740
|
+
message_id: Number(messageId),
|
|
1741
|
+
caption: fallbackHtml,
|
|
1742
|
+
parse_mode: 'HTML',
|
|
1743
|
+
reply_markup: replyMarkup,
|
|
1744
|
+
...extra,
|
|
1745
|
+
} as never)
|
|
1746
|
+
} catch (captionErr: any) {
|
|
1747
|
+
if (String(captionErr?.description).includes('message is not modified')) return false
|
|
1748
|
+
throw captionErr
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
// Fallback untuk Bot API versi lama tanpa parameter rich_message pada editMessageText
|
|
1752
|
+
if (desc.includes('rich_message') || desc.includes('not supported') || desc.includes('wrong type')) {
|
|
1753
|
+
return await this.callApi('editMessageText' as never, {
|
|
1754
|
+
chat_id: chatId,
|
|
1755
|
+
message_id: Number(messageId),
|
|
1756
|
+
text: fallbackHtml,
|
|
1757
|
+
parse_mode: 'HTML',
|
|
1758
|
+
reply_markup: replyMarkup,
|
|
1759
|
+
...extra,
|
|
1760
|
+
} as never)
|
|
1761
|
+
}
|
|
1706
1762
|
throw err
|
|
1707
1763
|
}
|
|
1708
1764
|
}
|
|
@@ -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,9 +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
|
-
*
|
|
694
|
-
* @see https://core.telegram.org/bots/api#editrichmessage
|
|
693
|
+
* Universal in-place message editor with Modern Rich UI formatting.
|
|
695
694
|
*/
|
|
696
|
-
editRichMessage(chatId: number | string, messageId: number | string,
|
|
695
|
+
editRichMessage(chatId: number | string, messageId: number | string, content: any, extra?: any): Promise<any>;
|
|
697
696
|
}
|
|
698
697
|
export default Telegram;
|