Compare commits

..

2 Commits

Author SHA1 Message Date
renovate[bot]
4f63010e02 Update telegram_bot_api_version to v36.1.0 2026-08-27 08:58:40 +00:00
3b7adc4033 Merge pull request #368 from InsanusMokrassar/36.0.0
36.0.0
2026-08-27 14:57:24 +06:00
19 changed files with 139 additions and 604 deletions

View File

@@ -1,6 +1,6 @@
# CommunitiesBot # CommunitiesBot
This long-polling example demonstrates Communities support introduced in Telegram Bot API 10.2 and extended in 10.3: typed service events when a chat joins or leaves a community, an event when a user joins a chat from a community, and inspection of a chat's current community. This long-polling example demonstrates Communities support introduced in Telegram Bot API 10.2: typed service events when a chat joins or leaves a community, and inspection of a chat's current community.
## Behavior, commands, and triggers ## Behavior, commands, and triggers
@@ -10,21 +10,18 @@ At startup, the bot calls `getMe` and prints its bot information. It also prints
| --- | --- | | --- | --- |
| `community_chat_added` service message | `onCommunityChatAdded` logs the chat and community name/ID, sends a join notice, then calls `getChat` and logs its nullable `community`. | | `community_chat_added` service message | `onCommunityChatAdded` logs the chat and community name/ID, sends a join notice, then calls `getChat` and logs its nullable `community`. |
| `community_chat_removed` service message | `onCommunityChatRemoved` logs the chat and sends a leave notice. This event is fieldless, so it has no former-community details. | | `community_chat_removed` service message | `onCommunityChatRemoved` logs the chat and sends a leave notice. This event is fieldless, so it has no former-community details. |
| `community_chat_joined` service message | `onCommunityChatJoined` logs the source community and replies with a welcome message. This means a user joined the current chat from a community; it is distinct from adding the chat itself to a community. |
| `/community` | Calls `getChat` and replies with the current community name/ID, or says the chat is not in a community. | | `/community` | Calls `getChat` and replies with the current community name/ID, or says the chat is not in a community. |
| `/wait_community_added` | Waits without a timeout for the next added event in the command's chat, then replies with the community name/ID. | | `/wait_community_added` | Waits without a timeout for the next added event in the command's chat, then replies with the community name/ID. |
| `/wait_community_removed` | Waits without a timeout for the next removed event in the command's chat, then replies with the chat ID. | | `/wait_community_removed` | Waits without a timeout for the next removed event in the command's chat, then replies with the chat ID. Its initial waiting reply currently says "added." |
| `/wait_community_joined` | Waits without a timeout for the next user-from-community join event in the command's chat, then replies with the source community name/ID. |
Commands use no positional arguments; other commands only appear in the generic update log. Each wait first sends a waiting reply, filters events with `sameChat`, and takes the first match. Commands use no positional arguments; other commands only appear in the generic update log. Each wait first sends a waiting reply, filters events with `sameChat`, and takes the first match.
## API concepts ## API concepts
- `CommunityChatAdded` carries a `Community` with a `CommunityId` and name; `CommunityChatRemoved` carries no fields. - `CommunityChatAdded` carries a `Community` with a `CommunityId` and name; `CommunityChatRemoved` carries no fields.
- `CommunityChatJoined` carries the community through which a user joined the current chat. - `onCommunityChatAdded` and `onCommunityChatRemoved` provide typed handlers for the service events.
- `onCommunityChatAdded`, `onCommunityChatRemoved`, and `onCommunityChatJoined` provide typed handlers for the service events.
- `getChat(...).community` exposes the nullable community on `ExtendedChat` without a subtype cast. - `getChat(...).community` exposes the nullable community on `ExtendedChat` without a subtype cast.
- `waitCommunityChatAddedEventsMessages`, `waitCommunityChatRemovedEventsMessages`, and `waitCommunityChatJoinedEventsMessages` expose typed event-message flows. - `waitCommunityChatAddedEventsMessages` and `waitCommunityChatRemovedEventsMessages` expose typed event-message flows.
## Telegram setup and permissions ## Telegram setup and permissions

View File

