@icanseeuanywhere/telekaf 4.16.5 → 4.16.6
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 +418 -387
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,40 +26,55 @@
|
|
|
26
26
|
> This package tracks the latest Telegram Bot API releases and ships features ahead of the upstream package.
|
|
27
27
|
> It is a drop-in replacement: just swap `npm install telegraf` → `npm install @icanseeuanywhere/telekaf` and change your imports from `'telegraf'` to `'@icanseeuanywhere/telekaf'`.
|
|
28
28
|
|
|
29
|
+
|
|
29
30
|
---
|
|
30
31
|
|
|
31
32
|
## 🆕 Rich Messages — Bot API 10.1
|
|
32
33
|
|
|
33
|
-
> Added in **Bot API 10.1** (June 11, 2026). Rich Messages
|
|
34
|
+
> Added in **Bot API 10.1** (June 11, 2026). Rich Messages let bots send highly structured content using HTML or Markdown — headings, lists, tables, media collages, code blocks, math expressions, AI thinking blocks, and more — with streaming support for AI-generated replies.
|
|
34
35
|
|
|
35
36
|
### Table of Contents
|
|
36
37
|
|
|
37
|
-
- [Quick Start](#
|
|
38
|
+
- [Quick Start](#quick-start)
|
|
38
39
|
- [Sending Methods](#sending-methods)
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
40
|
+
- [InputRichMessage Format](#inputrichmessage-format)
|
|
41
|
+
- [RichHTMLBuilder](#richhtmlbuilder)
|
|
42
|
+
- [Text Formatting](#text-formatting-html)
|
|
43
|
+
- [Headings & Paragraphs](#headings--paragraphs)
|
|
44
|
+
- [Lists](#lists-html)
|
|
45
|
+
- [Code Blocks](#code-blocks)
|
|
46
|
+
- [Blockquote & Pull Quote](#blockquote--pull-quote)
|
|
47
|
+
- [Math](#math-html)
|
|
48
|
+
- [Media — Photo, Video, Audio](#media--photo-video-audio)
|
|
49
|
+
- [Collage & Slideshow](#collage--slideshow)
|
|
50
|
+
- [Map](#map)
|
|
51
|
+
- [Table](#table-html)
|
|
52
|
+
- [Details (Expandable)](#details-expandable)
|
|
53
|
+
- [Footnote References](#footnote-references-html)
|
|
54
|
+
- [Anchors & In-document Links](#anchors--in-document-links)
|
|
55
|
+
- [Special Elements](#special-elements)
|
|
56
|
+
- [RichMarkdownBuilder](#richmarkdownbuilder)
|
|
43
57
|
- [Streaming with sendRichMessageDraft](#streaming-with-sendrichmessagedraft)
|
|
58
|
+
- [reply\_markup with Rich Messages](#reply_markup-with-rich-messages)
|
|
44
59
|
- [Inline / Web App Queries](#inline--web-app-queries)
|
|
45
|
-
- [TypeScript
|
|
60
|
+
- [TypeScript](#typescript)
|
|
46
61
|
|
|
47
62
|
---
|
|
48
63
|
|
|
49
|
-
###
|
|
64
|
+
### Quick Start
|
|
50
65
|
|
|
51
66
|
```ts
|
|
52
67
|
import { Telegraf, RichMessage } from '@icanseeuanywhere/telekaf'
|
|
53
|
-
const {
|
|
68
|
+
const { RichHTMLBuilder: HTML } = RichMessage
|
|
54
69
|
|
|
55
70
|
const bot = new Telegraf(process.env.BOT_TOKEN)
|
|
56
71
|
|
|
57
72
|
bot.command('hello', async (ctx) => {
|
|
58
|
-
const msg = new
|
|
59
|
-
.heading(1,
|
|
60
|
-
.paragraph(
|
|
73
|
+
const msg = new HTML()
|
|
74
|
+
.heading(1, 'Hello World!')
|
|
75
|
+
.paragraph(HTML.bold('Rich Messages') + ' are now supported in Bot API 10.1.')
|
|
61
76
|
.divider()
|
|
62
|
-
.
|
|
77
|
+
.ul('Headings', 'Lists', 'Tables', 'Media', 'Math', 'and more')
|
|
63
78
|
.build()
|
|
64
79
|
|
|
65
80
|
await ctx.sendRichMessage(msg)
|
|
@@ -74,478 +89,506 @@ process.once('SIGTERM', () => bot.stop('SIGTERM'))
|
|
|
74
89
|
|
|
75
90
|
### Sending Methods
|
|
76
91
|
|
|
77
|
-
There are three ways to send rich messages:
|
|
78
|
-
|
|
79
92
|
| Method | Description |
|
|
80
93
|
|---|---|
|
|
81
94
|
| `ctx.sendRichMessage(msg, extra?)` | Send a rich message to the current chat |
|
|
82
|
-
| `ctx.replyWithRichMessageContent(msg, extra?)` |
|
|
83
|
-
| `ctx.sendRichMessageDraft(msg, extra?)` | Stream a partial
|
|
84
|
-
| `ctx.telegram.sendRichMessage(chatId, msg, extra?)` | Explicit
|
|
85
|
-
| `ctx.telegram.sendRichMessageDraft(chatId, msg, extra?)` | Explicit streaming with chat ID |
|
|
95
|
+
| `ctx.replyWithRichMessageContent(msg, extra?)` | Send a rich message quoting the current message |
|
|
96
|
+
| `ctx.sendRichMessageDraft(draftId, msg, extra?)` | Stream a partial draft (private chats only) |
|
|
97
|
+
| `ctx.telegram.sendRichMessage(chatId, msg, extra?)` | Explicit call with chat ID |
|
|
98
|
+
| `ctx.telegram.sendRichMessageDraft(chatId, draftId, msg, extra?)` | Explicit streaming with chat ID |
|
|
86
99
|
|
|
87
|
-
|
|
88
|
-
bot.on('message', async (ctx) => {
|
|
89
|
-
const msg = new Builder()
|
|
90
|
-
.paragraph(RT.plain('I am replying to your message!'))
|
|
91
|
-
.build()
|
|
100
|
+
**`extra` options for `sendRichMessage`:**
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
```ts
|
|
103
|
+
await ctx.sendRichMessage(msg, {
|
|
104
|
+
message_thread_id: 123, // for forum topics
|
|
105
|
+
direct_messages_topic_id: 456, // for DM topics
|
|
106
|
+
disable_notification: true,
|
|
107
|
+
protect_content: true,
|
|
108
|
+
allow_paid_broadcast: false,
|
|
109
|
+
message_effect_id: 'effect_id',
|
|
110
|
+
reply_parameters: { message_id: ctx.message.message_id },
|
|
111
|
+
reply_markup: { inline_keyboard: [[{ text: 'OK', callback_data: 'ok' }]] },
|
|
95
112
|
})
|
|
96
113
|
```
|
|
97
114
|
|
|
98
|
-
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### InputRichMessage Format
|
|
118
|
+
|
|
119
|
+
`InputRichMessage` uses **either** `html` or `markdown` — not both:
|
|
99
120
|
|
|
100
121
|
```ts
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
// HTML format
|
|
123
|
+
const msg: InputRichMessage = {
|
|
124
|
+
html: '<h1>Title</h1><p>Body text</p>',
|
|
125
|
+
is_rtl: false,
|
|
126
|
+
skip_entity_detection: false,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Markdown format
|
|
130
|
+
const msg: InputRichMessage = {
|
|
131
|
+
markdown: '# Title\n\nBody text',
|
|
107
132
|
}
|
|
108
133
|
```
|
|
109
134
|
|
|
110
|
-
|
|
135
|
+
Use the builders — `RichHTMLBuilder` or `RichMarkdownBuilder` — to construct these conveniently.
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
---
|
|
113
138
|
|
|
114
|
-
|
|
139
|
+
### RichHTMLBuilder
|
|
115
140
|
|
|
116
|
-
|
|
141
|
+
Import and instantiate:
|
|
117
142
|
|
|
118
143
|
```ts
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
RT.italic(RT.plain('italic'))
|
|
122
|
-
RT.underline(RT.plain('underlined'))
|
|
123
|
-
RT.strikethrough(RT.plain('crossed out'))
|
|
124
|
-
RT.spoiler(RT.plain('hidden until tapped'))
|
|
125
|
-
RT.marked(RT.plain('highlighted')) // like a text marker
|
|
126
|
-
RT.subscript(RT.plain('₂'))
|
|
127
|
-
RT.superscript(RT.plain('²'))
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Nesting is supported:
|
|
144
|
+
import { RichMessage } from '@icanseeuanywhere/telekaf'
|
|
145
|
+
const { RichHTMLBuilder: HTML } = RichMessage
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
const msg = new HTML()
|
|
148
|
+
.heading(1, 'Title')
|
|
149
|
+
.paragraph('Content')
|
|
150
|
+
.build() // returns InputRichMessage { html: '...' }
|
|
135
151
|
```
|
|
136
152
|
|
|
137
|
-
####
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
RT.code(RT.plain('const x = 1'), 'javascript') // inline code with language
|
|
141
|
-
RT.math('E = mc^2') // inline mathematical expression
|
|
142
|
-
```
|
|
153
|
+
#### Text Formatting (HTML)
|
|
143
154
|
|
|
144
|
-
|
|
155
|
+
Static inline helpers — return strings to embed inside block methods:
|
|
145
156
|
|
|
146
157
|
```ts
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
HTML.bold('bold text') // <b>bold text</b>
|
|
159
|
+
HTML.italic('italic text') // <i>italic text</i>
|
|
160
|
+
HTML.underline('underlined') // <u>underlined</u>
|
|
161
|
+
HTML.strikethrough('crossed out') // <s>crossed out</s>
|
|
162
|
+
HTML.spoiler('hidden until tapped') // <tg-spoiler>hidden</tg-spoiler>
|
|
163
|
+
HTML.code('inline code') // <code>inline code</code>
|
|
164
|
+
HTML.marked('highlighted') // <mark>highlighted</mark>
|
|
165
|
+
HTML.sub('subscript') // <sub>subscript</sub>
|
|
166
|
+
HTML.sup('superscript') // <sup>superscript</sup>
|
|
167
|
+
|
|
168
|
+
HTML.url('https://t.me', 'Telegram') // <a href="...">Telegram</a>
|
|
169
|
+
HTML.email('hi@bot.com', 'Email us') // <a href="mailto:...">Email us</a>
|
|
170
|
+
HTML.phone('+6281234567', 'Call us') // <a href="tel:...">Call us</a>
|
|
171
|
+
HTML.mention(123456789, 'Alice') // <a href="tg://user?id=...">Alice</a>
|
|
172
|
+
HTML.customEmoji('5368324170671202286', '👍') // <tg-emoji emoji-id="...">👍</tg-emoji>
|
|
173
|
+
HTML.time(1647531900, 'wDT', '22:45 tomorrow') // <tg-time unix="..." format="...">...</tg-time>
|
|
174
|
+
HTML.inlineMath('E = mc^2') // $E = mc^2$
|
|
175
|
+
|
|
176
|
+
// Nesting
|
|
177
|
+
HTML.bold(HTML.italic('bold italic'))
|
|
178
|
+
HTML.underline(HTML.spoiler('underlined spoiler'))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Headings & Paragraphs
|
|
155
182
|
|
|
156
183
|
```ts
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
184
|
+
new HTML()
|
|
185
|
+
.heading(1, 'Main Title')
|
|
186
|
+
.heading(2, 'Subtitle')
|
|
187
|
+
.heading(3, HTML.bold('Bold heading'))
|
|
188
|
+
.heading(4, 'H4')
|
|
189
|
+
.heading(5, 'H5')
|
|
190
|
+
.heading(6, 'H6')
|
|
191
|
+
.paragraph('Normal paragraph text.')
|
|
192
|
+
.paragraph(HTML.bold('Bold') + ' and ' + HTML.italic('italic') + ' combined.')
|
|
193
|
+
.footer('Footer text — smaller and muted')
|
|
194
|
+
.divider() // <hr/>
|
|
195
|
+
.build()
|
|
163
196
|
```
|
|
164
197
|
|
|
165
|
-
####
|
|
198
|
+
#### Lists (HTML)
|
|
166
199
|
|
|
167
200
|
```ts
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
201
|
+
new HTML()
|
|
202
|
+
// Unordered list
|
|
203
|
+
.ul('First item', 'Second item', HTML.bold('Bold item'))
|
|
204
|
+
|
|
205
|
+
// Ordered list
|
|
206
|
+
.ol('Step one', 'Step two', 'Step three')
|
|
207
|
+
|
|
208
|
+
// Task list (checkboxes)
|
|
209
|
+
.taskList(
|
|
210
|
+
{ text: 'Completed task', checked: true },
|
|
211
|
+
{ text: 'Pending task', checked: false },
|
|
212
|
+
{ text: HTML.bold('Important task'), checked: false },
|
|
213
|
+
)
|
|
214
|
+
.build()
|
|
171
215
|
```
|
|
172
216
|
|
|
173
|
-
####
|
|
217
|
+
#### Code Blocks
|
|
174
218
|
|
|
175
219
|
```ts
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
await ctx.sendRichMessage(builder.build())
|
|
220
|
+
new HTML()
|
|
221
|
+
.pre('npm install @icanseeuanywhere/telekaf', 'bash')
|
|
222
|
+
.pre('SELECT * FROM users WHERE active = 1;', 'sql')
|
|
223
|
+
.pre(
|
|
224
|
+
`const bot = new Telegraf(token)
|
|
225
|
+
bot.launch()`,
|
|
226
|
+
'javascript'
|
|
227
|
+
)
|
|
228
|
+
.pre('plain preformatted block without language')
|
|
229
|
+
.build()
|
|
188
230
|
```
|
|
189
231
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
### RichBlock — Block Elements
|
|
193
|
-
|
|
194
|
-
`RichBlock` types are block-level elements that form the structure of a rich message.
|
|
232
|
+
Supported language identifiers: `javascript`, `typescript`, `python`, `bash`, `sql`, `json`, `html`, `css`, etc.
|
|
195
233
|
|
|
196
|
-
####
|
|
234
|
+
#### Blockquote & Pull Quote
|
|
197
235
|
|
|
198
236
|
```ts
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// Preformatted / code block
|
|
209
|
-
new Builder().preformatted('SELECT * FROM users;', 'sql')
|
|
210
|
-
new Builder().preformatted('plain preformatted block')
|
|
237
|
+
new HTML()
|
|
238
|
+
// Standard block quotation
|
|
239
|
+
.blockQuote(HTML.italic('"To be or not to be."'))
|
|
240
|
+
|
|
241
|
+
// Pull quotation with attribution (cite)
|
|
242
|
+
.pullQuote(
|
|
243
|
+
HTML.italic('"Design is not just what it looks like."'),
|
|
244
|
+
'Steve Jobs'
|
|
245
|
+
)
|
|
211
246
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
247
|
+
// Nested formatting inside quote
|
|
248
|
+
.blockQuote(
|
|
249
|
+
HTML.bold('Telekaf') + ' supports ' + HTML.marked('highlighted') +
|
|
250
|
+
' and ' + HTML.spoiler('spoiler') + ' text inside quotes.'
|
|
251
|
+
)
|
|
252
|
+
.build()
|
|
215
253
|
```
|
|
216
254
|
|
|
217
|
-
|
|
218
|
-
import type { RichBlock } from '@icanseeuanywhere/telekaf/types'
|
|
255
|
+
#### Math (HTML)
|
|
219
256
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
257
|
+
```ts
|
|
258
|
+
new HTML()
|
|
259
|
+
// Inline math (embed inside paragraph)
|
|
260
|
+
.paragraph(
|
|
261
|
+
'The formula is: ' + HTML.inlineMath('a^2 + b^2 = c^2')
|
|
262
|
+
)
|
|
224
263
|
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
.
|
|
228
|
-
.
|
|
264
|
+
// Block math expression
|
|
265
|
+
.mathBlock('\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}')
|
|
266
|
+
.mathBlock('F(x) = \\int_{-\\infty}^{x} f(t)\\,dt')
|
|
267
|
+
.mathBlock('E = mc^2')
|
|
229
268
|
.build()
|
|
230
|
-
|
|
231
|
-
// Append footer manually
|
|
232
|
-
msg.blocks.push(footer)
|
|
233
|
-
await ctx.sendRichMessage(msg)
|
|
234
269
|
```
|
|
235
270
|
|
|
236
|
-
####
|
|
271
|
+
#### Media — Photo, Video, Audio
|
|
237
272
|
|
|
238
273
|
```ts
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
274
|
+
new HTML()
|
|
275
|
+
// Photo
|
|
276
|
+
.photo('https://example.com/photo.jpg')
|
|
277
|
+
.photo('https://example.com/photo.jpg', 'Caption text')
|
|
278
|
+
.photo('https://example.com/photo.jpg', 'Spoiler photo', /* spoiler */ true)
|
|
279
|
+
|
|
280
|
+
// Video
|
|
281
|
+
.video('https://example.com/video.mp4')
|
|
282
|
+
.video('https://example.com/video.mp4', 'Caption text')
|
|
283
|
+
.video('https://example.com/video.mp4', 'Spoiler video', /* spoiler */ true)
|
|
284
|
+
|
|
285
|
+
// Audio / Voice note (.ogg for voice)
|
|
286
|
+
.audio('https://example.com/audio.mp3')
|
|
287
|
+
.audio('https://example.com/audio.mp3', 'Audio caption')
|
|
288
|
+
|
|
289
|
+
// With figcaption (HTML figure)
|
|
290
|
+
.raw('<figure><img src="https://example.com/photo.jpg"/><figcaption>Caption <b>bold</b></figcaption></figure>')
|
|
291
|
+
.build()
|
|
251
292
|
```
|
|
252
293
|
|
|
253
|
-
|
|
294
|
+
You can also use a Telegram `file_id` in place of a URL once the file is uploaded.
|
|
254
295
|
|
|
255
|
-
|
|
256
|
-
// Block quotation
|
|
257
|
-
new Builder().blockQuote(
|
|
258
|
-
{ type: 'paragraph', texts: [RT.italic(RT.plain('To be or not to be.'))] },
|
|
259
|
-
)
|
|
296
|
+
#### Collage & Slideshow
|
|
260
297
|
|
|
261
|
-
|
|
262
|
-
|
|
298
|
+
```ts
|
|
299
|
+
const img = (src) => `<img src="${src}"/>`
|
|
300
|
+
const vid = (src) => `<video src="${src}"></video>`
|
|
301
|
+
|
|
302
|
+
new HTML()
|
|
303
|
+
// Collage — displays as a grid
|
|
304
|
+
.collage(
|
|
305
|
+
img('https://example.com/photo1.jpg'),
|
|
306
|
+
img('https://example.com/photo2.jpg'),
|
|
307
|
+
vid('https://example.com/clip.mp4'),
|
|
308
|
+
)
|
|
263
309
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
310
|
+
// Slideshow — swipeable carousel
|
|
311
|
+
.slideshow(
|
|
312
|
+
img('https://example.com/photo1.jpg'),
|
|
313
|
+
img('https://example.com/photo2.jpg'),
|
|
314
|
+
img('https://example.com/photo3.jpg'),
|
|
315
|
+
)
|
|
316
|
+
.build()
|
|
269
317
|
```
|
|
270
318
|
|
|
271
|
-
####
|
|
319
|
+
#### Map
|
|
272
320
|
|
|
273
321
|
```ts
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// Single audio
|
|
281
|
-
new Builder().audio('CQACAgIAAxk...')
|
|
282
|
-
|
|
283
|
-
// Voice note
|
|
284
|
-
import type { RichBlock } from '@icanseeuanywhere/telekaf/types'
|
|
285
|
-
const voiceBlock: RichBlock = {
|
|
286
|
-
type: 'voice_note',
|
|
287
|
-
file_id: 'AwACAgIAAxk...',
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Collage (multiple photos/videos in a grid)
|
|
291
|
-
import type { RichBlock } from '@icanseeuanywhere/telekaf/types'
|
|
292
|
-
const collage: RichBlock = {
|
|
293
|
-
type: 'collage',
|
|
294
|
-
media: [
|
|
295
|
-
{ type: 'photo', file_id: 'AgACAgIA...' },
|
|
296
|
-
{ type: 'photo', file_id: 'AgACAgIB...' },
|
|
297
|
-
{ type: 'video', file_id: 'BAACAgIA...' },
|
|
298
|
-
],
|
|
299
|
-
caption: { type: 'caption', texts: [RT.plain('Our gallery')] },
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Slideshow (swipeable gallery)
|
|
303
|
-
const slideshow: RichBlock = {
|
|
304
|
-
type: 'slideshow',
|
|
305
|
-
media: [
|
|
306
|
-
{ type: 'photo', file_id: 'AgACAgIA...' },
|
|
307
|
-
{ type: 'photo', file_id: 'AgACAgIB...' },
|
|
308
|
-
],
|
|
309
|
-
}
|
|
322
|
+
new HTML()
|
|
323
|
+
.map(-6.2088, 106.8456) // Jakarta
|
|
324
|
+
.map(48.8584, 2.2945, 16) // Paris, zoom 16
|
|
325
|
+
.map(51.5074, -0.1278, 14, 'Our office in London') // with caption
|
|
326
|
+
.build()
|
|
310
327
|
```
|
|
311
328
|
|
|
312
|
-
#### Table
|
|
329
|
+
#### Table (HTML)
|
|
313
330
|
|
|
314
331
|
```ts
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const table: RichBlock = {
|
|
318
|
-
type: 'table',
|
|
319
|
-
is_bordered: true,
|
|
320
|
-
is_striped: true,
|
|
321
|
-
cells: [
|
|
322
|
-
// Header row
|
|
332
|
+
new HTML()
|
|
333
|
+
.table(
|
|
323
334
|
[
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
['Name', 'Version', 'Downloads'], // header row (hasHeader: true)
|
|
336
|
+
['telekaf', '4.16.5', '—' ],
|
|
337
|
+
['telegraf','4.16.3', '~120k/wk' ],
|
|
326
338
|
],
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{ type: 'table_cell', content: [{ type: 'paragraph', texts: [RT.plain('98')] }] },
|
|
331
|
-
],
|
|
332
|
-
[
|
|
333
|
-
{ type: 'table_cell', content: [{ type: 'paragraph', texts: [RT.plain('Bob')] }] },
|
|
334
|
-
{ type: 'table_cell', content: [{ type: 'paragraph', texts: [RT.plain('87')] }] },
|
|
335
|
-
],
|
|
336
|
-
],
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
await ctx.sendRichMessage({ blocks: [table] })
|
|
339
|
+
{ bordered: true, striped: true, hasHeader: true }
|
|
340
|
+
)
|
|
341
|
+
.build()
|
|
340
342
|
```
|
|
341
343
|
|
|
342
|
-
|
|
344
|
+
For advanced table markup (colspan, rowspan, alignment), use `.raw()`:
|
|
343
345
|
|
|
344
346
|
```ts
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
new HTML()
|
|
348
|
+
.raw(
|
|
349
|
+
'<table bordered striped>' +
|
|
350
|
+
'<tr><th>Name</th><th colspan="2">Details</th></tr>' +
|
|
351
|
+
'<tr><td>Alice</td><td align="center">98</td><td align="right">Pass</td></tr>' +
|
|
352
|
+
'</table>'
|
|
353
|
+
)
|
|
354
|
+
.build()
|
|
347
355
|
```
|
|
348
356
|
|
|
349
|
-
#### Expandable
|
|
357
|
+
#### Details (Expandable)
|
|
350
358
|
|
|
351
359
|
```ts
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
new HTML()
|
|
361
|
+
// Collapsed by default
|
|
362
|
+
.details('Click to expand', '<p>Hidden content here.</p>')
|
|
363
|
+
|
|
364
|
+
// Open by default
|
|
365
|
+
.details(
|
|
366
|
+
HTML.bold('Changelog v4.16.5'),
|
|
367
|
+
'<ul><li>Fix InputRichMessage format</li><li>Add RichHTMLBuilder</li></ul>',
|
|
368
|
+
/* open */ true
|
|
369
|
+
)
|
|
370
|
+
.build()
|
|
361
371
|
```
|
|
362
372
|
|
|
363
|
-
####
|
|
373
|
+
#### Footnote References (HTML)
|
|
364
374
|
|
|
365
375
|
```ts
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
376
|
+
new HTML()
|
|
377
|
+
.paragraph(
|
|
378
|
+
'Telekaf ' + HTML.ref('note-1', '[1]') + ' is based on Telegraf ' + HTML.ref('note-2', '[2]') + '.'
|
|
379
|
+
)
|
|
380
|
+
.divider()
|
|
381
|
+
.referenceDefinition('note-1', HTML.url('https://npmjs.com/package/@icanseeuanywhere/telekaf', 'telekaf on npm'))
|
|
382
|
+
.referenceDefinition('note-2', HTML.url('https://github.com/telegraf/telegraf', 'telegraf on GitHub'))
|
|
383
|
+
.build()
|
|
375
384
|
```
|
|
376
385
|
|
|
377
|
-
####
|
|
386
|
+
#### Anchors & In-document Links
|
|
378
387
|
|
|
379
388
|
```ts
|
|
380
|
-
new
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
389
|
+
new HTML()
|
|
390
|
+
.raw(HTML.anchor('section-intro')) // invisible anchor target
|
|
391
|
+
.heading(2, 'Introduction')
|
|
392
|
+
.paragraph('Jump to: ' + HTML.anchorLink('section-api', 'API Reference'))
|
|
393
|
+
.raw(HTML.anchor('section-api'))
|
|
394
|
+
.heading(2, 'API Reference')
|
|
395
|
+
.build()
|
|
384
396
|
```
|
|
385
397
|
|
|
386
|
-
####
|
|
398
|
+
#### Special Elements
|
|
387
399
|
|
|
388
400
|
```ts
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
expression: '\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}',
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
#### Thinking Block (AI reasoning)
|
|
401
|
+
new HTML()
|
|
402
|
+
// AI thinking block (visible in sendRichMessageDraft)
|
|
403
|
+
.thinking(HTML.italic('Analyzing your request...'))
|
|
397
404
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
{ type: 'paragraph', texts: [RT.plain('Let me reason through this step by step...')] },
|
|
403
|
-
{ type: 'paragraph', texts: [RT.plain('First, I consider the user intent.')] },
|
|
404
|
-
)
|
|
405
|
+
// Raw HTML for anything not covered by builder methods
|
|
406
|
+
.raw('<tg-map lat="41.9" long="12.5" zoom="14"/>')
|
|
407
|
+
.raw('<aside>Pull quote<cite>The Author</cite></aside>')
|
|
408
|
+
.build()
|
|
405
409
|
```
|
|
406
410
|
|
|
407
411
|
---
|
|
408
412
|
|
|
409
|
-
###
|
|
413
|
+
### RichMarkdownBuilder
|
|
410
414
|
|
|
411
|
-
|
|
415
|
+
Same API surface as `RichHTMLBuilder` but produces Markdown output:
|
|
412
416
|
|
|
413
417
|
```ts
|
|
414
418
|
import { RichMessage } from '@icanseeuanywhere/telekaf'
|
|
415
|
-
const {
|
|
416
|
-
|
|
417
|
-
const msg = new
|
|
418
|
-
.heading(1,
|
|
419
|
-
.paragraph(
|
|
420
|
-
|
|
419
|
+
const { RichMarkdownBuilder: MD } = RichMessage
|
|
420
|
+
|
|
421
|
+
const msg = new MD()
|
|
422
|
+
.heading(1, 'Rich Markdown')
|
|
423
|
+
.paragraph(
|
|
424
|
+
MD.bold('bold') + ' ' +
|
|
425
|
+
MD.italic('italic') + ' ' +
|
|
426
|
+
MD.strikethrough('strike') + ' ' +
|
|
427
|
+
MD.marked('==highlighted==') + ' ' +
|
|
428
|
+
MD.spoiler('||spoiler||') + ' ' +
|
|
429
|
+
MD.code('`code`')
|
|
430
|
+
)
|
|
421
431
|
.divider()
|
|
422
|
-
.
|
|
423
|
-
|
|
424
|
-
|
|
432
|
+
.ul('Item 1', 'Item 2', MD.bold('Bold item'))
|
|
433
|
+
.ol('Step 1', 'Step 2', 'Step 3')
|
|
434
|
+
.taskList(
|
|
435
|
+
{ text: 'Done', checked: true },
|
|
436
|
+
{ text: 'Pending', checked: false },
|
|
425
437
|
)
|
|
426
|
-
.
|
|
427
|
-
.
|
|
428
|
-
.
|
|
429
|
-
.
|
|
430
|
-
|
|
431
|
-
|
|
438
|
+
.divider()
|
|
439
|
+
.pre('console.log("hello")', 'javascript')
|
|
440
|
+
.divider()
|
|
441
|
+
.table(
|
|
442
|
+
['Name', 'Score'],
|
|
443
|
+
[['Alice', '98'], ['Bob', '87']],
|
|
444
|
+
['left', 'center'],
|
|
445
|
+
)
|
|
446
|
+
.divider()
|
|
447
|
+
.mathBlock('E = mc^2')
|
|
448
|
+
.divider()
|
|
449
|
+
.blockQuote(MD.italic('"Quote text"'), '— Author')
|
|
450
|
+
.divider()
|
|
451
|
+
.photo('https://example.com/photo.jpg', 'Photo caption')
|
|
452
|
+
.collage('https://example.com/1.jpg', 'https://example.com/2.jpg')
|
|
453
|
+
.slideshow('https://example.com/1.jpg', 'https://example.com/2.jpg')
|
|
454
|
+
.divider()
|
|
455
|
+
.details('Expand me', '### Hidden heading\n\n- item 1\n- item 2')
|
|
456
|
+
.divider()
|
|
457
|
+
.paragraph('See footnote' + MD.sup('[1]'))
|
|
458
|
+
.footnote('1', MD.url('https://t.me/kafk6', '@kafka'))
|
|
459
|
+
.build()
|
|
432
460
|
```
|
|
433
461
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
| Method | Description |
|
|
437
|
-
|---|---|
|
|
438
|
-
| `.paragraph(...texts)` | Add a paragraph block |
|
|
439
|
-
| `.heading(level, ...texts)` | Add a heading (h1–h6) |
|
|
440
|
-
| `.preformatted(text, language?)` | Add a code/preformatted block |
|
|
441
|
-
| `.divider()` | Add a horizontal divider |
|
|
442
|
-
| `.list(isOrdered, ...items)` | Add an ordered or unordered list |
|
|
443
|
-
| `.blockQuote(...blocks)` | Add a block quotation |
|
|
444
|
-
| `.thinking(...blocks)` | Add an AI thinking block |
|
|
445
|
-
| `.photo(fileId, caption?)` | Add a photo block |
|
|
446
|
-
| `.video(fileId, caption?)` | Add a video block |
|
|
447
|
-
| `.audio(fileId, caption?)` | Add an audio block |
|
|
448
|
-
| `.reference(richText)` | Register a footnote, returns its numeric ID |
|
|
449
|
-
| `.build()` | Returns the final `InputRichMessage` |
|
|
450
|
-
|
|
451
|
-
---
|
|
452
|
-
|
|
453
|
-
### RT Helpers
|
|
454
|
-
|
|
455
|
-
`RT` is a collection of factory functions for creating `RichText` nodes concisely.
|
|
462
|
+
**All Markdown inline helpers:**
|
|
456
463
|
|
|
457
464
|
```ts
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
RT.phone('+1234567890')
|
|
475
|
-
RT.bankCard('4111111111111111')
|
|
476
|
-
RT.mention(RT.plain('Alice'), 123456789)
|
|
477
|
-
RT.mention(RT.plain('@alice'), undefined, 'alice')
|
|
478
|
-
RT.botCommand('/start')
|
|
479
|
-
RT.botCommand('/help', 'mybot')
|
|
480
|
-
RT.hashtag('news')
|
|
481
|
-
RT.cashtag('ETH')
|
|
482
|
-
RT.dateTime(1750000000)
|
|
483
|
-
RT.customEmoji('5368324170671202286')
|
|
484
|
-
RT.anchor('section-id')
|
|
485
|
-
RT.anchorLink('section-id', RT.plain('Go to section'))
|
|
486
|
-
RT.reference(1) // footnote number
|
|
487
|
-
RT.referenceLink(1, RT.plain('[1]')) // clickable footnote
|
|
465
|
+
MD.bold('text') // **text**
|
|
466
|
+
MD.italic('text') // *text*
|
|
467
|
+
MD.underline('text') // <u>text</u>
|
|
468
|
+
MD.strikethrough('text') // ~~text~~
|
|
469
|
+
MD.spoiler('text') // ||text||
|
|
470
|
+
MD.code('text') // `text`
|
|
471
|
+
MD.marked('text') // ==text==
|
|
472
|
+
MD.sub('text') // <sub>text</sub>
|
|
473
|
+
MD.sup('text') // <sup>text</sup>
|
|
474
|
+
MD.url('https://...', 'label') // [label](url)
|
|
475
|
+
MD.email('a@b.com', 'label') // [label](mailto:a@b.com)
|
|
476
|
+
MD.phone('+123', 'label') // [label](tel:+123)
|
|
477
|
+
MD.mention(123456789, 'Alice') // [Alice](tg://user?id=123456789)
|
|
478
|
+
MD.customEmoji('id', '👍') // 
|
|
479
|
+
MD.time(1647531900, 'wDT') // 
|
|
480
|
+
MD.inlineMath('a^2') // $a^2$
|
|
488
481
|
```
|
|
489
482
|
|
|
490
483
|
---
|
|
491
484
|
|
|
492
|
-
### Streaming with
|
|
485
|
+
### Streaming with sendRichMessageDraft
|
|
493
486
|
|
|
494
|
-
`sendRichMessageDraft`
|
|
487
|
+
`sendRichMessageDraft` streams a partial rich message in private chats. The draft is ephemeral (30-second preview). You **must** finalize with `sendRichMessage` to persist it.
|
|
495
488
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const { RichMessageBuilder: Builder, RT } = RichMessage
|
|
489
|
+
- `chat_id` — private chat only (integer)
|
|
490
|
+
- `draft_id` — non-zero integer; updates with the same `draft_id` are animated
|
|
499
491
|
|
|
492
|
+
```ts
|
|
500
493
|
bot.command('ai', async (ctx) => {
|
|
501
|
-
|
|
502
|
-
await ctx.sendRichMessageDraft(
|
|
503
|
-
new Builder()
|
|
504
|
-
.thinking({ type: 'paragraph', texts: [RT.plain('Analyzing your request...')] })
|
|
505
|
-
.build()
|
|
506
|
-
)
|
|
494
|
+
const DRAFT_ID = 1 // any non-zero integer
|
|
507
495
|
|
|
508
|
-
// Simulate streaming by sending updated drafts
|
|
509
496
|
const steps = [
|
|
510
|
-
'
|
|
511
|
-
'
|
|
512
|
-
'Composing
|
|
497
|
+
'Reading your request...',
|
|
498
|
+
'Searching knowledge base...',
|
|
499
|
+
'Composing answer...',
|
|
513
500
|
]
|
|
514
501
|
|
|
502
|
+
// Stream thinking blocks
|
|
515
503
|
for (const step of steps) {
|
|
516
|
-
await new Promise((r) => setTimeout(r, 800))
|
|
517
504
|
await ctx.sendRichMessageDraft(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
.build()
|
|
505
|
+
DRAFT_ID,
|
|
506
|
+
new HTML().thinking(HTML.italic(step)).build()
|
|
521
507
|
)
|
|
508
|
+
await new Promise((r) => setTimeout(r, 900))
|
|
522
509
|
}
|
|
523
510
|
|
|
524
|
-
//
|
|
511
|
+
// Finalize — must call sendRichMessage after streaming
|
|
525
512
|
await ctx.sendRichMessage(
|
|
526
|
-
new
|
|
527
|
-
.heading(2,
|
|
528
|
-
.paragraph(
|
|
513
|
+
new HTML()
|
|
514
|
+
.heading(2, '🤖 AI Response')
|
|
515
|
+
.paragraph('Here is the final answer from the AI.')
|
|
516
|
+
.divider()
|
|
517
|
+
.footer(HTML.url('https://t.me/kafk6', '@kafka'))
|
|
529
518
|
.build()
|
|
530
519
|
)
|
|
531
520
|
})
|
|
532
521
|
```
|
|
533
522
|
|
|
523
|
+
You can also call `sendRichMessageDraft` explicitly:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
// Explicit
|
|
527
|
+
await ctx.telegram.sendRichMessageDraft(
|
|
528
|
+
ctx.chat.id, // private chat integer ID
|
|
529
|
+
42, // draft_id
|
|
530
|
+
new HTML().thinking('Processing...').build(),
|
|
531
|
+
{ message_thread_id: 123 }
|
|
532
|
+
)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
### reply_markup with Rich Messages
|
|
538
|
+
|
|
539
|
+
All `sendRichMessage` calls accept a `reply_markup` option with inline keyboards:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { Markup } from '@icanseeuanywhere/telekaf'
|
|
543
|
+
|
|
544
|
+
const msg = new HTML()
|
|
545
|
+
.heading(2, 'Choose an option')
|
|
546
|
+
.paragraph('Tap a button below:')
|
|
547
|
+
.build()
|
|
548
|
+
|
|
549
|
+
await ctx.sendRichMessage(msg, {
|
|
550
|
+
reply_markup: Markup.inlineKeyboard([
|
|
551
|
+
[
|
|
552
|
+
Markup.button.callback('✅ Yes', 'answer:yes'),
|
|
553
|
+
Markup.button.callback('❌ No', 'answer:no'),
|
|
554
|
+
],
|
|
555
|
+
[Markup.button.url('🌐 Visit', 'https://t.me/kafk6')],
|
|
556
|
+
]).reply_markup,
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
bot.action('answer:yes', async (ctx) => {
|
|
560
|
+
await ctx.answerCbQuery('You chose Yes!')
|
|
561
|
+
})
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Or use `reply_markup` directly:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
await ctx.sendRichMessage(msg, {
|
|
568
|
+
reply_markup: {
|
|
569
|
+
inline_keyboard: [
|
|
570
|
+
[{ text: 'Button 1', callback_data: 'btn1' }],
|
|
571
|
+
[{ text: 'Open URL', url: 'https://telegram.org' }],
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
})
|
|
575
|
+
```
|
|
576
|
+
|
|
534
577
|
---
|
|
535
578
|
|
|
536
579
|
### Inline / Web App Queries
|
|
537
580
|
|
|
538
|
-
`InputRichMessageContent`
|
|
581
|
+
Use `InputRichMessageContent` as `input_message_content` in inline query results:
|
|
539
582
|
|
|
540
583
|
```ts
|
|
541
584
|
import { RichMessage } from '@icanseeuanywhere/telekaf'
|
|
542
|
-
const {
|
|
585
|
+
const { RichHTMLBuilder: HTML } = RichMessage
|
|
543
586
|
|
|
544
587
|
bot.on('inline_query', async (ctx) => {
|
|
545
588
|
const richContent: RichMessage.InputRichMessageContent = {
|
|
546
|
-
rich_message: new
|
|
547
|
-
.heading(1,
|
|
548
|
-
.paragraph(
|
|
589
|
+
rich_message: new HTML()
|
|
590
|
+
.heading(1, 'Result from Inline Query')
|
|
591
|
+
.paragraph('Sent via ' + HTML.url('https://t.me/kafk6', '@kafka') + '.')
|
|
549
592
|
.build(),
|
|
550
593
|
}
|
|
551
594
|
|
|
@@ -562,47 +605,35 @@ bot.on('inline_query', async (ctx) => {
|
|
|
562
605
|
|
|
563
606
|
---
|
|
564
607
|
|
|
565
|
-
### TypeScript
|
|
608
|
+
### TypeScript
|
|
566
609
|
|
|
567
|
-
All types are exported
|
|
610
|
+
All types are exported under the `RichMessage` namespace:
|
|
568
611
|
|
|
569
612
|
```ts
|
|
570
613
|
import { RichMessage } from '@icanseeuanywhere/telekaf'
|
|
614
|
+
import type { ExtraSendRichMessage, ExtraSendRichMessageDraft } from '@icanseeuanywhere/telekaf/types'
|
|
571
615
|
|
|
572
|
-
//
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
const msg: RichMessage.InputRichMessage = { blocks: [block] }
|
|
576
|
-
const received: RichMessage.RichMessage = ctx.message.rich_message // on incoming messages
|
|
577
|
-
|
|
578
|
-
// All RichText types
|
|
579
|
-
type RichText = RichMessage.RichText
|
|
580
|
-
type RichTextBold = RichMessage.RichTextBold
|
|
581
|
-
type RichTextItalic = RichMessage.RichTextItalic
|
|
582
|
-
// ... etc
|
|
616
|
+
// Builder types
|
|
617
|
+
const builder: RichMessage.RichHTMLBuilder = new RichMessage.RichHTMLBuilder()
|
|
618
|
+
const mdBuilder: RichMessage.RichMarkdownBuilder = new RichMessage.RichMarkdownBuilder()
|
|
583
619
|
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
// ... etc
|
|
589
|
-
|
|
590
|
-
// Extra types for sendRichMessage
|
|
591
|
-
import type { ExtraSendRichMessage } from '@icanseeuanywhere/telekaf/types'
|
|
592
|
-
```
|
|
620
|
+
// Message types
|
|
621
|
+
const input: RichMessage.InputRichMessage = { html: '<p>hello</p>' }
|
|
622
|
+
const content: RichMessage.InputRichMessageContent = { rich_message: input }
|
|
623
|
+
const received: RichMessage.RichMessage = ctx.message.rich_message
|
|
593
624
|
|
|
594
|
-
|
|
625
|
+
// Extra types
|
|
626
|
+
const extra: ExtraSendRichMessage = {
|
|
627
|
+
disable_notification: true,
|
|
628
|
+
reply_markup: { inline_keyboard: [] },
|
|
629
|
+
}
|
|
595
630
|
|
|
596
|
-
|
|
597
|
-
import
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
InputRichMessageContent,
|
|
603
|
-
RichMessageBuilder,
|
|
604
|
-
RT,
|
|
605
|
-
} from '@icanseeuanywhere/telekaf/core/types/rich-message'
|
|
631
|
+
// Custom context with rich message
|
|
632
|
+
import { Context, Telegraf } from '@icanseeuanywhere/telekaf'
|
|
633
|
+
interface MyCtx extends Context {
|
|
634
|
+
session?: { lastDraftId: number }
|
|
635
|
+
}
|
|
636
|
+
const bot = new Telegraf<MyCtx>(process.env.BOT_TOKEN)
|
|
606
637
|
```
|
|
607
638
|
|
|
608
639
|
---
|
|
@@ -795,7 +826,7 @@ process.once('SIGTERM', () => bot.stop('SIGTERM'))
|
|
|
795
826
|
### Webhooks
|
|
796
827
|
|
|
797
828
|
```TS
|
|
798
|
-
import { Telegraf } from "
|
|
829
|
+
import { Telegraf } from "@icanseeuanywhere/telekaf";
|
|
799
830
|
import { message } from '@icanseeuanywhere/telekaf/filters';
|
|
800
831
|
|
|
801
832
|
const bot = new Telegraf(token);
|