TitleContentTitleContentE = 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()
}