@@ -7,13 +7,13 @@ import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatAdded
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatAddedEventsMessages import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatAddedEventsMessages
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatJoinedEventsMessages import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatRemoved
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatRemovedEventsMessages import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatRemovedEventsMessages
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling 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.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatAdded import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatAdded
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatJoined
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatRemoved import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatRemoved
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -25,11 +25,10 @@ import kotlinx.coroutines.flow.first
* Starts a long-polling bot that demonstrates Telegram Communities. * Starts a long-polling bot that demonstrates Telegram Communities.
* *
* [onCommunityChatAdded] receives the joined [dev.inmo.tgbotapi.types.communities.Community], while * [onCommunityChatAdded] receives the joined [dev.inmo.tgbotapi.types.communities.Community], while
* [onCommunityChatRemoved] receives a fieldless removal event, while [onCommunityChatJoined] reports a user joining * [onCommunityChatRemoved] receives a fieldless removal event. `/community` reads
* the chat from a community. `/community` reads * [dev.inmo.tgbotapi.types.chat.ExtendedChat.community] with [getChat]. The two wait commands use
* [dev.inmo.tgbotapi.types.chat.ExtendedChat.community] with [getChat]. The three wait commands use typed event-message * [waitCommunityChatAddedEventsMessages] and [waitCommunityChatRemovedEventsMessages] to take the first same-chat
* expectations to take the first same-chat event without a timeout. The bot prints its [getMe] result and every * event without a timeout. The bot prints its [getMe] result and every received update.
* received update.
* *
* @param args the bot token followed by the optional, case-sensitive `debug` and `testServer` flags; unknown trailing * @param args the bot token followed by the optional, case-sensitive `debug` and `testServer` flags; unknown trailing
* arguments are ignored * arguments are ignored
@@ -73,13 +72,6 @@ suspend fun main(vararg args: String) {
send(message.chat.id, "This chat has left its community") send(message.chat.id, "This chat has left its community")
} }
// community_chat_joined: a user joined this chat through a community
onCommunityChatJoined { message ->
val community = message.chatEvent.community
println("A user joined chat ${message.chat.id} from community '${community.name}' (id=${community.id.long})")
reply(message, "Welcome! You joined from the ${community.name} community.")
}
// Inspect the current chat's community on demand // Inspect the current chat's community on demand
onCommand("community") { onCommand("community") {
val community = getChat(it.chat.id).community val community = getChat(it.chat.id).community
@@ -102,18 +94,11 @@ suspend fun main(vararg args: String) {
// Suspend until the next community-removed event message from this chat. // Suspend until the next community-removed event message from this chat.
onCommand("wait_community_removed") { origin -> onCommand("wait_community_removed") { origin ->
reply(origin, "Waiting for this chat to be removed from a community...") reply(origin, "Waiting for this chat to be added to a community...")
waitCommunityChatRemovedEventsMessages().filter { it.sameChat(origin) }.first() waitCommunityChatRemovedEventsMessages().filter { it.sameChat(origin) }.first()
reply(origin, "Chat removed from its community (${origin.chat.id})") reply(origin, "Chat removed from its community (${origin.chat.id})")
} }
// Suspend until a user joins this chat from a community.
onCommand("wait_community_joined") { origin ->
reply(origin, "Waiting for somebody to join this chat from a community...")
val event = waitCommunityChatJoinedEventsMessages().filter { it.sameChat(origin) }.first().chatEvent
reply(origin, "A user joined from ${event.community.name} (id=${event.community.id.long})")
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it) println(it)
} }

View File

@@ -2,8 +2,7 @@
DraftsBot demonstrates streaming a message draft before sending the finished DraftsBot demonstrates streaming a message draft before sending the finished
message. It receives updates through long polling and uses the same built-in message. It receives updates through long polling and uses the same built-in
Lorem ipsum text for all examples. Bot API 10.3's stoppable generation controls Lorem ipsum text for both examples.
and `stopped_message_generation` updates are included.
## Commands ## Commands
@@ -11,32 +10,18 @@ and `stopped_message_generation` updates are included.
500 ms, then sends the complete text as a normal message. 500 ms, then sends the complete text as a normal message.
- `/test_empty_draft` first publishes an empty draft, waits 1.5 seconds, streams - `/test_empty_draft` first publishes an empty draft, waits 1.5 seconds, streams
the same prefixes, and then sends the complete text. the same prefixes, and then sends the complete text.
- `/test_stoppable_draft` continuously streams progressively longer revisions
with `canStop = true` and `keepOnStop = true`. Telegram displays a stop control;
stopping keeps the most recent draft revision and causes the flow helper to
return `false`. Before streaming, the handler subscribes to
`waitMessageGenerationStopped` and filters the expectation by chat and draft
ID, then logs the matched stop event. It deliberately sends no confirmation
message, because sending one would immediately remove the draft retained by
`keepOnStop`. If streaming ends because of another request failure and no
matching update arrives within five seconds, the bot logs that distinction
instead of waiting forever.
The bot advertises all three commands in Telegram's command menu for private The bot advertises both commands in Telegram's command menu for all group chats.
chats and filters each handler to private chats, as required by Telegram's draft The handlers themselves are not restricted by chat type, so either command can
methods. also be entered manually in a private chat.
The typed `onMessageGenerationStopped` handler logs the chat, optional topic ID,
and draft ID from every generation-stopped update, independently of the scoped
expectation used by `/test_stoppable_draft`. Both apply to text and rich-message
drafts sent by this bot token.
## Setup ## Setup
1. Obtain a bot token and keep it out of source control. 1. Obtain a bot token and keep it out of source control.
2. Start a private chat with the bot. Telegram doesn't accept a group or channel 2. Start a private chat with the bot, or add it to a group where you want to run
ID for `sendMessageDraft`. the example.
3. No administrator rights are required by this example. 3. In groups, allow the bot to send messages. No administrator rights are
otherwise required by this example.
The first program argument is required and must be the bot token. Omitting it The first program argument is required and must be the bot token. Omitting it
causes startup to fail; any later arguments are ignored. causes startup to fail; any later arguments are ignored.
@@ -49,6 +34,8 @@ From the repository root, run:
./gradlew :DraftsBot:run --args="<BOT_TOKEN>" ./gradlew :DraftsBot:run --args="<BOT_TOKEN>"
``` ```
> **Known issue:** `DraftsBot/build.gradle` currently declares `TopicsHandlingKt` as the main class, while this bot's entry point is `DraftsBotKt`. The `run` task cannot start until that Gradle setting is corrected; it is left unchanged by this documentation-only update.
Every received update is printed to standard output. Unhandled polling errors Every received update is printed to standard output. Unhandled polling errors
are printed with their stack traces, and HTTP request, socket, and connection are printed with their stack traces, and HTTP request, socket, and connection
timeouts are each configured to 30 seconds. timeouts are each configured to 30 seconds.

View File

@@ -11,7 +11,7 @@ buildscript {
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'application' apply plugin: 'application'
mainClassName="DraftsBotKt" mainClassName="TopicsHandlingKt"
dependencies { dependencies {

View File

@@ -12,9 +12,7 @@ import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlow import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlow
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageGenerationStopped
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageGenerationStopped
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicClosed import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicClosed
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicCreated import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicCreated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicEdited import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicEdited
@@ -32,26 +30,18 @@ import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.ForumTopic import dev.inmo.tgbotapi.types.ForumTopic
import dev.inmo.tgbotapi.types.chat.PrivateChat import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.commands.BotCommandScope import dev.inmo.tgbotapi.types.commands.BotCommandScope
import dev.inmo.tgbotapi.utils.DraftIdAllocator
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeoutOrNull
/** Sample text streamed as a draft and then sent as the completed message. */ /** Sample text streamed as a draft and then sent as the completed message. */
const val testText = """ const val testText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""" """
private val stoppableDraftIds = DraftIdAllocator()
/** /**
* Starts DraftsBot with long polling and registers the draft demonstration commands. * Starts DraftsBot with long polling and registers the draft demonstration commands.
* *
@@ -77,7 +67,7 @@ suspend fun main(vararg args: String) {
} }
} }
) { ) {
onCommand("test_draft_flow", initialFilter = { it.chat is PrivateChat }) { onCommand("test_draft_flow") {
sendMessageDraftFlowWithTexts( sendMessageDraftFlowWithTexts(
it.chat.id, it.chat.id,
flow<String> { flow<String> {
@@ -95,7 +85,7 @@ suspend fun main(vararg args: String) {
// sendMessageDraft now accepts empty text (length 0 is valid since TG Bot API 9.0) // sendMessageDraft now accepts empty text (length 0 is valid since TG Bot API 9.0)
// Useful to show a typing indicator without any text yet // Useful to show a typing indicator without any text yet
onCommand("test_empty_draft", initialFilter = { it.chat is PrivateChat }) { onCommand("test_empty_draft") {
sendMessageDraftFlowWithTexts( sendMessageDraftFlowWithTexts(
it.chat.id, it.chat.id,
flow<String> { flow<String> {
@@ -113,62 +103,10 @@ suspend fun main(vararg args: String) {
send(it.chat, testText) send(it.chat, testText)
} }
// Bot API 10.3 lets the user stop generation. The expectation is subscribed before streaming starts so it
// cannot miss a fast stop update; matching both chat and draft ID also avoids consuming another draft's event.
onCommand("test_stoppable_draft", initialFilter = { it.chat is PrivateChat }) { origin ->
val draftId = stoppableDraftIds.allocate()
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
waitMessageGenerationStopped()
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
.first()
}
try {
val completed = sendMessageDraftFlowWithTexts(
origin.chat.id,
flow<String> {
val step = 20
var currentLength = step
while (isActive) {
delay(500L)
emit(testText.take(currentLength.coerceAtMost(testText.length)))
currentLength = (currentLength + step).coerceAtMost(testText.length)
}
},
draftId = draftId,
canStop = true,
keepOnStop = true,
)
if (!completed) {
val stopped = withTimeoutOrNull(5_000L) { stoppedUpdate.await() }
if (stopped == null) {
println("Draft streaming ended without a matching stopped_message_generation update")
} else {
// A normal message here would immediately remove the kept draft, so report only to stdout.
println(
"Expectation matched stopped draft ${stopped.draftId.long} in ${stopped.chat.id}; " +
"Telegram kept its last revision temporarily."
)
}
}
} finally {
stoppedUpdate.cancel()
stoppableDraftIds.free(draftId)
}
}
// The stopped_message_generation update includes the chat, optional topic and stopped draft ID.
onMessageGenerationStopped { stopped ->
println(
"Message generation stopped in ${stopped.chat.id}; " +
"thread=${stopped.messageThreadId}, draft=${stopped.draftId}"
)
}
setMyCommands( setMyCommands(
BotCommand("test_draft_flow", "Start draft testing with flow"), BotCommand("test_draft_flow", "Start draft testing with flow"),
BotCommand("test_empty_draft", "Draft starting from empty text (TG Bot API 9.0)"), BotCommand("test_empty_draft", "Draft starting from empty text (TG Bot API 9.0)"),
BotCommand("test_stoppable_draft", "Stream a draft that the user can stop"), scope = BotCommandScope.AllGroupChats
scope = BotCommandScope.AllPrivateChats
) )
allUpdatesFlow.subscribeLoggingDropExceptions(this) { allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it) println(it)

View File

@@ -1,31 +1,22 @@
# EphemeralMessagesBot # EphemeralMessagesBot
Demonstrates Telegram Bot API 10.2 and 10.3 ephemeral messages: group messages that Telegram shows only to one receiver. Demonstrates ephemeral messages: group messages that Telegram shows only to one receiver.
## Behavior ## Behavior
- `/ephemeral` replies with a **Reveal a secret** inline button. The command is registered with Telegram as - `/ephemeral` replies with a **Reveal a secret** inline button. The command is registered with Telegram as
an ephemeral command. an ephemeral command.
- Pressing the button (`reveal` callback data) uses `EphemeralMessageParameters` to replace the callback-query - Pressing the button (`reveal` callback data) sends a personal message visible only to the user who
message with a personal rich message visible only to the user who pressed it. After three seconds the bot replaces pressed it. After three seconds the bot edits the message, then deletes it three seconds later.
its text with typed rich blocks, then deletes it three seconds later.
- `/ephemeral_photo` waits for a photo from the same user and chat, sends it back ephemerally by its Telegram file ID,
downloads it, and edits the ephemeral media using a new multipart upload. A second edit sets
`showCaptionAboveMedia = true`.
- `/ephemeral_live_photo` waits for a Live Photo, sends it ephemerally by existing file IDs, then downloads and
re-uploads both its main file and secondary `photo` file in one `editEphemeralMessageMedia` request. This exercises
ktgbotapi 37.0.0's secondary multipart attachment collection.
- When the bot receives an ephemeral content message, it sends two ephemeral replies: one through the - When the bot receives an ephemeral content message, it sends two ephemeral replies: one through the
general `reply` API and one through the explicit `replyToEphemeral` API. The explicit form also uses general `reply` API and one through the explicit `replyToEphemeral` API.
`EphemeralMessageParameters`.
- Updates and basic bot information are printed to standard output. - Updates and basic bot information are printed to standard output.
## Setup ## Setup
Create a bot token, keep it secret, and add the bot to a group. The bot must be allowed to send messages Create a bot token, keep it secret, and add the bot to a group. The bot must be allowed to send messages
there; this example does not request or validate group permissions itself. Use a Telegram environment that there; this example does not request or validate group permissions itself. Use a Telegram environment that
supports ephemeral messages. The photo demonstrations download all selected media into memory before uploading it supports ephemeral messages.
again.
## Run ## Run

View File

@@ -6,47 +6,29 @@ import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.deleteEphemeralMessage import dev.inmo.tgbotapi.extensions.api.deleteEphemeralMessage
import dev.inmo.tgbotapi.extensions.api.edit.caption.editEphemeralMessageCaption import dev.inmo.tgbotapi.extensions.api.edit.text.editEphemeralMessageText
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.reply
import dev.inmo.tgbotapi.extensions.api.send.replyToEphemeral import dev.inmo.tgbotapi.extensions.api.send.replyToEphemeral
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage import dev.inmo.tgbotapi.extensions.api.send.sendTextMessage
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.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand 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.onContentMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery 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.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard 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.BotCommand
import dev.inmo.tgbotapi.types.EphemeralMessageParameters
import dev.inmo.tgbotapi.types.ephemeralReplyReceiverUserIdOrNull 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.message.abstracts.PossiblyEphemeralMessage
import dev.inmo.tgbotapi.types.rich.InputRichMessageBlocks
import korlibs.time.seconds import korlibs.time.seconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
/** /**
* Runs the ephemeral-messages example bot using long polling. * 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 * `/ephemeral` posts an inline button whose callback sends, edits, and deletes a message visible only to
* the user who pressed it, edits that message, and deletes it. `/ephemeral_photo` demonstrates uploading new media * the user who pressed it. Incoming [PossiblyEphemeralMessage] instances receive both an automatic
* 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]. * ephemeral [reply] and an explicit [replyToEphemeral].
* *
* [args] must start with the bot token. The optional exact values `debug` and `testServer` respectively * [args] must start with the bot token. The optional exact values `debug` and `testServer` respectively
@@ -86,25 +68,18 @@ suspend fun main(vararg args: String) {
) )
} }
// Bot API 10.3 groups the recipient/callback fields in EphemeralMessageParameters. Setting // Send an ephemeral message in response to a callback query. `receiverUserId` + `callbackQueryId`
// replaceCallbackQueryMessage replaces the button message only for the user who pressed it. // make the outgoing message ephemeral — Telegram shows it only to the querying user (and this also
// serves as the answer to the callback query).
onMessageDataCallbackQuery(Regex("reveal")) { query -> onMessageDataCallbackQuery(Regex("reveal")) { query ->
val chatId = query.message.chat.id val chatId = query.message.chat.id
val receiverUserId = query.from.id val receiverUserId = query.from.id
val sent = sendRichMessage( val sent = sendTextMessage(
chatId, chatId,
InputRichMessageBlocks { "🔒 ${query.from.firstName}, here is your personal secret: 42",
paragraph { receiverUserId = receiverUserId,
plain("🔒 ${query.from.firstName}, here is your personal secret: ") callbackQueryId = query.id,
code("42")
}
},
ephemeralMessageParameters = EphemeralMessageParameters(
receiverUserId = receiverUserId,
callbackQueryId = query.id,
replaceCallbackQueryMessage = true,
),
) )
// Only the group-family Common*ContentMessage types implement PossiblyEphemeralMessage, so the // Only the group-family Common*ContentMessage types implement PossiblyEphemeralMessage, so the
@@ -112,91 +87,14 @@ suspend fun main(vararg args: String) {
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
if (ephemeralMessageId != null) { if (ephemeralMessageId != null) {
delay(3.seconds) delay(3.seconds)
// editEphemeralMessageText now accepts rich_message; the typed extension exposes that as // editEphemeralMessageText: address the ephemeral message by chatId + receiverUserId + ephemeralMessageId
// editEphemeralMessageRichText. editEphemeralMessageText(chatId, receiverUserId, ephemeralMessageId, "🔓 Revealed: the answer is 42")
editEphemeralMessageRichText(
chatId,
receiverUserId,
ephemeralMessageId,
InputRichMessageBlocks {
h2("Secret revealed")
paragraph { plain("The answer is "); code("42") }
},
)
delay(3.seconds) delay(3.seconds)
// deleteEphemeralMessage: same addressing (there is also a PossiblyEphemeralMessage overload) // deleteEphemeralMessage: same addressing (there is also a PossiblyEphemeralMessage overload)
deleteEphemeralMessage(chatId, receiverUserId, ephemeralMessageId) 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")),
)
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. // Incoming ephemeral messages: detect them via PossiblyEphemeralMessage, then answer them.
onContentMessage { message -> onContentMessage { message ->
val ephemeral = (message as? PossiblyEphemeralMessage)?.takeIf { it.ephemeralMessageId != null } val ephemeral = (message as? PossiblyEphemeralMessage)?.takeIf { it.ephemeralMessageId != null }
@@ -211,7 +109,7 @@ suspend fun main(vararg args: String) {
if (receiverUserId != null) { if (receiverUserId != null) {
replyToEphemeral( replyToEphemeral(
message.chat.id, message.chat.id,
EphemeralMessageParameters(receiverUserId), receiverUserId,
ephemeral.ephemeralMessageId!!, ephemeral.ephemeralMessageId!!,
"Explicit ephemeral reply via replyToEphemeral", "Explicit ephemeral reply via replyToEphemeral",
) )
@@ -221,8 +119,6 @@ suspend fun main(vararg args: String) {
setMyCommands( setMyCommands(
// isEphemeral marks a command whose response is an ephemeral (personal) message // 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", "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) { allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {

View File

@@ -15,16 +15,12 @@ Regular gifts are shown with their ID, optional text, and Stars cost. Unique gif
name, model, and number. Long results are split into multiple Telegram messages; an empty result produces name, model, and number. Long results are split into multiple Telegram messages; an empty result produces
`This chat have no any gifts`. `This chat have no any gifts`.
The bot also handles `UniqueGiftInfo` service messages with `onUniqueGiftSentOrReceived`. It logs and replies with
Bot API 10.3's `text`, parsed `textSources` (`entities`), and `isPrivate` fields. When entities are present, the reply
reuses them so the gift text keeps its formatting.
## Command ## Command
- `/start` — lists the owned gifts selected by the current chat type. It must be the only command in the message and - `/start` — lists the owned gifts selected by the current chat type. It must be the only command in the message and
takes no arguments. takes no arguments.
Other commands and ordinary non-command messages are ignored; unique-gift service messages are handled separately. Other commands and non-command messages are ignored.
## Setup and permissions ## Setup and permissions

View File

@@ -10,7 +10,10 @@ import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling 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.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onUniqueGiftSentOrReceived import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCompleted
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayContent
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCreated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayWinners
import dev.inmo.tgbotapi.types.chat.BusinessChat import dev.inmo.tgbotapi.types.chat.BusinessChat
import dev.inmo.tgbotapi.types.chat.PrivateChat import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.chat.PublicChat import dev.inmo.tgbotapi.types.chat.PublicChat
@@ -19,7 +22,6 @@ import dev.inmo.tgbotapi.types.gifts.OwnedGift
import dev.inmo.tgbotapi.types.message.textsources.splitForText import dev.inmo.tgbotapi.types.message.textsources.splitForText
import dev.inmo.tgbotapi.utils.bold import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -28,8 +30,7 @@ import kotlinx.coroutines.Dispatchers
* *
* Business chats are queried through their business connection, private chats through their user ID, and public or * Business chats are queried through their business connection, private chats through their user ID, and public or
* unknown chat types through their chat ID. Regular and unique gifts are rendered as formatted text and long results * unknown chat types through their chat ID. Regular and unique gifts are rendered as formatted text and long results
* are split across replies. Unique-gift service messages also expose the Bot API 10.3 text, entities and privacy flag. * are split across replies. The bot also prints its [getMe] result at startup.
* The bot prints its [getMe] result at startup.
* *
* @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags; unknown trailing * @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags; unknown trailing
* arguments are ignored * arguments are ignored
@@ -54,28 +55,6 @@ suspend fun main(vararg args: String) {
val me = getMe() val me = getMe()
println(me) println(me)
onUniqueGiftSentOrReceived { message ->
val uniqueGiftInfo = message.chatEvent
println(
"Unique gift ${uniqueGiftInfo.gift.name.value}: " +
"text=${uniqueGiftInfo.text}, textSources=${uniqueGiftInfo.textSources}, " +
"isPrivate=${uniqueGiftInfo.isPrivate}"
)
reply(
message,
buildEntities {
bold("Unique gift") + ": ${uniqueGiftInfo.gift.name.value}\n"
bold("Private") + ": ${uniqueGiftInfo.isPrivate}\n"
bold("Text") + ": "
if (uniqueGiftInfo.textSources.isEmpty()) {
regular(uniqueGiftInfo.text ?: "(None)")
} else {
+uniqueGiftInfo.textSources
}
}
)
}
onCommand("start") { onCommand("start") {
val giftsFlow = when (val chat = it.chat) { val giftsFlow = when (val chat = it.chat) {
is BusinessChat -> { is BusinessChat -> {

View File

@@ -57,11 +57,7 @@ fun InlineKeyboardBuilder.includePageButtons(page: Int, count: Int) {
val numbersRange = 1 .. count val numbersRange = 1 .. count
numericButtons.forEach { numericButtons.forEach {
if (it in numbersRange) { if (it in numbersRange) {
if (it == page) { dataButton(it.toString(), "$it $count")
disabledButton(it.toString(), style = KeyboardButtonStyle.Primary)
} else {
dataButton(it.toString(), "$it $count")
}
} }
} }
} }
@@ -121,7 +117,7 @@ suspend fun activateKeyboardsBot(
val page = numberArgs.firstOrNull()?.takeIf { numberArgs.size > 1 }?.coerceAtLeast(1) ?: 1 val page = numberArgs.firstOrNull()?.takeIf { numberArgs.size > 1 }?.coerceAtLeast(1) ?: 1
reply( reply(
message, message,
replyMarkup = inlineKeyboard(forceReply = true) { replyMarkup = inlineKeyboard {
includePageButtons(page, numberOfPages) includePageButtons(page, numberOfPages)
} }
) { ) {
@@ -140,7 +136,7 @@ suspend fun activateKeyboardsBot(
answer(it, "Unsupported message type :(") answer(it, "Unsupported message type :(")
return@onMessageDataCallbackQuery return@onMessageDataCallbackQuery
}, },
replyMarkup = inlineKeyboard(forceReply = true) { replyMarkup = inlineKeyboard {
includePageButtons(page, count) includePageButtons(page, count)
} }
) { ) {
@@ -156,7 +152,7 @@ suspend fun activateKeyboardsBot(
editMessageText( editMessageText(
it.inlineMessageId, it.inlineMessageId,
replyMarkup = inlineKeyboard(forceReply = true) { replyMarkup = inlineKeyboard {
includePageButtons(page, count) includePageButtons(page, count)
} }
) { ) {
@@ -177,7 +173,7 @@ suspend fun activateKeyboardsBot(
InlineQueryId(it.query), InlineQueryId(it.query),
"Send buttons", "Send buttons",
InputTextMessageContent("It is sent via inline mode inline buttons"), InputTextMessageContent("It is sent via inline mode inline buttons"),
replyMarkup = inlineKeyboard(forceReply = true) { replyMarkup = inlineKeyboard {
includePageButtons(page, count) includePageButtons(page, count)
} }
) )
@@ -188,11 +184,7 @@ suspend fun activateKeyboardsBot(
onUnhandledCommand { onUnhandledCommand {
reply( reply(
it, it,
replyMarkup = replyKeyboard( replyMarkup = replyKeyboard(resizeKeyboard = true, oneTimeKeyboard = true) {
resizeKeyboard = true,
oneTimeKeyboard = true,
forceReply = true,
) {
row { row {
simpleButton("/inline", style = KeyboardButtonStyle.Primary) simpleButton("/inline", style = KeyboardButtonStyle.Primary)
} }

View File

@@ -1,6 +1,6 @@
# KeyboardsBot # KeyboardsBot
A multiplatform long-polling example that demonstrates Telegram reply keyboards, inline keyboards, callback queries, copy-text buttons, inline-mode buttons, keyboard button styles, disabled buttons, and forced reply interfaces. The shared bot behavior lives in `KeyboardsBotLib`; the project provides a browser/JS entry point and a separate JVM launcher. A multiplatform long-polling example that demonstrates Telegram reply keyboards, inline keyboards, callback queries, copy-text buttons, inline-mode buttons, and keyboard button styles. The shared bot behavior lives in `KeyboardsBotLib`; the project provides a browser/JS entry point and a separate JVM launcher.
## Bot behavior ## Bot behavior
@@ -16,16 +16,16 @@ At startup, the bot calls `getMe`, reports the returned bot information through
Only numeric command arguments are considered. Use positive integers with `page <= count`; the example does not validate the count or clamp the page to the upper bound. Only numeric command arguments are considered. Use positive integers with `page <= count`; the example does not validate the count or clamp the page to the upper bound.
The generated inline keyboard sets Bot API 10.3's `force_reply` field and contains: The generated inline keyboard contains:
- a disabled button for the current page and callback buttons for adjacent pages within `1..count`; - numbered buttons for the current page and any adjacent pages that are within `1..count`;
- styled jump buttons for moving toward the first or last page when applicable; - styled jump buttons for moving toward the first or last page when applicable;
- a **Command copy button** that copies `/inline <page> <count>`; - a **Command copy button** that copies `/inline <page> <count>`;
- a **Send somebody page** button that starts inline mode and lets the user choose a user, bot, group, or channel. - a **Send somebody page** button that starts inline mode and lets the user choose a user, bot, group, or channel.
Pagination callbacks edit the original message and replace its text with `This is <page> of <count>`. This works for both ordinary bot messages and messages sent through inline mode. Unsupported callback data or an unsupported message type is answered with a callback notification instead. Pagination callbacks edit the original message and replace its text with `This is <page> of <count>`. This works for both ordinary bot messages and messages sent through inline mode. Unsupported callback data or an unsupported message type is answered with a callback notification instead.
Any command not handled above, including `/start`, receives a one-time reply keyboard containing a styled `/inline` button. That reply keyboard also sets `force_reply`, demonstrating the field on both markup types. Ordinary non-command messages are ignored. Any command not handled above, including `/start`, receives a one-time reply keyboard containing a styled `/inline` button. Ordinary non-command messages are ignored.
### Inline mode ### Inline mode

View File

@@ -8,23 +8,17 @@ The bot defines no commands. It prints every incoming update to standard output
| Trigger | Behavior | | Trigger | Behavior |
| --- | --- | | --- | --- |
| Standalone Live Photo | Logs its file identifiers, dimensions, duration, thumbnail, MIME type, size, and caption. It resends the Live Photo by file ID, downloads both components, edits the resent message with new multipart files, and uploads the files again as paid content costing 1 Star. | | Standalone Live Photo | Logs its file identifiers, dimensions, duration, thumbnail, MIME type, size, and caption. It resends the Live Photo, sends the same media as paid content costing 1 Star, and then edits the resent message with `TelegramMediaLivePhoto`. |
| Live Photo gallery | Logs every item, downloads each main and cover file, and re-uploads the gallery with `sendMediaGroup`. | | Live Photo gallery | Logs every item and resends the gallery with `sendMediaGroup`. |
| Paid-media message containing Live Photos | Logs each Live Photo and replies with the number found. Paid-media messages without a Live Photo get no reply from this handler. | | Paid-media message containing Live Photos | Logs each Live Photo and replies with the number found. Paid-media messages without a Live Photo get no reply from this handler. |
| Edited Live Photo | Logs the file ID and updated caption. | | Edited Live Photo | Logs the file ID and updated caption. |
| Media group containing at least one regular photo and one regular video | Uses the first photo as the cover and the first video as the motion part, then replies with a Live Photo. Albums missing either type are ignored by this handler. | | Media group containing at least one regular photo and one regular video | Uses the first photo as the cover and the first video as the motion part, then replies with a Live Photo. Albums missing either type are ignored by this handler. |
## Live Photo handling ## Live Photo handling
The initial `sendLivePhoto` call reuses Telegram file IDs. The regular-media edit, The bot does not download or transform media. It reuses Telegram file IDs: the received Live Photo is passed directly to `sendLivePhoto`, while its main file ID and thumbnail file ID are used to construct `TelegramMediaLivePhoto` and `TelegramPaidMediaLivePhoto`. If Telegram supplies no thumbnail, the code falls back to the main file ID for the photo field.
paid-media send, and gallery resend then demonstrate ktgbotapi 37.0.0 collecting
the secondary Live Photo `photo` attachment as well as the main multipart file.
If Telegram supplies no cover photo, the code falls back to the main file ID for
the `photo` field. Downloaded files are held in memory and are not transformed.
The standalone handler performs its requests in order: resend, download, edit, The standalone handler performs its requests in order: resend, send paid media, then edit the resent message. Consequently, a failure while sending paid media prevents the edit for that update.
then send paid media. The edit can therefore succeed in a private or group chat
before the channel-only paid-media request fails.
## Telegram setup and permissions ## Telegram setup and permissions

View File

@@ -5,7 +5,6 @@ import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.edit.media.editMessageMedia import dev.inmo.tgbotapi.extensions.api.edit.media.editMessageMedia
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.send.media.sendLivePhoto import dev.inmo.tgbotapi.extensions.api.send.media.sendLivePhoto
import dev.inmo.tgbotapi.extensions.api.send.media.sendMediaGroup import dev.inmo.tgbotapi.extensions.api.send.media.sendMediaGroup
import dev.inmo.tgbotapi.extensions.api.send.media.sendPaidMedia import dev.inmo.tgbotapi.extensions.api.send.media.sendPaidMedia
@@ -24,7 +23,6 @@ import dev.inmo.tgbotapi.extensions.utils.photoFileOrNull
import dev.inmo.tgbotapi.extensions.utils.videoContentOrNull import dev.inmo.tgbotapi.extensions.utils.videoContentOrNull
import dev.inmo.tgbotapi.extensions.utils.videoFileOrNull import dev.inmo.tgbotapi.extensions.utils.videoFileOrNull
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
import dev.inmo.tgbotapi.types.message.content.LivePhotoContent import dev.inmo.tgbotapi.types.message.content.LivePhotoContent
import dev.inmo.tgbotapi.types.message.payments.PaidMedia import dev.inmo.tgbotapi.types.message.payments.PaidMedia
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
@@ -92,42 +90,34 @@ suspend fun main(vararg args: String) {
) )
println(" sent message id: ${sent.messageId}") println(" sent message id: ${sent.messageId}")
// Download both Live Photo components once. ktgbotapi 37.0.0 collects the secondary `photo` // InputPaidMediaLivePhoto (TelegramPaidMediaLivePhoto): send the live photo as paid media (1 star)
// MultipartFile alongside the main file for edits, media groups, and paid-media requests.
val livePhotoBytes = downloadFile(livePhotoFile)
val coverPhotoBytes = livePhotoFile.photo?.let { downloadFile(it) }
// editMessageMedia with InputMediaLivePhoto (TelegramMediaLivePhoto):
// edit the previously sent message using newly uploaded main and cover files.
val sentAsMedia = sent.withContentOrNull<LivePhotoContent>()
if (sentAsMedia != null) {
editMessageMedia(
message = sentAsMedia,
media = TelegramMediaLivePhoto(
file = livePhotoBytes.asMultipartFile("edited-live-photo.mp4"),
photo = coverPhotoBytes?.asMultipartFile("edited-live-photo-cover.jpg")
?: livePhotoFile.photo?.fileId
?: livePhotoFile.fileId,
text = "Edited with newly uploaded Live Photo files"
)
)
}
// InputPaidMediaLivePhoto (TelegramPaidMediaLivePhoto): upload both files as paid media (1 star).
// Telegram currently restricts sendPaidMedia to channel chats.
sendPaidMedia( sendPaidMedia(
chatId = message.chat.id, chatId = message.chat.id,
starCount = 1, starCount = 1,
media = listOf( media = listOf(
// TelegramPaidMediaLivePhoto is InputPaidMediaLivePhoto
TelegramPaidMediaLivePhoto( TelegramPaidMediaLivePhoto(
file = livePhotoBytes.asMultipartFile("paid-live-photo.mp4"), file = livePhotoFile.fileId,
photo = coverPhotoBytes?.asMultipartFile("paid-live-photo-cover.jpg") photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId
?: livePhotoFile.photo?.fileId
?: livePhotoFile.fileId
) )
), ),
text = "Paid live photo uploaded as new files (1 star)" text = "Paid live photo (1 star)"
) )
// editMessageMedia with InputMediaLivePhoto (TelegramMediaLivePhoto):
// edit the previously sent message to replace it with itself via TelegramMediaLivePhoto
val sentAsMedia = sent.withContentOrNull<LivePhotoContent>()
if (sentAsMedia != null) {
editMessageMedia(
message = sentAsMedia,
// TelegramMediaLivePhoto is InputMediaLivePhoto
media = TelegramMediaLivePhoto(
file = livePhotoFile.fileId,
photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId,
text = "Edited via editMessageMedia with TelegramMediaLivePhoto"
)
)
}
} }
// Demonstrates: sendMediaGroup with live photos, InputMediaLivePhoto (TelegramMediaLivePhoto) // Demonstrates: sendMediaGroup with live photos, InputMediaLivePhoto (TelegramMediaLivePhoto)
@@ -138,17 +128,15 @@ suspend fun main(vararg args: String) {
println(" - fileId: ${livePhotoFile.fileId}, ${livePhotoFile.width}x${livePhotoFile.height}") println(" - fileId: ${livePhotoFile.fileId}, ${livePhotoFile.width}x${livePhotoFile.height}")
} }
// sendMediaGroup with newly uploaded main and cover files for every TelegramMediaLivePhoto. // sendMediaGroup with TelegramMediaLivePhoto (InputMediaLivePhoto)
sendMediaGroup( sendMediaGroup(
chatId = mediaGroupContent.group.first().sourceMessage.chat.id, chatId = mediaGroupContent.group.first().sourceMessage.chat.id,
media = mediaGroupContent.group.mapIndexed { index, groupMember -> media = mediaGroupContent.group.map { groupMember ->
val livePhotoFile = groupMember.content.media val livePhotoFile = groupMember.content.media
val coverPhoto = livePhotoFile.photo // TelegramMediaLivePhoto is InputMediaLivePhoto — used here in sendMediaGroup
TelegramMediaLivePhoto( TelegramMediaLivePhoto(
file = downloadFile(livePhotoFile).asMultipartFile("gallery-live-photo-$index.mp4"), file = livePhotoFile.fileId,
photo = coverPhoto?.let { photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId
downloadFile(it).asMultipartFile("gallery-live-photo-cover-$index.jpg")
} ?: livePhotoFile.fileId
) )
} }
) )

View File

@@ -1,6 +1,6 @@
# TelegramBotAPI examples # TelegramBotAPI examples
Runnable examples for [TelegramBotAPI](https://github.com/InsanusMokrassar/TelegramBotAPI), currently targeting tgbotapi 37.0.0 and Telegram Bot API 10.3. Each module focuses on a small Telegram Bot API feature and has its own README with detailed behavior, setup, permissions, and optional arguments. Runnable examples for [TelegramBotAPI](https://github.com/InsanusMokrassar/TelegramBotAPI). Each module focuses on a small Telegram Bot API feature and has its own README with detailed behavior, setup, permissions, and optional arguments.
## Running an example ## Running an example
@@ -24,24 +24,24 @@ sudo apt install libcurl4-gnutls-dev
| [ChatAvatarSetter](ChatAvatarSetter/) | Sets a chat photo from an image sent to the bot. | `./gradlew :ChatAvatarSetter:run --args="<BOT_TOKEN>"` | | [ChatAvatarSetter](ChatAvatarSetter/) | Sets a chat photo from an image sent to the bot. | `./gradlew :ChatAvatarSetter:run --args="<BOT_TOKEN>"` |
| [ChatManagementBot](ChatManagementBot/) | Exercises member permissions, administrator queries, reaction deletion, and bot-to-bot messages. | `./gradlew :ChatManagementBot:run --args="<BOT_TOKEN> debug testServer"` | | [ChatManagementBot](ChatManagementBot/) | Exercises member permissions, administrator queries, reaction deletion, and bot-to-bot messages. | `./gradlew :ChatManagementBot:run --args="<BOT_TOKEN> debug testServer"` |
| [ChecklistsBot](ChecklistsBot/) | Receives and renders checklist messages and checklist service events. | `./gradlew :ChecklistsBot:run --args="<BOT_TOKEN> debug testServer"` | | [ChecklistsBot](ChecklistsBot/) | Receives and renders checklist messages and checklist service events. | `./gradlew :ChecklistsBot:run --args="<BOT_TOKEN> debug testServer"` |
| [CommunitiesBot](CommunitiesBot/) | Handles chat/community membership and user-from-community join events. | `./gradlew :CommunitiesBot:run --args="<BOT_TOKEN> debug testServer"` | | [CommunitiesBot](CommunitiesBot/) | Handles community join/leave events and inspects a chat's current community. | `./gradlew :CommunitiesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [CustomBot](CustomBot/) | Provides a diagnostics playground for contexts, request logging, profile audio, and Stars balance. | `./gradlew :CustomBot:run --args="<BOT_TOKEN> debug testServer"` | | [CustomBot](CustomBot/) | Provides a diagnostics playground for contexts, request logging, profile audio, and Stars balance. | `./gradlew :CustomBot:run --args="<BOT_TOKEN> debug testServer"` |
| [DeepLinksBot](DeepLinksBot/) | Generates bot deep links and consumes their start payloads. | `./gradlew :DeepLinksBot:run --args="<BOT_TOKEN>"` | | [DeepLinksBot](DeepLinksBot/) | Generates bot deep links and consumes their start payloads. | `./gradlew :DeepLinksBot:run --args="<BOT_TOKEN>"` |
| [DraftsBot](DraftsBot/) | Streams empty or stoppable message drafts and handles generation-stopped updates. | `./gradlew :DraftsBot:run --args="<BOT_TOKEN>"` | | [DraftsBot](DraftsBot/) | Streams message drafts before sending the completed message. | `./gradlew :DraftsBot:run --args="<BOT_TOKEN>"` |
| [EphemeralMessagesBot](EphemeralMessagesBot/) | Sends, replaces, and edits rich, photo, or Live Photo ephemeral messages. | `./gradlew :EphemeralMessagesBot:run --args="<BOT_TOKEN> debug testServer"` | | [EphemeralMessagesBot](EphemeralMessagesBot/) | Sends ephemeral messages revealed through an inline button. | `./gradlew :EphemeralMessagesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [FSMBot](FSMBot/) | Implements a conversational finite-state machine with chat-scoped in-memory state. | `./gradlew :FSMBot:run --args="<BOT_TOKEN>"` | | [FSMBot](FSMBot/) | Implements a conversational finite-state machine with chat-scoped in-memory state. | `./gradlew :FSMBot:run --args="<BOT_TOKEN>"` |
| [FilesLoaderBot](FilesLoaderBot/) | Downloads incoming media to disk and sends it back to the chat. | `./gradlew :FilesLoaderBot:run --args="<BOT_TOKEN> <OUTPUT_DIRECTORY>"` | | [FilesLoaderBot](FilesLoaderBot/) | Downloads incoming media to disk and sends it back to the chat. | `./gradlew :FilesLoaderBot:run --args="<BOT_TOKEN> <OUTPUT_DIRECTORY>"` |
| [ForwardInfoSenderBot](ForwardInfoSenderBot/) | Reports the forward-origin metadata of received content. | `./gradlew :ForwardInfoSenderBot:run --args="<BOT_TOKEN>"` | | [ForwardInfoSenderBot](ForwardInfoSenderBot/) | Reports the forward-origin metadata of received content. | `./gradlew :ForwardInfoSenderBot:run --args="<BOT_TOKEN>"` |
| [GiftsBot](GiftsBot/) | Lists owned gifts and renders unique-gift service-message metadata. | `./gradlew :GiftsBot:run --args="<BOT_TOKEN> debug testServer"` | | [GiftsBot](GiftsBot/) | Paginates and lists gifts owned by a user or chat. | `./gradlew :GiftsBot:run --args="<BOT_TOKEN> debug testServer"` |
| [GiveawaysBot](GiveawaysBot/) | Logs giveaway creation, completion, and winner events. | `./gradlew :GiveawaysBot:run --args="<BOT_TOKEN> debug testServer"` | | [GiveawaysBot](GiveawaysBot/) | Logs giveaway creation, completion, and winner events. | `./gradlew :GiveawaysBot:run --args="<BOT_TOKEN> debug testServer"` |
| [GuestQueryBot](GuestQueryBot/) | Handles guest queries in chats where the bot is not a member. | `./gradlew :GuestQueryBot:run --args="<BOT_TOKEN> debug testServer"` | | [GuestQueryBot](GuestQueryBot/) | Handles guest queries in chats where the bot is not a member. | `./gradlew :GuestQueryBot:run --args="<BOT_TOKEN> debug testServer"` |
| [HelloBot](HelloBot/) | Greets users, groups, channels, or business chats when mentioned. | `./gradlew :HelloBot:run --args="<BOT_TOKEN>"` | | [HelloBot](HelloBot/) | Greets users, groups, channels, or business chats when mentioned. | `./gradlew :HelloBot:run --args="<BOT_TOKEN>"` |
| [InlineQueriesBot](InlineQueriesBot/) | Answers inline queries and supplies a deep-link result. | `./gradlew :InlineQueriesBot:runJvm --args="<BOT_TOKEN>"` | | [InlineQueriesBot](InlineQueriesBot/) | Answers inline queries and supplies a deep-link result. | `./gradlew :InlineQueriesBot:runJvm --args="<BOT_TOKEN>"` |
| [JoinRequestQueriesBot](JoinRequestQueriesBot/) | Queues or approves join requests, optionally delegating the decision to a Web App. | `./gradlew :JoinRequestQueriesBot:run --args="<BOT_TOKEN> https://example.com/verify debug testServer"` | | [JoinRequestQueriesBot](JoinRequestQueriesBot/) | Queues or approves join requests, optionally delegating the decision to a Web App. | `./gradlew :JoinRequestQueriesBot:run --args="<BOT_TOKEN> https://example.com/verify debug testServer"` |
| [KeyboardsBot](KeyboardsBot/) | Demonstrates reply, inline, disabled, forced-reply, paged, and inline-mode keyboards. | `./gradlew :KeyboardsBot:jvm_launcher:run --args="<BOT_TOKEN> debug"` | | [KeyboardsBot](KeyboardsBot/) | Demonstrates reply, inline, callback, paged, copy-text, and inline-mode keyboards. | `./gradlew :KeyboardsBot:jvm_launcher:run --args="<BOT_TOKEN> debug"` |
| [LinkPreviewsBot](LinkPreviewsBot/) | Sends the same link using multiple link-preview configurations. | `./gradlew :LinkPreviewsBot:run --args="<BOT_TOKEN> debug"` | | [LinkPreviewsBot](LinkPreviewsBot/) | Sends the same link using multiple link-preview configurations. | `./gradlew :LinkPreviewsBot:run --args="<BOT_TOKEN> debug"` |
| [LiveLocationsBot](LiveLocationsBot/) | Sends, updates, cancels, and stops a live-location message. | `./gradlew :LiveLocationsBot:run --args="<BOT_TOKEN>"` | | [LiveLocationsBot](LiveLocationsBot/) | Sends, updates, cancels, and stops a live-location message. | `./gradlew :LiveLocationsBot:run --args="<BOT_TOKEN>"` |
| [LivePhotosBot](LivePhotosBot/) | Receives, uploads, groups, edits, and sells Telegram Live Photos. | `./gradlew :LivePhotosBot:run --args="<BOT_TOKEN> debug testServer"` | | [LivePhotosBot](LivePhotosBot/) | Receives, sends, groups, edits, and sells Telegram Live Photos. | `./gradlew :LivePhotosBot:run --args="<BOT_TOKEN> debug testServer"` |
| [ManagedBotsBot](ManagedBotsBot/) † | Creates and administers managed bots and inspects personal-channel messages. | `./gradlew :ManagedBotsBot:run --args="<BOT_TOKEN> debug testServer"` | | [ManagedBotsBot](ManagedBotsBot/) † | Creates and administers managed bots and inspects personal-channel messages. | `./gradlew :ManagedBotsBot:run --args="<BOT_TOKEN> debug testServer"` |
| [MemberUpdatedWatcherBot](MemberUpdatedWatcherBot/) | Logs and reports bot/member status transitions in chats. | `./gradlew :MemberUpdatedWatcherBot:run --args="<BOT_TOKEN> debug"` | | [MemberUpdatedWatcherBot](MemberUpdatedWatcherBot/) | Logs and reports bot/member status transitions in chats. | `./gradlew :MemberUpdatedWatcherBot:run --args="<BOT_TOKEN> debug"` |
| [MyBot](MyBot/) † | Replaces or removes the bot's global profile photo and prints diagnostics. | `./gradlew :MyBot:run --args="<BOT_TOKEN> debug testServer"` | | [MyBot](MyBot/) † | Replaces or removes the bot's global profile photo and prints diagnostics. | `./gradlew :MyBot:run --args="<BOT_TOKEN> debug testServer"` |
@@ -49,8 +49,8 @@ sudo apt install libcurl4-gnutls-dev
| [RandomFileSenderBot](RandomFileSenderBot/) | Picks random local files and sends them individually or as media groups. | `./gradlew :RandomFileSenderBot:runJvm --args="<BOT_TOKEN> <FILES_DIRECTORY>"` | | [RandomFileSenderBot](RandomFileSenderBot/) | Picks random local files and sends them individually or as media groups. | `./gradlew :RandomFileSenderBot:runJvm --args="<BOT_TOKEN> <FILES_DIRECTORY>"` |
| [ReactionsInfoBot](ReactionsInfoBot/) | Handles per-user reaction changes and anonymous reaction-count updates. | `./gradlew :ReactionsInfoBot:run --args="<BOT_TOKEN> debug"` | | [ReactionsInfoBot](ReactionsInfoBot/) | Handles per-user reaction changes and anonymous reaction-count updates. | `./gradlew :ReactionsInfoBot:run --args="<BOT_TOKEN> debug"` |
| [ResenderBot](ResenderBot/) | Recreates received content while preserving reply, quote, effect, and business context. | `./gradlew :ResenderBot:jvm_launcher:run --args="<BOT_TOKEN> debug"` | | [ResenderBot](ResenderBot/) | Recreates received content while preserving reply, quote, effect, and business context. | `./gradlew :ResenderBot:jvm_launcher:run --args="<BOT_TOKEN> debug"` |
| [RichMessagesBot](RichMessagesBot/) | Demonstrates rich markup/blocks, buttons, documents, drafts, queries, and media. | `./gradlew :RichMessagesBot:run --args="<BOT_TOKEN> debug testServer"` | | [RichMessagesBot](RichMessagesBot/) | Demonstrates rich HTML/Markdown/blocks, streaming drafts, inline results, and media. | `./gradlew :RichMessagesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [RightsChangerBot](RightsChangerBot/) | Uses an FSM and inline keyboards to change member and administrator rights, including welcome messages. | `./gradlew :RightsChangerBot:run --args="<BOT_TOKEN> <ALLOWED_USER_ID> debug"` | | [RightsChangerBot](RightsChangerBot/) | Uses an FSM and inline keyboards to change member and administrator rights. | `./gradlew :RightsChangerBot:run --args="<BOT_TOKEN> <ALLOWED_USER_ID> debug"` |
| [SlotMachineDetectorBot](SlotMachineDetectorBot/) | Detects slot-machine dice and decodes their reel values. | `./gradlew :SlotMachineDetectorBot:run --args="<BOT_TOKEN>"` | | [SlotMachineDetectorBot](SlotMachineDetectorBot/) | Detects slot-machine dice and decodes their reel values. | `./gradlew :SlotMachineDetectorBot:run --args="<BOT_TOKEN>"` |
| [StarTransactionsBot](StarTransactionsBot/) | Demonstrates Stars invoices, transaction history, paid media, and refunds. | `./gradlew :StarTransactionsBot:run --args="<BOT_TOKEN> <ADMIN_USER_ID> debug testServer"` | | [StarTransactionsBot](StarTransactionsBot/) | Demonstrates Stars invoices, transaction history, paid media, and refunds. | `./gradlew :StarTransactionsBot:run --args="<BOT_TOKEN> <ADMIN_USER_ID> debug testServer"` |
| [StickerInfoBot](StickerInfoBot/) † | Looks up sticker-set metadata for stickers and custom emoji. | `./gradlew :StickerInfoBot:jvm_launcher:run --args="<BOT_TOKEN>"` | | [StickerInfoBot](StickerInfoBot/) † | Looks up sticker-set metadata for stickers and custom emoji. | `./gradlew :StickerInfoBot:jvm_launcher:run --args="<BOT_TOKEN>"` |

View File

@@ -3,13 +3,11 @@
RichMessagesBot is a long-polling showcase of Telegram rich messages. It sends RichMessagesBot is a long-polling showcase of Telegram rich messages. It sends
rich content from HTML, Markdown, and the typed `InputRichMessageBlocks` DSL; rich content from HTML, Markdown, and the typed `InputRichMessageBlocks` DSL;
streams drafts; edits rich text; handles incoming rich messages; and supplies streams drafts; edits rich text; handles incoming rich messages; and supplies
rich content from inline and guest queries. Bot API 10.3 coverage includes rich rich content from inline and guest queries.
buttons, compact tables, expandable quotations, document blocks, document
references, direct document uploads, and stoppable drafts.
## Commands ## Commands
The bot installs eight commands in Telegram's default command menu. Three The bot installs seven commands in Telegram's default command menu. Three
additional handlers can be invoked by typing their commands manually. additional handlers can be invoked by typing their commands manually.
| Command | In menu | Demonstration | | Command | In menu | Demonstration |
@@ -20,24 +18,21 @@ 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_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_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_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_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 | Streams three Markdown revisions under draft ID `1`, one second apart, then sends a normal final rich message. |
| `/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 | Streams two draft-only `thinking()` blocks under draft ID `2`, one second apart, then sends a normal typed-block answer. |
| `/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`. | | `/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. | | `/wait_rich` | Yes | Prompts for a rich message, waits for the next matching content, and reports its block count. |
The two draft examples allocate a unique draft ID and subscribe to The two draft examples finalize by sending a new normal rich message; they do not
`waitMessageGenerationStopped` before sending the first revision. They finalize turn the draft itself into the final message. They use distinct fixed IDs (`1`
by sending a new normal rich message only if no matching stop update arrives; and `2`); concurrent runs of the same command in one chat reuse that command's
when stopped, they leave the retained draft untouched and log the event. ID.
## Other triggers ## Other triggers
| Trigger | Behavior | | 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 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 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`. | | 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. | | 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. |
@@ -55,9 +50,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 The source also shows the two library shapes used for rich media: an
`InputRichMessageMedia` mapping for markup references and first-class photo, `InputRichMessageMedia` mapping for markup references and first-class photo,
video, audio, voice-note, animation, document, collage, and slideshow blocks. video, audio, voice-note, animation, collage, and slideshow blocks. The library
The document trigger demonstrates the library collecting a multipart file nested can collect multipart files nested in a rich-message tree as `attach://` uploads,
in a rich-message tree as an `attach://` upload. although this example's running handlers use URLs or an existing file ID.
## Telegram setup and permissions ## Telegram setup and permissions
@@ -65,7 +60,7 @@ in a rich-message tree as an `attach://` upload.
control. control.
2. Start a private chat with the bot, or add it to a chat and allow it to send 2. Start a private chat with the bot, or add it to a chat and allow it to send
messages and media. messages and media.
3. To test ordinary photo, document, and rich-message triggers in a group, ensure Telegram 3. To test ordinary photo and rich-message triggers in a group, ensure Telegram
delivers non-command messages to the bot, for example by disabling Group delivers non-command messages to the bot, for example by disabling Group
Privacy Mode or making the bot an administrator. Privacy Mode or making the bot an administrator.
4. Enable Inline Mode in BotFather to exercise the inline-query results. 4. Enable Inline Mode in BotFather to exercise the inline-query results.

View File

@@ -5,18 +5,14 @@ import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands 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.reply
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessageDraft 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.expectations.waitRichMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling 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.onBaseInlineQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand 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.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.onPhoto
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onRichMessage import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onRichMessage
import dev.inmo.tgbotapi.extensions.utils.baseSentMessageUpdateOrNull import dev.inmo.tgbotapi.extensions.utils.baseSentMessageUpdateOrNull
@@ -25,19 +21,16 @@ import dev.inmo.tgbotapi.extensions.utils.onlyRichMessageContentMessages
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.requests.edit.text.EditChatMessageRichText import dev.inmo.tgbotapi.requests.edit.text.EditChatMessageRichText
import dev.inmo.tgbotapi.requests.abstracts.InputFile 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.BotCommand
import dev.inmo.tgbotapi.types.CustomEmojiId import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputRichMessageContent import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputRichMessageContent
import dev.inmo.tgbotapi.types.InlineQueryId import dev.inmo.tgbotapi.types.InlineQueryId
import dev.inmo.tgbotapi.types.TelegramDate 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.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
import dev.inmo.tgbotapi.types.media.TelegramMediaAnimation import dev.inmo.tgbotapi.types.media.TelegramMediaAnimation
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio 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.TelegramMediaPhoto
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
@@ -47,31 +40,19 @@ import dev.inmo.tgbotapi.types.rich.InputRichMessageHTML
import dev.inmo.tgbotapi.types.rich.InputRichMessageMarkdown import dev.inmo.tgbotapi.types.rich.InputRichMessageMarkdown
import dev.inmo.tgbotapi.types.rich.InputRichMessageMedia import dev.inmo.tgbotapi.types.rich.InputRichMessageMedia
import dev.inmo.tgbotapi.types.rich.RichBlockCaption 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.RichBlockTableCellAlign
import dev.inmo.tgbotapi.types.rich.RichBlockTableCellVAlign 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.RichTextPlain
import dev.inmo.tgbotapi.types.rich.buildRichText 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.types.toChatId
import dev.inmo.tgbotapi.utils.DraftIdAllocator
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull 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. * Runs a long-polling showcase of the rich-message APIs introduced in Telegram Bot API 10.1 and 10.2.
* *
* Outgoing [dev.inmo.tgbotapi.types.rich.InputRichMessage] values use one of three representations: * Outgoing [dev.inmo.tgbotapi.types.rich.InputRichMessage] values use one of three representations:
* [InputRichMessageHTML], [InputRichMessageMarkdown], or a typed [InputRichMessageBlocks] tree of * [InputRichMessageHTML], [InputRichMessageMarkdown], or a typed [InputRichMessageBlocks] tree of
@@ -79,9 +60,7 @@ private val richDraftIds = DraftIdAllocator()
* [sendRichMessageDraft] revisions sharing a draft ID (including draft-only `thinking()` blocks), and edits * [sendRichMessageDraft] revisions sharing a draft ID (including draft-only `thinking()` blocks), and edits
* through [EditChatMessageRichText]. Media is shown both as [InputRichMessageMedia] references such as * 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 * `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, * multipart files inside an input tree into `attach://` uploads.
* 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 * Incoming [dev.inmo.tgbotapi.types.message.content.RichMessageContent] and user-selected content covers
* [onRichMessage], [waitRichMessage], [onlyRichMessageContentMessages], photo reuse, and * [onRichMessage], [waitRichMessage], [onlyRichMessageContentMessages], photo reuse, and
@@ -713,40 +692,20 @@ suspend fun main(vararg args: String) {
// sendRichMessageDraft: stream partial rich messages sharing one draftId, then finalize // sendRichMessageDraft: stream partial rich messages sharing one draftId, then finalize
// with a full sendRichMessage. Emulates streaming of an AI-generated reply. // with a full sendRichMessage. Emulates streaming of an AI-generated reply.
onCommand("rich_draft", initialFilter = { it.chat is PrivateChat }) { origin -> onCommand("rich_draft") {
val chatId = origin.chat.id.toChatId() val chatId = it.chat.id.toChatId()
val draftId = richDraftIds.allocate() val draftId = 1L
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
waitMessageGenerationStopped()
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
.first()
}
val parts = listOf( val parts = listOf(
"Thinking", "Thinking",
"Thinking about *rich* messages", "Thinking about *rich* messages",
"Thinking about *rich* messages and how to _stream_ them" "Thinking about *rich* messages and how to _stream_ them"
) )
try { parts.forEach { part ->
parts.forEach { part -> sendRichMessageDraft(chatId, draftId, InputRichMessageMarkdown(part))
sendRichMessageDraft( delay(1000)
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 // EditChatMessageRichText: send a rich message, then edit it with new rich content
@@ -813,146 +772,27 @@ suspend fun main(vararg args: String) {
) )
} }
// === 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), "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 // 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. // 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 -> onCommand("rich_blocks_draft") {
val chatId = origin.chat.id.toChatId() val chatId = it.chat.id.toChatId()
val draftId = richDraftIds.allocate() val draftId = 2L
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) { listOf("Analyzing your request", "Composing a structured answer").forEach { step ->
waitMessageGenerationStopped() sendRichMessageDraft(
.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, chatId,
InputRichMessageBlocks { draftId,
heading("Answer", level = 2) InputRichMessageBlocks { thinking(step) }
paragraph("Here is the finalized, structured reply.")
}
) )
} finally { delay(1000)
stoppedUpdate.cancel()
richDraftIds.free(draftId)
} }
// finalize the streamed draft with the real (non-thinking) blocks
sendRichMessage(
chatId,
InputRichMessageBlocks {
heading("Answer", level = 2)
paragraph("Here is the finalized, structured reply.")
}
)
} }
// Rich message media: send me a photo and it gets embedded into a rich message two ways. // Rich message media: send me a photo and it gets embedded into a rich message two ways.
@@ -988,40 +828,6 @@ 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 // waitRichMessage expectation: wait for the user to send a rich message
onCommand("wait_rich") { onCommand("wait_rich") {
reply(it, "Send me a rich message now") reply(it, "Send me a rich message now")
@@ -1107,7 +913,6 @@ suspend fun main(vararg args: String) {
BotCommand("rich_html", "Send a rich message described with HTML"), BotCommand("rich_html", "Send a rich message described with HTML"),
BotCommand("rich_markdown", "Send a rich message described with Markdown"), BotCommand("rich_markdown", "Send a rich message described with Markdown"),
BotCommand("rich_blocks", "Send a rich message built from the InputRichBlocks DSL"), 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_draft", "Stream a rich message draft, then finalize it"),
BotCommand("rich_blocks_draft", "Stream a blocks draft with thinking(), 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("rich_edit", "Send a rich message and edit it with new rich content"),

View File

@@ -19,7 +19,7 @@ Send `/rights_in_channel` in a private chat with the bot. Telegram's chat/user r
1. Select a channel where the bot is already a member. The picker requests `can_promote_members` and `can_restrict_members` for both the bot and the selecting user. 1. Select a channel where the bot is already a member. The picker requests `can_promote_members` and `can_restrict_members` for both the bot and the selecting user.
2. Select a user, or send `/cancel` during either selection step. 2. Select a user, or send `/cancel` during either selection step.
3. If the selected user is currently a channel administrator, the bot sends a keyboard for refreshing or toggling post-message, edit-message, delete-message, post-story, edit-story, delete-story, and Bot API 10.3's send-welcome-messages rights. 3. If the selected user is currently a channel administrator, the bot sends a keyboard for refreshing or toggling post-message, edit-message, delete-message, post-story, edit-story, and delete-story rights.
Selecting a non-administrator ends the flow without a message. A stale keyboard whose target is later demoted can display **Promote to admin**; that action promotes the target with the post-messages right enabled. Telegram allows the bot to change an administrator's rights only when the bot promoted that administrator itself. Selecting a non-administrator ends the flow without a message. A stale keyboard whose target is later demoted can display **Promote to admin**; that action promotes the target with the post-messages right enabled. Telegram allows the bot to change an administrator's rights only when the bot promoted that administrator itself.
@@ -29,7 +29,7 @@ Callbacks are processed only when clicked by the configured allowed user. They a
- Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token. - Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
- Choose one trusted Telegram numeric user ID as `ALLOWED_USER_ID`. `/simple` and all permission-changing callbacks are restricted to this ID. - Choose one trusted Telegram numeric user ID as `ALLOWED_USER_ID`. `/simple` and all permission-changing callbacks are restricted to this ID.
- Promote the bot in managed groups/channels. It needs `can_restrict_members` for member permissions and `can_promote_members` for channel administrator rights, plus enough rights to read member state and send/edit its keyboard messages. To grant `can_send_welcome_messages`, the bot must possess that right itself. It cannot grant rights it does not possess. - Promote the bot in managed groups/channels. It needs `can_restrict_members` for member permissions and `can_promote_members` for channel administrator rights, plus enough rights to read member state and send/edit its keyboard messages. It cannot grant rights it does not possess.
- BotFather privacy mode can remain enabled because the group workflows use commands, replies, and callbacks. - BotFather privacy mode can remain enabled because the group workflows use commands, replies, and callbacks.
- Treat console output as sensitive: the example prints every raw update, and handler failures print stack traces. - Treat console output as sensitive: the example prints every raw update, and handler failures print stack traces.

View File

@@ -116,7 +116,6 @@ suspend fun main(args: Array<String>) {
val editStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_stories" val editStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_stories"
val deleteStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_stories" val deleteStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_stories"
val postStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_post_stories" val postStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_post_stories"
val sendWelcomeMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_send_welcome_messages"
suspend fun BehaviourContext.getUserChatPermissions(chatId: ChatId, userId: UserId): ChatPermissions? { suspend fun BehaviourContext.getUserChatPermissions(chatId: ChatId, userId: UserId): ChatPermissions? {
val chatMember = getChatMember(chatId, userId) val chatMember = getChatMember(chatId, userId)
@@ -183,12 +182,6 @@ suspend fun main(args: Array<String>) {
row { row {
dataButton("Post stories${permissions.canPostStories.allowedSymbol()}", "$postStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}") dataButton("Post stories${permissions.canPostStories.allowedSymbol()}", "$postStoriesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
} }
row {
dataButton(
"Send welcome messages${permissions.canSendWelcomeMessages.allowedSymbol()}",
"$sendWelcomeMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}"
)
}
} ?: row { } ?: row {
dataButton("Promote to admin", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}") dataButton("Promote to admin", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
} }
@@ -422,7 +415,6 @@ suspend fun main(args: Array<String>) {
canEditStories = asAdmin ?.canEditStories.toggleIfData(editStoriesToggleAdminRightsData), canEditStories = asAdmin ?.canEditStories.toggleIfData(editStoriesToggleAdminRightsData),
canDeleteStories = asAdmin ?.canDeleteStories.toggleIfData(deleteStoriesToggleAdminRightsData), canDeleteStories = asAdmin ?.canDeleteStories.toggleIfData(deleteStoriesToggleAdminRightsData),
canPostStories = asAdmin ?.canPostStories.toggleIfData(postStoriesToggleAdminRightsData), canPostStories = asAdmin ?.canPostStories.toggleIfData(postStoriesToggleAdminRightsData),
canSendWelcomeMessages = asAdmin ?.canSendWelcomeMessages.toggleIfData(sendWelcomeMessagesToggleAdminRightsData),
) )
} }

View File

@@ -6,7 +6,7 @@ kotlin.daemon.jvmargs=-Xmx3g -Xms500m
kotlin_version=2.3.20 kotlin_version=2.3.20
telegram_bot_api_version=37.0.0 telegram_bot_api_version=36.1.0
micro_utils_version=0.29.1 micro_utils_version=0.29.1
serialization_version=1.10.0 serialization_version=1.10.0
ktor_version=3.4.1 ktor_version=3.4.1