teledzik 1.0.7 → 1.0.9
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 +9 -3
- package/lib/telegram.js +21 -7
- package/package.json +2 -2
- package/src/core/helpers/rich-sanitizer.ts +12 -5
- package/src/telegram.ts +21 -8
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
|
|
|
@@ -3,8 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.sanitizeRichHtml = void 0;
|
|
4
4
|
function formatTableToCard(tableHtml) {
|
|
5
5
|
const rows = [];
|
|
6
|
+
let hasHeader = false;
|
|
6
7
|
const rowMatches = tableHtml.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
|
|
7
8
|
for (const row of rowMatches) {
|
|
9
|
+
if (/<th[^>]*>/i.test(row))
|
|
10
|
+
hasHeader = true;
|
|
8
11
|
const cells = [];
|
|
9
12
|
const cellMatches = row.match(/<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi) || [];
|
|
10
13
|
for (const cell of cellMatches) {
|
|
@@ -17,15 +20,18 @@ function formatTableToCard(tableHtml) {
|
|
|
17
20
|
if (rows.length === 0)
|
|
18
21
|
return '';
|
|
19
22
|
// Header vs Row handling (Format sebagai Kartu Elegan)
|
|
20
|
-
const isKeyValue = rows.length >
|
|
23
|
+
const isKeyValue = rows.length > 0 && rows.every((r) => r.length === 2);
|
|
21
24
|
let cardText = '';
|
|
22
25
|
if (isKeyValue) {
|
|
23
|
-
const
|
|
26
|
+
const isHeaderRow = hasHeader && rows.length > 1 && (/^(parameter|informasi|metrik|field|key|unit|format|statistik|opsi|nama|item)$/i.test(rows[0][0].replace(/<[^>]*>/g, '').trim()) ||
|
|
27
|
+
/^(status|keterangan|detail|nilai|value|jumlah|estimasi|tipe|contoh input)$/i.test(rows[0][1].replace(/<[^>]*>/g, '').trim()));
|
|
28
|
+
const dataRows = isHeaderRow ? rows.slice(1) : rows;
|
|
29
|
+
const lines = dataRows.map((r) => `• <b>${r[0]}:</b> ${r[1]}`);
|
|
24
30
|
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`;
|
|
25
31
|
}
|
|
26
32
|
else {
|
|
27
33
|
const lines = rows.map((r, idx) => {
|
|
28
|
-
if (idx === 0)
|
|
34
|
+
if (idx === 0 && hasHeader)
|
|
29
35
|
return `<b>${r.join(' │ ')}</b>`;
|
|
30
36
|
return `${r.join(' │ ')}`;
|
|
31
37
|
});
|
package/lib/telegram.js
CHANGED
|
@@ -1275,10 +1275,12 @@ class Telegram extends client_1.default {
|
|
|
1275
1275
|
* Universal in-place message editor with Modern Rich UI formatting.
|
|
1276
1276
|
*/
|
|
1277
1277
|
async editRichMessage(chatId, messageId, content, extra) {
|
|
1278
|
+
let richPayload = content;
|
|
1278
1279
|
let htmlText = '';
|
|
1279
1280
|
let replyMarkup = extra?.reply_markup;
|
|
1280
1281
|
if (typeof content?.build === 'function') {
|
|
1281
1282
|
const built = content.build();
|
|
1283
|
+
richPayload = built;
|
|
1282
1284
|
if (typeof built === 'object' && built !== null) {
|
|
1283
1285
|
htmlText = built.html || built.text || built.caption || '';
|
|
1284
1286
|
if (built.reply_markup) {
|
|
@@ -1290,6 +1292,7 @@ class Telegram extends client_1.default {
|
|
|
1290
1292
|
}
|
|
1291
1293
|
}
|
|
1292
1294
|
else if (typeof content === 'object' && content !== null) {
|
|
1295
|
+
richPayload = content;
|
|
1293
1296
|
htmlText = content.html || content.text || content.caption || '';
|
|
1294
1297
|
if (content.reply_markup) {
|
|
1295
1298
|
replyMarkup = replyMarkup || content.reply_markup;
|
|
@@ -1297,14 +1300,12 @@ class Telegram extends client_1.default {
|
|
|
1297
1300
|
}
|
|
1298
1301
|
else if (typeof content === 'string') {
|
|
1299
1302
|
htmlText = content;
|
|
1303
|
+
richPayload = content.startsWith('{') || content.includes('<') ? content : { text: content };
|
|
1300
1304
|
}
|
|
1301
|
-
// Format ke Telegram Modern HTML
|
|
1302
|
-
htmlText = (0, rich_sanitizer_1.sanitizeRichHtml)(htmlText);
|
|
1303
1305
|
const payload = {
|
|
1304
1306
|
chat_id: chatId,
|
|
1305
1307
|
message_id: Number(messageId),
|
|
1306
|
-
|
|
1307
|
-
parse_mode: 'HTML',
|
|
1308
|
+
rich_message: richPayload,
|
|
1308
1309
|
...extra,
|
|
1309
1310
|
};
|
|
1310
1311
|
if (replyMarkup)
|
|
@@ -1314,13 +1315,18 @@ class Telegram extends client_1.default {
|
|
|
1314
1315
|
}
|
|
1315
1316
|
catch (err) {
|
|
1316
1317
|
const desc = String(err?.description || err?.message || '');
|
|
1318
|
+
// Ignore if message is unchanged
|
|
1319
|
+
if (desc.includes('message is not modified')) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
const fallbackHtml = (0, rich_sanitizer_1.sanitizeRichHtml)(htmlText);
|
|
1317
1323
|
// Fallback jika pesan berupa Media (Foto/Video)
|
|
1318
1324
|
if (desc.includes('no text in the message') || desc.includes('message to edit not found')) {
|
|
1319
1325
|
try {
|
|
1320
1326
|
return await this.callApi('editMessageCaption', {
|
|
1321
1327
|
chat_id: chatId,
|
|
1322
1328
|
message_id: Number(messageId),
|
|
1323
|
-
caption:
|
|
1329
|
+
caption: fallbackHtml,
|
|
1324
1330
|
parse_mode: 'HTML',
|
|
1325
1331
|
reply_markup: replyMarkup,
|
|
1326
1332
|
...extra,
|
|
@@ -1332,8 +1338,16 @@ class Telegram extends client_1.default {
|
|
|
1332
1338
|
throw captionErr;
|
|
1333
1339
|
}
|
|
1334
1340
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
+
});
|
|
1337
1351
|
}
|
|
1338
1352
|
throw err;
|
|
1339
1353
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
function formatTableToCard(tableHtml: string): string {
|
|
2
2
|
const rows: string[][] = []
|
|
3
|
+
let hasHeader = false
|
|
3
4
|
const rowMatches = tableHtml.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || []
|
|
4
5
|
for (const row of rowMatches) {
|
|
6
|
+
if (/<th[^>]*>/i.test(row)) hasHeader = true
|
|
5
7
|
const cells: string[] = []
|
|
6
8
|
const cellMatches = row.match(/<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi) || []
|
|
7
9
|
for (const cell of cellMatches) {
|
|
@@ -13,14 +15,19 @@ function formatTableToCard(tableHtml: string): string {
|
|
|
13
15
|
if (rows.length === 0) return ''
|
|
14
16
|
|
|
15
17
|
// Header vs Row handling (Format sebagai Kartu Elegan)
|
|
16
|
-
const isKeyValue = rows.length >
|
|
17
|
-
let cardText = ''
|
|
18
|
+
const isKeyValue = rows.length > 0 && rows.every((r) => r.length === 2);
|
|
19
|
+
let cardText = '';
|
|
18
20
|
if (isKeyValue) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
+
const isHeaderRow = hasHeader && rows.length > 1 && (
|
|
22
|
+
/^(parameter|informasi|metrik|field|key|unit|format|statistik|opsi|nama|item)$/i.test(rows[0][0].replace(/<[^>]*>/g, '').trim()) ||
|
|
23
|
+
/^(status|keterangan|detail|nilai|value|jumlah|estimasi|tipe|contoh input)$/i.test(rows[0][1].replace(/<[^>]*>/g, '').trim())
|
|
24
|
+
);
|
|
25
|
+
const dataRows = isHeaderRow ? rows.slice(1) : rows;
|
|
26
|
+
const lines = dataRows.map((r) => `• <b>${r[0]}:</b> ${r[1]}`);
|
|
27
|
+
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`;
|
|
21
28
|
} else {
|
|
22
29
|
const lines = rows.map((r, idx) => {
|
|
23
|
-
if (idx === 0) return `<b>${r.join(' │ ')}</b>`
|
|
30
|
+
if (idx === 0 && hasHeader) return `<b>${r.join(' │ ')}</b>`
|
|
24
31
|
return `${r.join(' │ ')}`
|
|
25
32
|
})
|
|
26
33
|
cardText = `<blockquote>\n${lines.join('\n')}\n</blockquote>\n\n`
|
package/src/telegram.ts
CHANGED
|
@@ -1688,11 +1688,13 @@ export class Telegram extends ApiClient {
|
|
|
1688
1688
|
content: any,
|
|
1689
1689
|
extra?: any
|
|
1690
1690
|
): Promise<any> {
|
|
1691
|
+
let richPayload: any = content
|
|
1691
1692
|
let htmlText = ''
|
|
1692
1693
|
let replyMarkup = extra?.reply_markup
|
|
1693
1694
|
|
|
1694
1695
|
if (typeof content?.build === 'function') {
|
|
1695
1696
|
const built = content.build()
|
|
1697
|
+
richPayload = built
|
|
1696
1698
|
if (typeof built === 'object' && built !== null) {
|
|
1697
1699
|
htmlText = built.html || built.text || built.caption || ''
|
|
1698
1700
|
if (built.reply_markup) {
|
|
@@ -1702,22 +1704,20 @@ export class Telegram extends ApiClient {
|
|
|
1702
1704
|
htmlText = String(built || '')
|
|
1703
1705
|
}
|
|
1704
1706
|
} else if (typeof content === 'object' && content !== null) {
|
|
1707
|
+
richPayload = content
|
|
1705
1708
|
htmlText = content.html || content.text || content.caption || ''
|
|
1706
1709
|
if (content.reply_markup) {
|
|
1707
1710
|
replyMarkup = replyMarkup || content.reply_markup
|
|
1708
1711
|
}
|
|
1709
1712
|
} else if (typeof content === 'string') {
|
|
1710
1713
|
htmlText = content
|
|
1714
|
+
richPayload = content.startsWith('{') || content.includes('<') ? content : { text: content }
|
|
1711
1715
|
}
|
|
1712
1716
|
|
|
1713
|
-
// Format ke Telegram Modern HTML
|
|
1714
|
-
htmlText = sanitizeRichHtml(htmlText)
|
|
1715
|
-
|
|
1716
1717
|
const payload: Record<string, any> = {
|
|
1717
1718
|
chat_id: chatId,
|
|
1718
1719
|
message_id: Number(messageId),
|
|
1719
|
-
|
|
1720
|
-
parse_mode: 'HTML',
|
|
1720
|
+
rich_message: richPayload,
|
|
1721
1721
|
...extra,
|
|
1722
1722
|
}
|
|
1723
1723
|
|
|
@@ -1727,13 +1727,18 @@ export class Telegram extends ApiClient {
|
|
|
1727
1727
|
return await this.callApi('editMessageText' as never, payload as never)
|
|
1728
1728
|
} catch (err: any) {
|
|
1729
1729
|
const desc = String(err?.description || err?.message || '')
|
|
1730
|
+
// Ignore if message is unchanged
|
|
1731
|
+
if (desc.includes('message is not modified')) {
|
|
1732
|
+
return false
|
|
1733
|
+
}
|
|
1734
|
+
const fallbackHtml = sanitizeRichHtml(htmlText)
|
|
1730
1735
|
// Fallback jika pesan berupa Media (Foto/Video)
|
|
1731
1736
|
if (desc.includes('no text in the message') || desc.includes('message to edit not found')) {
|
|
1732
1737
|
try {
|
|
1733
1738
|
return await this.callApi('editMessageCaption' as never, {
|
|
1734
1739
|
chat_id: chatId,
|
|
1735
1740
|
message_id: Number(messageId),
|
|
1736
|
-
caption:
|
|
1741
|
+
caption: fallbackHtml,
|
|
1737
1742
|
parse_mode: 'HTML',
|
|
1738
1743
|
reply_markup: replyMarkup,
|
|
1739
1744
|
...extra,
|
|
@@ -1743,8 +1748,16 @@ export class Telegram extends ApiClient {
|
|
|
1743
1748
|
throw captionErr
|
|
1744
1749
|
}
|
|
1745
1750
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
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)
|
|
1748
1761
|
}
|
|
1749
1762
|
throw err
|
|
1750
1763
|
}
|