Files
TelegramBotAPI-examples/EphemeralMessagesBot/src/main/kotlin/EphemeralMessagesBot.kt
InsanusMokrassar 48b0e068d5 fix(examples): add bot selection and ephemeral photo captions
Limit channel-rights selection to private chats, preserve full chat
identifiers, offer bot selection, and use the replied-to sender ID.

Supply a caption when replacing ephemeral photo media so the next
caption edit starts from a captioned message.
2026-10-08 19:30:19 +06:00

237 lines
11 KiB
Kotlin

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.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.deleteEphemeralMessage
import dev.inmo.tgbotapi.extensions.api.edit.caption.editEphemeralMessageCaption
import dev.inmo.tgbotapi.extensions.api.edit.media.editEphemeralMessageMedia
import dev.inmo.tgbotapi.extensions.api.edit.text.editEphemeralMessageRichText
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.replyToEphemeral
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage
import dev.inmo.tgbotapi.extensions.api.send.media.sendLivePhoto
import dev.inmo.tgbotapi.extensions.api.send.media.sendPhoto
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitLivePhotoMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitPhotoMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.fromUserMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.EphemeralMessageParameters
import dev.inmo.tgbotapi.types.directMessageThreadIdOrMessageThreadIdAsDirectMessageThreadId
import dev.inmo.tgbotapi.types.ephemeralReplyReceiverUserIdOrNull
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
import dev.inmo.tgbotapi.types.message.abstracts.PossiblyEphemeralMessage
import dev.inmo.tgbotapi.types.rich.InputRichMessageBlocks
import korlibs.time.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
/**
* Runs the ephemeral-messages example bot using long polling.
*
* `/ephemeral` posts an inline button whose callback replaces it with a rich message visible only to
* the user who pressed it, edits that message, and deletes it. `/ephemeral_photo` demonstrates uploading new media
* to an ephemeral edit and moving its caption above the media. `/ephemeral_live_photo` demonstrates collecting both
* multipart files of a Live Photo edit. Incoming [PossiblyEphemeralMessage] instances receive both an automatic
* ephemeral [reply] and an explicit [replyToEphemeral].
*
* [args] must start with the bot token. The optional exact values `debug` and `testServer` respectively
* enable console logging and select Telegram's test server.
*
* @throws NoSuchElementException when [args] does not contain a bot token
*/
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))
}
)
}
telegramBotWithBehaviourAndLongPolling(
botToken,
testServer = isTestServer,
) {
val me = getMe()
println("Bot info: $me")
// Post (in a group) a message with an inline button. Tapping it triggers an ephemeral reply that is
// visible only to the user who tapped.
onCommand("ephemeral") {
reply(
it,
"Tap the button — the reply will be ephemeral (visible only to you).",
replyMarkup = flatInlineKeyboard {
dataButton("Reveal a secret", "reveal")
}
)
}
// Bot API 10.3 groups the recipient/callback fields in EphemeralMessageParameters. Setting
// replaceCallbackQueryMessage replaces the button message only for the user who pressed it.
onMessageDataCallbackQuery(Regex("reveal")) { query ->
val chatId = query.message.chat.id
val receiverUserId = query.from.id
val sent = sendRichMessage(
chatId,
InputRichMessageBlocks {
paragraph {
plain("🔒 ${query.from.firstName}, here is your personal secret: ")
code("42")
}
},
ephemeralMessageParameters = EphemeralMessageParameters(
receiverUserId = receiverUserId,
callbackQueryId = query.id,
replaceCallbackQueryMessage = false,
),
)
// Only the group-family Common*ContentMessage types implement PossiblyEphemeralMessage, so the
// sent ephemeral message exposes its ephemeralMessageId through that interface.
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
if (ephemeralMessageId != null) {
delay(3.seconds)
// editEphemeralMessageText now accepts rich_message; the typed extension exposes that as
// editEphemeralMessageRichText.
editEphemeralMessageRichText(
chatId,
receiverUserId,
ephemeralMessageId,
InputRichMessageBlocks {
h2("Secret revealed")
paragraph { plain("The answer is "); code("42") }
},
)
delay(3.seconds)
// deleteEphemeralMessage: same addressing (there is also a PossiblyEphemeralMessage overload)
deleteEphemeralMessage(chatId, receiverUserId, ephemeralMessageId)
}
}
// Upload a received photo again as a brand-new multipart file while editing an ephemeral media message.
onCommand("ephemeral_photo") { origin ->
val receiverUserId = origin.fromUserMessageOrNull()?.user?.id ?: return@onCommand
reply(origin, "Send a photo. I will return it as ephemeral media and then re-upload it in an edit.")
val photoMessage = waitPhotoMessage().filter {
it.sameChat(origin) && it.fromUserMessageOrNull()?.user?.id == receiverUserId
}.first()
val sent = sendPhoto(
photoMessage.chat.id,
photoMessage.content.media.fileId,
text = "Ephemeral photo using its existing Telegram file ID",
ephemeralMessageParameters = EphemeralMessageParameters(receiverUserId),
)
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
?: return@onCommand
val photoBytes = downloadFile(photoMessage.content)
editEphemeralMessageMedia(
photoMessage.chat.id,
receiverUserId,
ephemeralMessageId,
TelegramMediaPhoto(
photoBytes.asMultipartFile("ephemeral-photo.jpg"),
text = "sample"
),
)
editEphemeralMessageCaption(
photoMessage.chat.id,
receiverUserId,
ephemeralMessageId,
caption = "This caption is above newly uploaded media",
showCaptionAboveMedia = true,
)
}
// A Live Photo has a main file plus a secondary `photo` file. ktgbotapi 37.0.0 includes both multipart
// attachments when EditEphemeralMessageMedia builds its request.
onCommand("ephemeral_live_photo") { origin ->
val receiverUserId = origin.fromUserMessageOrNull()?.user?.id ?: return@onCommand
reply(origin, "Send a Live Photo. I will send it ephemerally and edit it using two new uploads.")
val livePhotoMessage = waitLivePhotoMessage().filter {
it.sameChat(origin) && it.fromUserMessageOrNull()?.user?.id == receiverUserId
}.first()
val livePhoto = livePhotoMessage.content.media
val sent = sendLivePhoto(
chatId = livePhotoMessage.chat.id,
livePhoto = livePhoto,
text = "Ephemeral Live Photo using existing Telegram file IDs",
ephemeralMessageParameters = EphemeralMessageParameters(receiverUserId),
)
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
?: return@onCommand
val livePhotoBytes = downloadFile(livePhoto)
val coverPhotoBytes = livePhoto.photo?.let { downloadFile(it) }
editEphemeralMessageMedia(
livePhotoMessage.chat.id,
receiverUserId,
ephemeralMessageId,
TelegramMediaLivePhoto(
file = livePhotoBytes.asMultipartFile("ephemeral-live-photo.mp4"),
photo = coverPhotoBytes?.asMultipartFile("ephemeral-live-photo-cover.jpg")
?: livePhoto.photo?.fileId
?: livePhoto.fileId,
text = "Edited with newly uploaded main and cover files",
),
)
}
// Incoming ephemeral messages: detect them via PossiblyEphemeralMessage, then answer them.
onContentMessage { message ->
val ephemeral = (message as? PossiblyEphemeralMessage)?.takeIf { it.ephemeralMessageId != null }
?: return@onContentMessage
// reply smart-branch: because `message` is ephemeral, this reply is sent ephemeral to the same
// receiver automatically — no ephemeral parameters needed here.
reply(message, "Got your ephemeral message — I am replying ephemerally too.")
// The explicit equivalent, addressing the ephemeral message by hand:
val receiverUserId = ephemeral.ephemeralReplyReceiverUserIdOrNull
if (receiverUserId != null) {
replyToEphemeral(
message.chat.id,
EphemeralMessageParameters(receiverUserId),
ephemeral.ephemeralMessageId!!,
"Explicit ephemeral reply via replyToEphemeral",
)
}
}
setMyCommands(
// isEphemeral marks a command whose response is an ephemeral (personal) message
BotCommand("ephemeral", "Post a button that reveals an ephemeral (personal) message", isEphemeral = true),
BotCommand("ephemeral_photo", "Re-upload a photo through an ephemeral media edit", isEphemeral = true),
BotCommand("ephemeral_live_photo", "Re-upload both files of an ephemeral Live Photo", isEphemeral = true),
)
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it)
}
}.second.join()
}