import dev.inmo.kslog.common.KSLog import dev.inmo.kslog.common.LogLevel import dev.inmo.kslog.common.defaultMessageFormatter 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 import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat 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.DateTimeEntityFormatting 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 import dev.inmo.tgbotapi.types.rich.InputRichMessage import dev.inmo.tgbotapi.types.rich.InputRichMessageBlocks 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.directMessageThreadIdOrMessageThreadIdAsDirectMessageThreadId 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 through 10.3. * * Outgoing [dev.inmo.tgbotapi.types.rich.InputRichMessage] values use one of three representations: * [InputRichMessageHTML], [InputRichMessageMarkdown], or a typed [InputRichMessageBlocks] tree of * [dev.inmo.tgbotapi.types.rich.InputRichBlock] values. The handlers demonstrate [sendRichMessage], * [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. 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 * [InputRichMessageContent] in inline and guest-query results. Parsed content is exposed as a * [dev.inmo.tgbotapi.types.rich.RichMessage] containing [dev.inmo.tgbotapi.types.rich.RichBlock]s. * * @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags. The token * must be present; unrecognized later arguments are ignored. */ suspend fun main(vararg args: String) { val botToken = args.first() val isDebug = args.any { it == "debug" } val isTestServer = args.any { it == "testServer" } if (isDebug) { setDefaultKSLog( KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? -> println(defaultMessageFormatter(level, tag, message, throwable)) } ) } val testMarkdownText = """ **bold text** __bold text__ *italic text* _italic text_ ~~strikethrough text~~ `inline fixed-width code` ==marked text== ||spoiler|| [inline URL](https://t.me/) [inline e-mail](mailto:user@example.com) [inline phone number](tel:+123456789) [inline mention of a user](tg://user?id=123456789) ![👍](tg://emoji?id=5368324170671202286) ![22:45 tomorrow](tg://time?unix=1647531900&format=wDT) ${'$'}x^2 + y^2$ \#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username all the text above was on the same line # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ###### Heading 6 Paragraph text ```python print('pre-formatted fixed-width code block written in the Python programming language') ``` --- - unordered list item * unordered list item + unordered list item 1. ordered list item 2. ordered list item - [ ] task list item - [x] completed task list item >Block quotation started > >Block quotation continued on the next line >Block quotation continued on the same line > >The last line of the block quotation ![](https://telegram.org/example/photo.jpg) ![](https://telegram.org/example/video.mp4) ![](https://telegram.org/example/audio.mp3) ![](https://telegram.org/example/audio.ogg) ![](https://telegram.org/example/animation.gif) ![](https://telegram.org/example/photo.jpg "Photo caption") ![](https://telegram.org/example/video.mp4 "Video caption") ![](https://telegram.org/example/audio.mp3 "Audio caption") ![](https://telegram.org/example/audio.ogg "Voice note caption") ![](https://telegram.org/example/animation.gif "Animation caption") | Header 1 | Header 2 | |:---------|:--------:| | left | center | Text with a reference[^id1] and another one[^id2]. [^id1]: Definition of the first footnote. [^id2]: Definition of the second footnote. $${'$'}E = mc^2$$ ```math E = mc^2 ``` ## Example Nested Syntax Report for _Q1_ Intro with underlined text, ==marked text==, and ${'$'}x^2 + y^2$. **Bold _italic underlined italic bold italic_ bold** In inline tags, nested **markdown** is parsed >Quote with **bold text, ~~strikethrough, and spoiler~~**, plus [a link](https://t.me/). - List item with `code`, superscript, subscript, and a footnote[^note] - Another item with **bold spoiler code** - Another item with ~~strikethrough and inserted text~~ | Metric | Value | |:-------|------:| | Speed | **42** ms | | Status | ready | [^note]: Footnote with _italic text_ and HTML underline. --- # Details blocks can contain Markdown content:
Summary with **bold text** ### Details heading - List item with _italic text_ - List item with spoiler
# Collages and slideshows can contain Markdown media blocks: ![](https://telegram.org/example/photo.jpg) ![](https://telegram.org/example/video.mp4) ![](https://telegram.org/example/photo.jpg) ![](https://telegram.org/example/video.mp4) """.trimIndent() val testMarkdownMediaLessText = """ **bold text** __bold text__ *italic text* _italic text_ ~~strikethrough text~~ `inline fixed-width code` ==marked text== ||spoiler|| [inline URL](https://t.me/) [inline e-mail](mailto:user@example.com) [inline phone number](tel:+123456789) [inline mention of a user](tg://user?id=123456789) ![👍](tg://emoji?id=5368324170671202286) ![22:45 tomorrow](tg://time?unix=1647531900&format=wDT) ${'$'}x^2 + y^2$ \#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username all the text above was on the same line # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ###### Heading 6 Paragraph text ```python print('pre-formatted fixed-width code block written in the Python programming language') ``` --- - unordered list item * unordered list item + unordered list item 1. ordered list item 2. ordered list item - [ ] task list item - [x] completed task list item >Block quotation started > >Block quotation continued on the next line >Block quotation continued on the same line > >The last line of the block quotation | Header 1 | Header 2 | |:---------|:--------:| | left | center | Text with a reference[^id1] and another one[^id2]. [^id1]: Definition of the first footnote. [^id2]: Definition of the second footnote. $${'$'}E = mc^2$$ ```math E = mc^2 ``` ## Example Nested Syntax Report for _Q1_ Intro with underlined text, ==marked text==, and ${'$'}x^2 + y^2$. **Bold _italic underlined italic bold italic_ bold** In inline tags, nested **markdown** is parsed >Quote with **bold text, ~~strikethrough, and spoiler~~**, plus [a link](https://t.me/). - List item with `code`, superscript, subscript, and a footnote[^note] - Another item with **bold spoiler code** - Another item with ~~strikethrough and inserted text~~ | Metric | Value | |:-------|------:| | Speed | **42** ms | | Status | ready | [^note]: Footnote with _italic text_ and HTML underline. --- # Details blocks can contain Markdown content:
Summary with **bold text** ### Details heading - List item with _italic text_ - List item with spoiler
""".trimIndent() val testMarkdownMediaLessInputRichMessageBlocks = InputRichMessageBlocks { paragraph { bold("bold text") plain("\n") bold("bold text") plain("\n") italic("italic text") plain("\n") italic("italic text") plain("\n") strikethrough("strikethrough text") plain("\n") code("inline fixed-width code") plain("\n") marked("marked text") plain("\n") spoiler("spoiler") } paragraph { url("inline URL", "https://t.me/") plain("\n") email("inline e-mail", "user@example.com") plain("\n") phone("inline phone number", "+123456789") plain("\n") url("inline mention of a user", "tg://user?id=123456789") plain("\n") customEmoji(CustomEmojiId("5368324170671202286"), "👍") plain("\n") dateTime( text = "22:45 tomorrow", unixTime = TelegramDate(1647531900L), dateTimeFormat = DateTimeEntityFormatting( useWeek = true, useDateLong = false, useTimeLong = false ) ) plain("\n") mathematicalExpression("x^2 + y^2") plain("\n#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username\n") plain("all the text above was on the same line") } h1("Heading 1") h2("Heading 2") h3("Heading 3") h4("Heading 4") h5("Heading 5") h6("Heading 6") paragraph("Paragraph text") preformatted( " print('pre-formatted fixed-width code block written in the Python programming language')", language = "python" ) divider() unorderedList { item("unordered list item") item("unordered list item") item("unordered list item") } orderedList { item(1) { paragraph("ordered list item") } item(2) { paragraph("ordered list item") } } unorderedList { item(hasCheckbox = true, isChecked = false) { paragraph("task list item") } item(hasCheckbox = true, isChecked = true) { paragraph("completed task list item") } } blockQuotation { paragraph("Block quotation started\nBlock quotation continued on the next line\nBlock quotation continued on the same line\nThe last line of the block quotation") } table { row { headerCell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Top) { plain("Header 1") } headerCell(align = RichBlockTableCellAlign.Center, valign = RichBlockTableCellVAlign.Middle) { plain("Header 2") } headerCell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Bottom) { plain("Header 2") } } row { cell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Top) { plain("left") } cell(align = RichBlockTableCellAlign.Center, valign = RichBlockTableCellVAlign.Middle) { plain("center") } } } paragraph { plain("Text with a reference") referenceLink("id1", "id1") plain(" and another one") referenceLink("id2", "id2") plain(".") } paragraph { reference("Definition of the first footnote.", "id1") } paragraph { reference("Definition of the second footnote.", "id2") } mathematicalExpression("E = mc^2") preformatted("E = mc^2", language = "math") h2 { plain("Example Nested Syntax Report for ") italic("Q1") } paragraph { plain("Intro with ") underline("underlined text") plain(", ") marked("marked text") plain(", and ") mathematicalExpression("x^2 + y^2") plain(".") } paragraph { bold { plain("Bold ") italic { plain("italic ") underline("underlined italic bold") plain(" italic") } plain(" bold") } } paragraph { underline { plain("In inline tags, nested ") bold("markdown") plain(" is parsed") } } blockQuotation { paragraph { plain("Quote with ") bold { plain("bold text, ") strikethrough { plain("strikethrough, and ") spoiler("spoiler") } } plain(", plus ") url("a link", "https://t.me/") plain(".") } } unorderedList { item { paragraph { plain("List item with ") code("code") plain(", ") superscript("superscript") plain(", ") subscript("subscript") plain(", and a footnote") referenceLink("note", "note") } } item { paragraph { plain("Another item with ") bold { spoiler { code("spoiler code") } } } } item { paragraph { plain("Another item with ") strikethrough { plain("strikethrough and ") underline("inserted text") } } } } table { row { headerCell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Middle) { plain("Metric") } headerCell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Middle) { plain("Value") } } row { cell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Middle) { plain("Speed") } cell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Middle) { bold("42") plain(" ") superscript("ms") } } row { cell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Middle) { plain("Status") } cell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Middle) { spoiler("ready") } } } paragraph { reference("note") { plain("Footnote with ") italic("italic text") plain(" and ") underline("HTML underline") plain(".") } } divider() h1("Details blocks can contain Markdown content:") details( summary = buildRichText { plain("Summary with ") bold("bold text") }, isOpen = true ) { h3("Details heading") unorderedList { item { paragraph { plain("List item with "); italic("italic text") } } item { paragraph { plain("List item with "); spoiler("spoiler") } } } } } val testMarkdownInputRichMessageBlocks = InputRichMessageBlocks { testMarkdownMediaLessInputRichMessageBlocks.blocks.orEmpty().forEach(::add) val photo = TelegramMediaPhoto(InputFile.fromUrl("https://telegram.org/example/photo.jpg")) val video = TelegramMediaVideo(InputFile.fromUrl("https://telegram.org/example/video.mp4")) val audio = TelegramMediaAudio(InputFile.fromUrl("https://telegram.org/example/audio.mp3")) val voiceNote = TelegramMediaVoiceNote(InputFile.fromUrl("https://telegram.org/example/audio.ogg")) val animation = TelegramMediaAnimation(InputFile.fromUrl("https://telegram.org/example/animation.gif")) photo(photo) video(video) audio(audio) voiceNote(voiceNote) animation(animation) photo(photo, RichBlockCaption(RichTextPlain("Photo caption"))) video(video, RichBlockCaption(RichTextPlain("Video caption"))) audio(audio, RichBlockCaption(RichTextPlain("Audio caption"))) voiceNote(voiceNote, RichBlockCaption(RichTextPlain("Voice note caption"))) animation(animation, RichBlockCaption(RichTextPlain("Animation caption"))) collage { photo(photo) video(video) } collage(RichBlockCaption(RichTextPlain("Collage caption"))) { video(video) photo(photo) } slideshow { photo(photo) video(video) } slideshow(RichBlockCaption(RichTextPlain("Slideshow caption"))) { video(video) photo(photo) } } telegramBotWithBehaviourAndLongPolling( botToken, CoroutineScope(Dispatchers.IO), testServer = isTestServer ) { // sendRichMessage with HTML-formatted content onCommand("rich_html") { sendRichMessage( it.chat.id, // InputRichMessageHTML factory — content described using HTML formatting InputRichMessageHTML( """ bold text, bold text italic text, italic text underlined text, underlined text strikethrough text, strikethrough text, strikethrough text inline fixed-width code marked text subscript text superscript text spoiler Reference inline URL inline e-mail inline phone number inline mention of a user in-document link Referenced text 👍 👍 22:45 tomorrow x^2 + y^2 #hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username all the text above was on the same line

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

Paragraph text

pre-formatted fixed-width code block
  print('pre-formatted fixed-width code block written in the Python programming language')

  1. ordered list item
  1. ordered list item
  1. ordered list item with explicit number
Block quotation started
Block quotation continued
The last line of the block quotationThe Author
Photo captionPhoto credit
Video caption
Audio caption
Voice note caption
Animation caption
Map caption
Header 1Header 2
Value 1Value 2
Table caption
ValueValue2Value3
Value4Value5Value6
Value7
TitleContent
TitleContent
E = mc^2 """.trimIndent() ) ) } // sendRichMessage with Markdown-formatted content onCommand("rich_markdown") { val sent = sendRichMessage( it.chat.id, InputRichMessageMarkdown( testMarkdownText ) ) println(sent) } // sendRichMessage with Markdown-formatted content onCommand("rich_markdown_medialess") { val sent = sendRichMessage( it.chat.id, InputRichMessageMarkdown( testMarkdownMediaLessText ) ) println(sent) } // sendRichMessage with Markdown-formatted content onCommand("rich_markdown_blocks") { val sent = sendRichMessage( it.chat.id, testMarkdownInputRichMessageBlocks ) println(sent) } // sendRichMessage with Markdown-formatted content onCommand("rich_markdown_medialess_blocks") { val sent = sendRichMessage( it.chat.id, testMarkdownMediaLessInputRichMessageBlocks ) println(sent) } // sendRichMessageDraft: stream partial rich messages sharing one draftId, then finalize // with a full sendRichMessage. Emulates streaming of an AI-generated reply. onCommand("rich_draft", initialFilter = { it.chat is PrivateChat }) { origin -> val draftId = richDraftIds.allocate() val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) { waitMessageGenerationStopped() .filter { it.sameChat(origin) && it.draftId == draftId } .first() } val parts = listOf( "Thinking", "Thinking about *rich* messages", "Thinking about *rich* messages and how to _stream_ them" ) try { parts.forEach { part -> sendRichMessageDraft( origin.chat.id, draftId.long, InputRichMessageMarkdown(part), canStop = true, keepOnStop = true, ) val stopped = withTimeoutOrNull(5000L) { stoppedUpdate.await() } if (stopped != null) { reply(origin, "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(origin.chat.id, InputRichMessageMarkdown("Done! Here is the *final* rich message.")) } finally { stoppedUpdate.cancel() richDraftIds.free(draftId) } } // EditChatMessageRichText: send a rich message, then edit it with new rich content onCommand("rich_edit") { val sent = sendRichMessage(it.chat.id, InputRichMessageMarkdown("*Original* rich message")) delay(2000) execute( EditChatMessageRichText( chatId = sent.chat.id, messageId = sent.messageId, // the new rich_message parameter of editMessageText richMessage = InputRichMessageMarkdown("*Edited* rich message — now _updated_") ) ) } // === Bots API 10.2 additions: InputRichBlocks DSL + rich message media === // InputRichMessageBlocks { } — build a rich message from a typed InputRichBlock tree instead // of an HTML/Markdown string (exactly one of html/markdown/blocks may be used). The lambda is an // InputRichBlocksBuilder; buildInputRichBlocks { } returns the raw List the same way. onCommand("rich_blocks") { sendRichMessage( it.chat.id, InputRichMessageBlocks { heading("Rich blocks (Bots API 10.2)", level = 1) paragraph { plain("This message is built from ") bold("structured InputRichBlocks") plain(" — no HTML or Markdown string is involved.") } h2("Lists") h3("Ordered") orderedList { item(0) { paragraph("A plain list item") } item(1) { paragraph { url("google", "google.com") } } item(2, hasCheckbox = true, isChecked = true) { paragraph("A plain list item") } item(3, hasCheckbox = true, isChecked = false) { paragraph("A plain list item") } } divider() h3("Unordered") unorderedList { item { paragraph("A plain list item") } item { paragraph { url("google", "google.com") } } item(hasCheckbox = true, isChecked = true) { paragraph("A plain list item") } item(hasCheckbox = true, isChecked = false) { paragraph("A plain list item") } } divider() heading("Code", level = 2) preformatted("val answer = 42", language = "kotlin") heading("Quotation", level = 2) blockQuotation { paragraph { plain("Quotations are themselves made of nested blocks — ") italic("including inline formatting") plain(".") } } } ) } // === 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( it.chat.id, InputRichMessageBlocks { 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), DateTimeEntityFormatting(useDateLong = true)) } 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://github.com/InsanusMokrassar/ktgbotapi/archive/refs/heads/master.zip"), ), 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, // build the TelegramMedia from file.asMultipartFile() — SendRichMessage collects any // MultipartFile inside the rich message and uploads it as attach:// automatically. val photoMedia = TelegramMediaPhoto(message.content.media.fileId) // (1) referenced from HTML via tg://photo?id=, resolved through InputRichMessage.media sendRichMessage( message.chat.id, InputRichMessageHTML( """

Your photo, referenced from HTML

Below is your photo, referenced via tg://photo?id=userphoto:

""".trimIndent(), media = listOf( InputRichMessageMedia(id = "userphoto", media = photoMedia) ) ) ) // (2) as a first-class media block inside an InputRichBlocks tree sendRichMessage( message.chat.id, InputRichMessageBlocks { heading("Your photo, as a media block", level = 2) paragraph("The same photo, this time a photo() block inside the blocks tree:") photo(photoMedia) } ) } // 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( """

Your document, referenced from HTML

""".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") val richMessageContent = waitRichMessage().first() reply( it, "Got rich message with ${richMessageContent.richMessage.blocks.size} block(s)" ) } onGuestRequestMessage { val withTextContent = it.withContentOrNull() ?: return@onGuestRequestMessage val haveCommand = withTextContent.content.text.contains("/rich_guest") if (haveCommand) { answer( it.guestQueryId, InlineQueryResultArticle( InlineQueryId("rich_content"), "Send rich message", InputRichMessageContent( InputRichMessageMarkdown( testMarkdownText ) ) ) ) } } // onRichMessage trigger: incoming messages carrying the new rich_message field onRichMessage { message -> val richMessage = message.content.richMessage println("=== Rich message received ===") println(" isRtl: ${richMessage.isRtl}") println(" blocks: ${richMessage.blocks.size}") richMessage.blocks.forEachIndexed { index, block -> println(" [$index] $block") } reply(message, "Received a rich message with ${richMessage.blocks.size} block(s)") execute( message.content.createResend( message.chat.id, ) ) } // InputRichMessageContent as InputMessageContent of inline query results onBaseInlineQuery { query -> answer( query, results = listOf( InlineQueryResultArticle( InlineQueryId("rich_html"), "Rich message (HTML)", // InputRichMessageContent wraps an InputRichMessage and is a valid InputMessageContent InputRichMessageContent( InputRichMessageHTML("Bold rich message sent via inline query") ), description = "InputRichMessageContent built from HTML" ), InlineQueryResultArticle( InlineQueryId("rich_markdown"), "Rich message (Markdown)", InputRichMessageContent( InputRichMessageMarkdown("*Bold* rich message sent via inline query") ), description = "InputRichMessageContent built from Markdown" ) ), cachedTime = 0 ) } // onlyRichMessageContentMessages: filter a Flow> down to rich message content allUpdatesFlow .mapNotNull { it.baseSentMessageUpdateOrNull() ?.data ?.contentMessageOrNull() } .onlyRichMessageContentMessages() .subscribeLoggingDropExceptions(scope = this) { richMessageContentMessage -> println("[onlyRichMessageContentMessages] ${richMessageContentMessage.content.richMessage.blocks.size} blocks") } setMyCommands( 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"), BotCommand("wait_rich", "Wait for you to send a rich message"), ) allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) { println(it) } }.second.join() }