mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI-examples.git
synced 2026-09-23 00:44:41 +00:00
update examples for ktgbotapi 37.0.0
This commit is contained in:
@@ -3,11 +3,13 @@
|
||||
RichMessagesBot is a long-polling showcase of Telegram rich messages. It sends
|
||||
rich content from HTML, Markdown, and the typed `InputRichMessageBlocks` DSL;
|
||||
streams drafts; edits rich text; handles incoming rich messages; and supplies
|
||||
rich content from inline and guest queries.
|
||||
rich content from inline and guest queries. Bot API 10.3 coverage includes rich
|
||||
buttons, compact tables, expandable quotations, document blocks, document
|
||||
references, direct document uploads, and stoppable drafts.
|
||||
|
||||
## Commands
|
||||
|
||||
The bot installs seven commands in Telegram's default command menu. Three
|
||||
The bot installs eight commands in Telegram's default command menu. Three
|
||||
additional handlers can be invoked by typing their commands manually.
|
||||
|
||||
| Command | In menu | Demonstration |
|
||||
@@ -18,21 +20,24 @@ additional handlers can be invoked by typing their commands manually.
|
||||
| `/rich_markdown_blocks` | No | Sends and logs the full fixture as a typed block tree, including first-class media blocks, captions, collages, and slideshows. |
|
||||
| `/rich_markdown_medialess_blocks` | No | Sends and logs the same typed block tree without its media section. |
|
||||
| `/rich_blocks` | Yes | Sends a smaller, directly constructed block tree with headings, formatted paragraphs, ordered and unordered checkbox lists, a divider, preformatted Kotlin, and a quotation. |
|
||||
| `/rich_draft` | Yes | Streams three Markdown revisions under draft ID `1`, one second apart, then sends a normal final rich message. |
|
||||
| `/rich_blocks_draft` | Yes | Streams two draft-only `thinking()` blocks under draft ID `2`, one second apart, then sends a normal typed-block answer. |
|
||||
| `/rich_10_3` | Yes | Sends an inline `RichTextButton`, three aligned `InputRichBlockButtons` rows with URL, callback, inline-query, copy-text, and disabled actions, a compact table, an expandable quotation, and a document block. |
|
||||
| `/rich_draft` | Yes | In a private chat, streams three Markdown revisions with `canStop` and `keepOnStop`, one second apart, then sends a normal final rich message unless generation is stopped. |
|
||||
| `/rich_blocks_draft` | Yes | In a private chat, streams two draft-only `thinking()` blocks with `canStop` and `keepOnStop`, then sends a normal typed-block answer unless generation is stopped. |
|
||||
| `/rich_edit` | Yes | Sends a Markdown rich message, waits two seconds, and replaces its rich content with `EditChatMessageRichText`. |
|
||||
| `/wait_rich` | Yes | Prompts for a rich message, waits for the next matching content, and reports its block count. |
|
||||
|
||||
The two draft examples finalize by sending a new normal rich message; they do not
|
||||
turn the draft itself into the final message. They use distinct fixed IDs (`1`
|
||||
and `2`); concurrent runs of the same command in one chat reuse that command's
|
||||
ID.
|
||||
The two draft examples allocate a unique draft ID and subscribe to
|
||||
`waitMessageGenerationStopped` before sending the first revision. They finalize
|
||||
by sending a new normal rich message only if no matching stop update arrives;
|
||||
when stopped, they leave the retained draft untouched and log the event.
|
||||
|
||||
## Other triggers
|
||||
|
||||
| Trigger | Behavior |
|
||||
| --- | --- |
|
||||
| Any photo | Reuses the received Telegram file ID without downloading it. The bot first sends HTML whose `tg://photo?id=userphoto` reference is resolved by `InputRichMessageMedia`, then sends the same photo as a typed `photo()` block. |
|
||||
| Any document | Reuses the received file ID through a `tg://document?id=userdocument` HTML reference, then downloads the file and sends it again as a multipart upload nested in a typed `document()` block. Large documents are therefore held in memory. |
|
||||
| A `rich_10_3_callback` rich button | Answers the callback with a notification confirming that the rich-message button was received. |
|
||||
| Any incoming rich message | Logs right-to-left state and every parsed block, replies with the block count, and resends the rich message with `createResend`. The `onlyRichMessageContentMessages` flow also logs its block count. |
|
||||
| Any inline query | Returns uncached HTML and Markdown articles whose selected messages use `InputRichMessageContent`. |
|
||||
| A text guest request containing `/rich_guest` | Returns one inline article containing the full Markdown fixture. This is a substring check, not a registered bot command. Non-text guest requests and text without that exact case-sensitive substring are ignored by this handler. |
|
||||
@@ -50,9 +55,9 @@ Telegram file ID and assigning an alias for a `tg://photo?id=...` reference.
|
||||
|
||||
The source also shows the two library shapes used for rich media: an
|
||||
`InputRichMessageMedia` mapping for markup references and first-class photo,
|
||||
video, audio, voice-note, animation, collage, and slideshow blocks. The library
|
||||
can collect multipart files nested in a rich-message tree as `attach://` uploads,
|
||||
although this example's running handlers use URLs or an existing file ID.
|
||||
video, audio, voice-note, animation, document, collage, and slideshow blocks.
|
||||
The document trigger demonstrates the library collecting a multipart file nested
|
||||
in a rich-message tree as an `attach://` upload.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
@@ -60,7 +65,7 @@ although this example's running handlers use URLs or an existing file ID.
|
||||
control.
|
||||
2. Start a private chat with the bot, or add it to a chat and allow it to send
|
||||
messages and media.
|
||||
3. To test ordinary photo and rich-message triggers in a group, ensure Telegram
|
||||
3. To test ordinary photo, document, and rich-message triggers in a group, ensure Telegram
|
||||
delivers non-command messages to the bot, for example by disabling Group
|
||||
Privacy Mode or making the bot an administrator.
|
||||
4. Enable Inline Mode in BotFather to exercise the inline-query results.
|
||||
|
||||
@@ -5,14 +5,18 @@ import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
|
||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage
|
||||
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessageDraft
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageGenerationStopped
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitRichMessage
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onDocument
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGuestRequestMessage
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onRichMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.baseSentMessageUpdateOrNull
|
||||
@@ -21,16 +25,19 @@ import dev.inmo.tgbotapi.extensions.utils.onlyRichMessageContentMessages
|
||||
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||
import dev.inmo.tgbotapi.requests.edit.text.EditChatMessageRichText
|
||||
import dev.inmo.tgbotapi.requests.abstracts.InputFile
|
||||
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||
import dev.inmo.tgbotapi.types.BotCommand
|
||||
import dev.inmo.tgbotapi.types.CustomEmojiId
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputRichMessageContent
|
||||
import dev.inmo.tgbotapi.types.InlineQueryId
|
||||
import dev.inmo.tgbotapi.types.TelegramDate
|
||||
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAnimation
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
|
||||
@@ -40,19 +47,31 @@ import dev.inmo.tgbotapi.types.rich.InputRichMessageHTML
|
||||
import dev.inmo.tgbotapi.types.rich.InputRichMessageMarkdown
|
||||
import dev.inmo.tgbotapi.types.rich.InputRichMessageMedia
|
||||
import dev.inmo.tgbotapi.types.rich.RichBlockCaption
|
||||
import dev.inmo.tgbotapi.types.rich.RichBlockButtonAlignment
|
||||
import dev.inmo.tgbotapi.types.rich.RichBlockTableCellAlign
|
||||
import dev.inmo.tgbotapi.types.rich.RichBlockTableCellVAlign
|
||||
import dev.inmo.tgbotapi.types.rich.RichMessageButton
|
||||
import dev.inmo.tgbotapi.types.rich.RichMessageButtonStyle
|
||||
import dev.inmo.tgbotapi.types.rich.RichTextPlain
|
||||
import dev.inmo.tgbotapi.types.rich.buildRichText
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CopyTextButtonData
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.SwitchInlineQueryChosenChat
|
||||
import dev.inmo.tgbotapi.types.toChatId
|
||||
import dev.inmo.tgbotapi.utils.DraftIdAllocator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
private val richDraftIds = DraftIdAllocator()
|
||||
|
||||
/**
|
||||
* Runs a long-polling showcase of the rich-message APIs introduced in Telegram Bot API 10.1 and 10.2.
|
||||
* Runs a long-polling showcase of the rich-message APIs introduced in Telegram Bot API 10.1 through 10.3.
|
||||
*
|
||||
* Outgoing [dev.inmo.tgbotapi.types.rich.InputRichMessage] values use one of three representations:
|
||||
* [InputRichMessageHTML], [InputRichMessageMarkdown], or a typed [InputRichMessageBlocks] tree of
|
||||
@@ -60,7 +79,9 @@ import kotlinx.coroutines.flow.mapNotNull
|
||||
* [sendRichMessageDraft] revisions sharing a draft ID (including draft-only `thinking()` blocks), and edits
|
||||
* through [EditChatMessageRichText]. Media is shown both as [InputRichMessageMedia] references such as
|
||||
* `tg://photo?id=...` and as typed blocks; [dev.inmo.tgbotapi.requests.send.SendRichMessage] also turns
|
||||
* multipart files inside an input tree into `attach://` uploads.
|
||||
* multipart files inside an input tree into `attach://` uploads. Bot API 10.3 button rows, inline rich-text buttons,
|
||||
* compact tables, expandable quotations and document blocks are demonstrated by `/rich_10_3` and the document
|
||||
* trigger.
|
||||
*
|
||||
* Incoming [dev.inmo.tgbotapi.types.message.content.RichMessageContent] and user-selected content covers
|
||||
* [onRichMessage], [waitRichMessage], [onlyRichMessageContentMessages], photo reuse, and
|
||||
@@ -692,20 +713,40 @@ suspend fun main(vararg args: String) {
|
||||
|
||||
// sendRichMessageDraft: stream partial rich messages sharing one draftId, then finalize
|
||||
// with a full sendRichMessage. Emulates streaming of an AI-generated reply.
|
||||
onCommand("rich_draft") {
|
||||
val chatId = it.chat.id.toChatId()
|
||||
val draftId = 1L
|
||||
onCommand("rich_draft", initialFilter = { it.chat is PrivateChat }) { origin ->
|
||||
val chatId = origin.chat.id.toChatId()
|
||||
val draftId = richDraftIds.allocate()
|
||||
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
|
||||
waitMessageGenerationStopped()
|
||||
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
|
||||
.first()
|
||||
}
|
||||
val parts = listOf(
|
||||
"Thinking",
|
||||
"Thinking about *rich* messages",
|
||||
"Thinking about *rich* messages and how to _stream_ them"
|
||||
)
|
||||
parts.forEach { part ->
|
||||
sendRichMessageDraft(chatId, draftId, InputRichMessageMarkdown(part))
|
||||
delay(1000)
|
||||
try {
|
||||
parts.forEach { part ->
|
||||
sendRichMessageDraft(
|
||||
chatId,
|
||||
draftId.long,
|
||||
InputRichMessageMarkdown(part),
|
||||
canStop = true,
|
||||
keepOnStop = true,
|
||||
)
|
||||
val stopped = withTimeoutOrNull(1000L) { stoppedUpdate.await() }
|
||||
if (stopped != null) {
|
||||
println("Stopped rich draft ${stopped.draftId.long} in ${stopped.chat.id}")
|
||||
return@onCommand
|
||||
}
|
||||
}
|
||||
// Finalize only if the user did not stop generation; a normal message removes a retained draft.
|
||||
sendRichMessage(chatId, InputRichMessageMarkdown("Done! Here is the *final* rich message."))
|
||||
} finally {
|
||||
stoppedUpdate.cancel()
|
||||
richDraftIds.free(draftId)
|
||||
}
|
||||
// finalize the streamed draft with the real message
|
||||
sendRichMessage(chatId, InputRichMessageMarkdown("Done! Here is the *final* rich message."))
|
||||
}
|
||||
|
||||
// EditChatMessageRichText: send a rich message, then edit it with new rich content
|
||||
@@ -772,29 +813,148 @@ suspend fun main(vararg args: String) {
|
||||
)
|
||||
}
|
||||
|
||||
// sendRichMessageDraft with blocks: the thinking() block is only valid inside a draft and is used
|
||||
// to stream a model's reasoning before the finalized rich message is sent via sendRichMessage.
|
||||
onCommand("rich_blocks_draft") {
|
||||
val chatId = it.chat.id.toChatId()
|
||||
val draftId = 2L
|
||||
listOf("Analyzing your request", "Composing a structured answer").forEach { step ->
|
||||
sendRichMessageDraft(
|
||||
chatId,
|
||||
draftId,
|
||||
InputRichMessageBlocks { thinking(step) }
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
// finalize the streamed draft with the real (non-thinking) blocks
|
||||
// === Bot API 10.3 additions: buttons, compact tables, expandable quotes and documents ===
|
||||
onCommand("rich_10_3") {
|
||||
val callbackButton = RichMessageButton.CallbackData(
|
||||
RichTextPlain("Callback"),
|
||||
callbackData = "rich_10_3_callback",
|
||||
style = RichMessageButtonStyle.Link,
|
||||
)
|
||||
sendRichMessage(
|
||||
chatId,
|
||||
it.chat.id,
|
||||
InputRichMessageBlocks {
|
||||
heading("Answer", level = 2)
|
||||
paragraph("Here is the finalized, structured reply.")
|
||||
h1("Bot API 10.3 rich blocks")
|
||||
paragraph {
|
||||
plain("A RichTextButton can live inline with text: ")
|
||||
button(
|
||||
RichMessageButton.CopyText(
|
||||
RichTextPlain("copy 42"),
|
||||
CopyTextButtonData("42"),
|
||||
style = RichMessageButtonStyle.Primary,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
expandableBlockQuotation(credit = RichTextPlain("Expandable quotation credit")) {
|
||||
plain("This quotation starts collapsed and can be expanded by the reader. ")
|
||||
dateTime("Bot API 10.3", TelegramDate(1787518800L), "d MMMM yyyy")
|
||||
}
|
||||
|
||||
table(
|
||||
isBordered = true,
|
||||
isStriped = true,
|
||||
isCompact = true,
|
||||
caption = RichTextPlain("A compact table"),
|
||||
) {
|
||||
row {
|
||||
headerCell(RichBlockTableCellAlign.Left, RichBlockTableCellVAlign.Middle) { plain("Feature") }
|
||||
headerCell(RichBlockTableCellAlign.Right, RichBlockTableCellVAlign.Middle) { plain("Version") }
|
||||
}
|
||||
row {
|
||||
cell(RichBlockTableCellAlign.Left, RichBlockTableCellVAlign.Middle) { plain("Compact tables") }
|
||||
cell(RichBlockTableCellAlign.Right, RichBlockTableCellVAlign.Middle) { plain("10.3") }
|
||||
}
|
||||
}
|
||||
|
||||
buttons(
|
||||
listOf(
|
||||
RichMessageButton.Url(
|
||||
RichTextPlain("Telegram"),
|
||||
"https://telegram.org",
|
||||
RichMessageButtonStyle.Primary,
|
||||
),
|
||||
callbackButton,
|
||||
),
|
||||
align = RichBlockButtonAlignment.Left,
|
||||
)
|
||||
buttons(
|
||||
listOf(
|
||||
RichMessageButton.SwitchInlineQuery(
|
||||
RichTextPlain("Choose chat"),
|
||||
"rich 10.3",
|
||||
RichMessageButtonStyle.Success,
|
||||
),
|
||||
RichMessageButton.SwitchInlineQueryCurrentChat(
|
||||
RichTextPlain("Current chat"),
|
||||
"rich 10.3",
|
||||
),
|
||||
RichMessageButton.SwitchInlineQueryChosenChat(
|
||||
RichTextPlain("Groups only"),
|
||||
SwitchInlineQueryChosenChat(
|
||||
query = "rich 10.3",
|
||||
allowGroups = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
align = RichBlockButtonAlignment.Center,
|
||||
)
|
||||
buttons(
|
||||
listOf(
|
||||
RichMessageButton.CopyText(
|
||||
RichTextPlain("Copy value"),
|
||||
CopyTextButtonData("Bot API 10.3"),
|
||||
),
|
||||
RichMessageButton.Disabled(
|
||||
RichTextPlain("Disabled"),
|
||||
RichMessageButtonStyle.Danger,
|
||||
),
|
||||
),
|
||||
align = RichBlockButtonAlignment.Right,
|
||||
)
|
||||
|
||||
document(
|
||||
TelegramMediaDocument(
|
||||
InputFile.fromUrl("https://telegram.org/example/document.pdf")
|
||||
),
|
||||
RichBlockCaption(RichTextPlain("A general-file document block")),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
onMessageDataCallbackQuery(Regex("rich_10_3_callback")) { query ->
|
||||
answer(query, "Rich-message callback received")
|
||||
}
|
||||
|
||||
// sendRichMessageDraft with blocks: the thinking() block is only valid inside a draft and is used
|
||||
// to stream a model's reasoning before the finalized rich message is sent via sendRichMessage.
|
||||
onCommand("rich_blocks_draft", initialFilter = { it.chat is PrivateChat }) { origin ->
|
||||
val chatId = origin.chat.id.toChatId()
|
||||
val draftId = richDraftIds.allocate()
|
||||
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
|
||||
waitMessageGenerationStopped()
|
||||
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
|
||||
.first()
|
||||
}
|
||||
try {
|
||||
listOf("Analyzing your request", "Composing a structured answer").forEach { step ->
|
||||
sendRichMessageDraft(
|
||||
chatId,
|
||||
draftId.long,
|
||||
InputRichMessageBlocks { thinking(step) },
|
||||
canStop = true,
|
||||
keepOnStop = true,
|
||||
)
|
||||
val stopped = withTimeoutOrNull(1000L) { stoppedUpdate.await() }
|
||||
if (stopped != null) {
|
||||
println("Stopped rich block draft ${stopped.draftId.long} in ${stopped.chat.id}")
|
||||
return@onCommand
|
||||
}
|
||||
}
|
||||
// Finalize only if the user did not stop generation; a normal message removes a retained draft.
|
||||
sendRichMessage(
|
||||
chatId,
|
||||
InputRichMessageBlocks {
|
||||
heading("Answer", level = 2)
|
||||
paragraph("Here is the finalized, structured reply.")
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
stoppedUpdate.cancel()
|
||||
richDraftIds.free(draftId)
|
||||
}
|
||||
}
|
||||
|
||||
// Rich message media: send me a photo and it gets embedded into a rich message two ways.
|
||||
onPhoto { message ->
|
||||
// reuse the received file by its fileId (no upload). To upload a brand-new file instead,
|
||||
@@ -828,6 +988,40 @@ suspend fun main(vararg args: String) {
|
||||
)
|
||||
}
|
||||
|
||||
// A received document demonstrates both tg://document?id= references and a direct multipart upload.
|
||||
onDocument { message ->
|
||||
val reusedDocument = TelegramMediaDocument(message.content.media.fileId)
|
||||
sendRichMessage(
|
||||
message.chat.id,
|
||||
InputRichMessageHTML(
|
||||
"""
|
||||
<h2>Your document, referenced from HTML</h2>
|
||||
<tg-document src="tg://document?id=userdocument"></tg-document>
|
||||
""".trimIndent(),
|
||||
media = listOf(
|
||||
InputRichMessageMedia(id = "userdocument", media = reusedDocument)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val uploadedDocument = TelegramMediaDocument(
|
||||
downloadFile(message.content).asMultipartFile(
|
||||
message.content.media.fileName ?: "document.bin"
|
||||
)
|
||||
)
|
||||
sendRichMessage(
|
||||
message.chat.id,
|
||||
InputRichMessageBlocks {
|
||||
h2("Your document, uploaded as a new file")
|
||||
paragraph("SendRichMessage collects the MultipartFile nested in this document block.")
|
||||
document(
|
||||
uploadedDocument,
|
||||
RichBlockCaption(RichTextPlain("Direct attach:// document upload")),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// waitRichMessage expectation: wait for the user to send a rich message
|
||||
onCommand("wait_rich") {
|
||||
reply(it, "Send me a rich message now")
|
||||
@@ -913,6 +1107,7 @@ suspend fun main(vararg args: String) {
|
||||
BotCommand("rich_html", "Send a rich message described with HTML"),
|
||||
BotCommand("rich_markdown", "Send a rich message described with Markdown"),
|
||||
BotCommand("rich_blocks", "Send a rich message built from the InputRichBlocks DSL"),
|
||||
BotCommand("rich_10_3", "Show Bot API 10.3 rich blocks and buttons"),
|
||||
BotCommand("rich_draft", "Stream a rich message draft, then finalize it"),
|
||||
BotCommand("rich_blocks_draft", "Stream a blocks draft with thinking(), then finalize it"),
|
||||
BotCommand("rich_edit", "Send a rich message and edit it with new rich content"),
|
||||
|
||||
Reference in New Issue
Block a user