Compare commits

..

1 Commits

Author SHA1 Message Date
447de0c3ce update examples for ktgbotapi 37.0.0 2026-08-30 17:44:15 +06:00
19 changed files with 603 additions and 138 deletions

View File

@@ -1,6 +1,6 @@
# CommunitiesBot
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.
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.
## Behavior, commands, and triggers
@@ -10,18 +10,21 @@ 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_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. |
| `/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. Its initial waiting reply currently says "added." |
| `/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_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.
## API concepts
- `CommunityChatAdded` carries a `Community` with a `CommunityId` and name; `CommunityChatRemoved` carries no fields.
- `onCommunityChatAdded` and `onCommunityChatRemoved` provide typed handlers for the service events.
- `CommunityChatJoined` carries the community through which a user joined the current chat.
- `onCommunityChatAdded`, `onCommunityChatRemoved`, and `onCommunityChatJoined` provide typed handlers for the service events.
- `getChat(...).community` exposes the nullable community on `ExtendedChat` without a subtype cast.
- `waitCommunityChatAddedEventsMessages` and `waitCommunityChatRemovedEventsMessages` expose typed event-message flows.
- `waitCommunityChatAddedEventsMessages`, `waitCommunityChatRemovedEventsMessages`, and `waitCommunityChatJoinedEventsMessages` expose typed event-message flows.
## 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.send.reply
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.waitCommunityChatRemoved
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatJoinedEventsMessages
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatRemovedEventsMessages
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.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.utils.extensions.sameChat
import kotlinx.coroutines.CoroutineScope
@@ -25,10 +25,11 @@ import kotlinx.coroutines.flow.first
* Starts a long-polling bot that demonstrates Telegram Communities.
*
* [onCommunityChatAdded] receives the joined [dev.inmo.tgbotapi.types.communities.Community], while
* [onCommunityChatRemoved] receives a fieldless removal event. `/community` reads
* [dev.inmo.tgbotapi.types.chat.ExtendedChat.community] with [getChat]. The two wait commands use
* [waitCommunityChatAddedEventsMessages] and [waitCommunityChatRemovedEventsMessages] to take the first same-chat
* event without a timeout. The bot prints its [getMe] result and every received update.
* [onCommunityChatRemoved] receives a fieldless removal event, while [onCommunityChatJoined] reports a user joining
* the chat from a community. `/community` reads
* [dev.inmo.tgbotapi.types.chat.ExtendedChat.community] with [getChat]. The three wait commands use typed event-message
* expectations to take the first same-chat event without a timeout. The bot prints its [getMe] result and every
* received update.
*
* @param args the bot token followed by the optional, case-sensitive `debug` and `testServer` flags; unknown trailing
* arguments are ignored
@@ -72,6 +73,13 @@ suspend fun main(vararg args: String) {
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
onCommand("community") {
val community = getChat(it.chat.id).community
@@ -94,11 +102,18 @@ suspend fun main(vararg args: String) {
// Suspend until the next community-removed event message from this chat.
onCommand("wait_community_removed") { origin ->
reply(origin, "Waiting for this chat to be added to a community...")
reply(origin, "Waiting for this chat to be removed from a community...")
waitCommunityChatRemovedEventsMessages().filter { it.sameChat(origin) }.first()
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) {
println(it)
}

View File

@@ -2,7 +2,8 @@
DraftsBot demonstrates streaming a message draft before sending the finished
message. It receives updates through long polling and uses the same built-in
Lorem ipsum text for both examples.
Lorem ipsum text for all examples. Bot API 10.3's stoppable generation controls
and `stopped_message_generation` updates are included.
## Commands
@@ -10,18 +11,32 @@ Lorem ipsum text for both examples.
500 ms, then sends the complete text as a normal message.
- `/test_empty_draft` first publishes an empty draft, waits 1.5 seconds, streams
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 both commands in Telegram's command menu for all group chats.
The handlers themselves are not restricted by chat type, so either command can
also be entered manually in a private chat.
The bot advertises all three commands in Telegram's command menu for private
chats and filters each handler to private chats, as required by Telegram's draft
methods.
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
1. Obtain a bot token and keep it out of source control.
2. Start a private chat with the bot, or add it to a group where you want to run
the example.
3. In groups, allow the bot to send messages. No administrator rights are
otherwise required by this example.
2. Start a private chat with the bot. Telegram doesn't accept a group or channel
ID for `sendMessageDraft`.
3. No administrator rights are required by this example.
The first program argument is required and must be the bot token. Omitting it
causes startup to fail; any later arguments are ignored.
@@ -34,8 +49,6 @@ From the repository root, run:
./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
are printed with their stack traces, and HTTP request, socket, and connection
timeouts are each configured to 30 seconds.

View File

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

View File

@@ -12,7 +12,9 @@ import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlow
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts
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.onMessageGenerationStopped
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.onForumTopicEdited
@@ -30,18 +32,26 @@ import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.ForumTopic
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.commands.BotCommandScope
import dev.inmo.tgbotapi.utils.DraftIdAllocator
import io.ktor.client.plugins.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeoutOrNull
/** Sample text streamed as a draft and then sent as the completed message. */
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.
"""
private val stoppableDraftIds = DraftIdAllocator()
/**
* Starts DraftsBot with long polling and registers the draft demonstration commands.
*
@@ -67,7 +77,7 @@ suspend fun main(vararg args: String) {
}
}
) {
onCommand("test_draft_flow") {
onCommand("test_draft_flow", initialFilter = { it.chat is PrivateChat }) {
sendMessageDraftFlowWithTexts(
it.chat.id,
flow<String> {
@@ -85,7 +95,7 @@ suspend fun main(vararg args: String) {
// 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
onCommand("test_empty_draft") {
onCommand("test_empty_draft", initialFilter = { it.chat is PrivateChat }) {
sendMessageDraftFlowWithTexts(
it.chat.id,
flow<String> {
@@ -103,10 +113,62 @@ suspend fun main(vararg args: String) {
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(
BotCommand("test_draft_flow", "Start draft testing with flow"),
BotCommand("test_empty_draft", "Draft starting from empty text (TG Bot API 9.0)"),
scope = BotCommandScope.AllGroupChats
BotCommand("test_stoppable_draft", "Stream a draft that the user can stop"),
scope = BotCommandScope.AllPrivateChats
)
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)

View File

@@ -1,22 +1,31 @@
# EphemeralMessagesBot
Demonstrates ephemeral messages: group messages that Telegram shows only to one receiver.
Demonstrates Telegram Bot API 10.2 and 10.3 ephemeral messages: group messages that Telegram shows only to one receiver.
## Behavior
- `/ephemeral` replies with a **Reveal a secret** inline button. The command is registered with Telegram as
an ephemeral command.
- Pressing the button (`reveal` callback data) sends a personal message visible only to the user who
pressed it. After three seconds the bot edits the message, then deletes it three seconds later.
- Pressing the button (`reveal` callback data) uses `EphemeralMessageParameters` to replace the callback-query
message with a personal rich message visible only to the user who pressed it. After three seconds the bot replaces
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
general `reply` API and one through the explicit `replyToEphemeral` API.
general `reply` API and one through the explicit `replyToEphemeral` API. The explicit form also uses
`EphemeralMessageParameters`.
- Updates and basic bot information are printed to standard output.
## Setup
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
supports ephemeral messages.
supports ephemeral messages. The photo demonstrations download all selected media into memory before uploading it
again.
## Run

View File

@@ -6,29 +6,47 @@ import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.deleteEphemeralMessage
import dev.inmo.tgbotapi.extensions.api.edit.text.editEphemeralMessageText
import dev.inmo.tgbotapi.extensions.api.edit.caption.editEphemeralMessageCaption
import dev.inmo.tgbotapi.extensions.api.edit.media.editEphemeralMessageMedia
import dev.inmo.tgbotapi.extensions.api.edit.text.editEphemeralMessageRichText
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.replyToEphemeral
import dev.inmo.tgbotapi.extensions.api.send.sendTextMessage
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage
import dev.inmo.tgbotapi.extensions.api.send.media.sendLivePhoto
import dev.inmo.tgbotapi.extensions.api.send.media.sendPhoto
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitLivePhotoMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitPhotoMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.fromUserMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.EphemeralMessageParameters
import dev.inmo.tgbotapi.types.ephemeralReplyReceiverUserIdOrNull
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
import dev.inmo.tgbotapi.types.message.abstracts.PossiblyEphemeralMessage
import dev.inmo.tgbotapi.types.rich.InputRichMessageBlocks
import korlibs.time.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
/**
* Runs the ephemeral-messages example bot using long polling.
*
* `/ephemeral` posts an inline button whose callback sends, edits, and deletes a message visible only to
* the user who pressed it. Incoming [PossiblyEphemeralMessage] instances receive both an automatic
* `/ephemeral` posts an inline button whose callback replaces it with a rich message visible only to
* the user who pressed it, edits that message, and deletes it. `/ephemeral_photo` demonstrates uploading new media
* to an ephemeral edit and moving its caption above the media. `/ephemeral_live_photo` demonstrates collecting both
* multipart files of a Live Photo edit. Incoming [PossiblyEphemeralMessage] instances receive both an automatic
* ephemeral [reply] and an explicit [replyToEphemeral].
*
* [args] must start with the bot token. The optional exact values `debug` and `testServer` respectively
@@ -68,18 +86,25 @@ suspend fun main(vararg args: String) {
)
}
// Send an ephemeral message in response to a callback query. `receiverUserId` + `callbackQueryId`
// make the outgoing message ephemeral — Telegram shows it only to the querying user (and this also
// serves as the answer to the callback query).
// Bot API 10.3 groups the recipient/callback fields in EphemeralMessageParameters. Setting
// replaceCallbackQueryMessage replaces the button message only for the user who pressed it.
onMessageDataCallbackQuery(Regex("reveal")) { query ->
val chatId = query.message.chat.id
val receiverUserId = query.from.id
val sent = sendTextMessage(
val sent = sendRichMessage(
chatId,
"🔒 ${query.from.firstName}, here is your personal secret: 42",
InputRichMessageBlocks {
paragraph {
plain("🔒 ${query.from.firstName}, here is your personal secret: ")
code("42")
}
},
ephemeralMessageParameters = EphemeralMessageParameters(
receiverUserId = receiverUserId,
callbackQueryId = query.id,
replaceCallbackQueryMessage = true,
),
)
// Only the group-family Common*ContentMessage types implement PossiblyEphemeralMessage, so the
@@ -87,14 +112,91 @@ suspend fun main(vararg args: String) {
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
if (ephemeralMessageId != null) {
delay(3.seconds)
// editEphemeralMessageText: address the ephemeral message by chatId + receiverUserId + ephemeralMessageId
editEphemeralMessageText(chatId, receiverUserId, ephemeralMessageId, "🔓 Revealed: the answer is 42")
// editEphemeralMessageText now accepts rich_message; the typed extension exposes that as
// editEphemeralMessageRichText.
editEphemeralMessageRichText(
chatId,
receiverUserId,
ephemeralMessageId,
InputRichMessageBlocks {
h2("Secret revealed")
paragraph { plain("The answer is "); code("42") }
},
)
delay(3.seconds)
// deleteEphemeralMessage: same addressing (there is also a PossiblyEphemeralMessage overload)
deleteEphemeralMessage(chatId, receiverUserId, ephemeralMessageId)
}
}
// Upload a received photo again as a brand-new multipart file while editing an ephemeral media message.
onCommand("ephemeral_photo") { origin ->
val receiverUserId = origin.fromUserMessageOrNull()?.user?.id ?: return@onCommand
reply(origin, "Send a photo. I will return it as ephemeral media and then re-upload it in an edit.")
val photoMessage = waitPhotoMessage().filter {
it.sameChat(origin) && it.fromUserMessageOrNull()?.user?.id == receiverUserId
}.first()
val sent = sendPhoto(
photoMessage.chat.id,
photoMessage.content.media.fileId,
text = "Ephemeral photo using its existing Telegram file ID",
ephemeralMessageParameters = EphemeralMessageParameters(receiverUserId),
)
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
?: return@onCommand
val photoBytes = downloadFile(photoMessage.content)
editEphemeralMessageMedia(
photoMessage.chat.id,
receiverUserId,
ephemeralMessageId,
TelegramMediaPhoto(photoBytes.asMultipartFile("ephemeral-photo.jpg")),
)
editEphemeralMessageCaption(
photoMessage.chat.id,
receiverUserId,
ephemeralMessageId,
caption = "This caption is above newly uploaded media",
showCaptionAboveMedia = true,
)
}
// A Live Photo has a main file plus a secondary `photo` file. ktgbotapi 37.0.0 includes both multipart
// attachments when EditEphemeralMessageMedia builds its request.
onCommand("ephemeral_live_photo") { origin ->
val receiverUserId = origin.fromUserMessageOrNull()?.user?.id ?: return@onCommand
reply(origin, "Send a Live Photo. I will send it ephemerally and edit it using two new uploads.")
val livePhotoMessage = waitLivePhotoMessage().filter {
it.sameChat(origin) && it.fromUserMessageOrNull()?.user?.id == receiverUserId
}.first()
val livePhoto = livePhotoMessage.content.media
val sent = sendLivePhoto(
chatId = livePhotoMessage.chat.id,
livePhoto = livePhoto,
text = "Ephemeral Live Photo using existing Telegram file IDs",
ephemeralMessageParameters = EphemeralMessageParameters(receiverUserId),
)
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
?: return@onCommand
val livePhotoBytes = downloadFile(livePhoto)
val coverPhotoBytes = livePhoto.photo?.let { downloadFile(it) }
editEphemeralMessageMedia(
livePhotoMessage.chat.id,
receiverUserId,
ephemeralMessageId,
TelegramMediaLivePhoto(
file = livePhotoBytes.asMultipartFile("ephemeral-live-photo.mp4"),
photo = coverPhotoBytes?.asMultipartFile("ephemeral-live-photo-cover.jpg")
?: livePhoto.photo?.fileId
?: livePhoto.fileId,
text = "Edited with newly uploaded main and cover files",
),
)
}
// Incoming ephemeral messages: detect them via PossiblyEphemeralMessage, then answer them.
onContentMessage { message ->
val ephemeral = (message as? PossiblyEphemeralMessage)?.takeIf { it.ephemeralMessageId != null }
@@ -109,7 +211,7 @@ suspend fun main(vararg args: String) {
if (receiverUserId != null) {
replyToEphemeral(
message.chat.id,
receiverUserId,
EphemeralMessageParameters(receiverUserId),
ephemeral.ephemeralMessageId!!,
"Explicit ephemeral reply via replyToEphemeral",
)
@@ -119,6 +221,8 @@ suspend fun main(vararg args: String) {
setMyCommands(
// isEphemeral marks a command whose response is an ephemeral (personal) message
BotCommand("ephemeral", "Post a button that reveals an ephemeral (personal) message", isEphemeral = true),
BotCommand("ephemeral_photo", "Re-upload a photo through an ephemeral media edit", isEphemeral = true),
BotCommand("ephemeral_live_photo", "Re-upload both files of an ephemeral Live Photo", isEphemeral = true),
)
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {

View File

@@ -15,12 +15,16 @@ 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
`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
- `/start` — lists the owned gifts selected by the current chat type. It must be the only command in the message and
takes no arguments.
Other commands and non-command messages are ignored.
Other commands and ordinary non-command messages are ignored; unique-gift service messages are handled separately.
## Setup and permissions

View File

@@ -10,10 +10,7 @@ import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
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.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.extensions.behaviour_builder.triggers_handling.onUniqueGiftSentOrReceived
import dev.inmo.tgbotapi.types.chat.BusinessChat
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.chat.PublicChat
@@ -22,6 +19,7 @@ import dev.inmo.tgbotapi.types.gifts.OwnedGift
import dev.inmo.tgbotapi.types.message.textsources.splitForText
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -30,7 +28,8 @@ import kotlinx.coroutines.Dispatchers
*
* 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
* are split across replies. The bot also prints its [getMe] result at startup.
* are split across replies. Unique-gift service messages also expose the Bot API 10.3 text, entities and privacy flag.
* The bot prints its [getMe] result at startup.
*
* @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags; unknown trailing
* arguments are ignored
@@ -55,6 +54,28 @@ suspend fun main(vararg args: String) {
val me = getMe()
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") {
val giftsFlow = when (val chat = it.chat) {
is BusinessChat -> {

View File

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

View File

@@ -1,6 +1,6 @@
# KeyboardsBot
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.
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.
## 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.
The generated inline keyboard contains:
The generated inline keyboard sets Bot API 10.3's `force_reply` field and contains:
- numbered buttons for the current page and any adjacent pages that are within `1..count`;
- a disabled button for the current page and callback buttons for adjacent pages within `1..count`;
- styled jump buttons for moving toward the first or last page when applicable;
- 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.
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. Ordinary non-command messages are ignored.
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.
### Inline mode

View File

@@ -8,17 +8,23 @@ The bot defines no commands. It prints every incoming update to standard output
| Trigger | Behavior |
| --- | --- |
| 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 and resends the gallery with `sendMediaGroup`. |
| 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. |
| Live Photo gallery | Logs every item, downloads each main and cover file, and re-uploads 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. |
| 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. |
## Live Photo handling
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.
The initial `sendLivePhoto` call reuses Telegram file IDs. The regular-media edit,
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, send paid media, then edit the resent message. Consequently, a failure while sending paid media prevents the edit for that update.
The standalone handler performs its requests in order: resend, download, edit,
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

View File

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

View File

@@ -1,6 +1,6 @@
# TelegramBotAPI examples
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.
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.
## 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>"` |
| [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"` |
| [CommunitiesBot](CommunitiesBot/) | Handles community join/leave events and inspects a chat's current community. | `./gradlew :CommunitiesBot: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"` |
| [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>"` |
| [DraftsBot](DraftsBot/) | Streams message drafts before sending the completed message. | `./gradlew :DraftsBot:run --args="<BOT_TOKEN>"` |
| [EphemeralMessagesBot](EphemeralMessagesBot/) | Sends ephemeral messages revealed through an inline button. | `./gradlew :EphemeralMessagesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [DraftsBot](DraftsBot/) | Streams empty or stoppable message drafts and handles generation-stopped updates. | `./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"` |
| [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>"` |
| [ForwardInfoSenderBot](ForwardInfoSenderBot/) | Reports the forward-origin metadata of received content. | `./gradlew :ForwardInfoSenderBot:run --args="<BOT_TOKEN>"` |
| [GiftsBot](GiftsBot/) | Paginates and lists gifts owned by a user or chat. | `./gradlew :GiftsBot:run --args="<BOT_TOKEN> debug testServer"` |
| [GiftsBot](GiftsBot/) | Lists owned gifts and renders unique-gift service-message metadata. | `./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"` |
| [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>"` |
| [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"` |
| [KeyboardsBot](KeyboardsBot/) | Demonstrates reply, inline, callback, paged, copy-text, and inline-mode keyboards. | `./gradlew :KeyboardsBot:jvm_launcher:run --args="<BOT_TOKEN> debug"` |
| [KeyboardsBot](KeyboardsBot/) | Demonstrates reply, inline, disabled, forced-reply, paged, 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"` |
| [LiveLocationsBot](LiveLocationsBot/) | Sends, updates, cancels, and stops a live-location message. | `./gradlew :LiveLocationsBot:run --args="<BOT_TOKEN>"` |
| [LivePhotosBot](LivePhotosBot/) | Receives, sends, groups, edits, and sells Telegram Live Photos. | `./gradlew :LivePhotosBot:run --args="<BOT_TOKEN> debug testServer"` |
| [LivePhotosBot](LivePhotosBot/) | Receives, uploads, 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"` |
| [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"` |
@@ -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>"` |
| [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"` |
| [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. | `./gradlew :RightsChangerBot:run --args="<BOT_TOKEN> <ALLOWED_USER_ID> debug"` |
| [RichMessagesBot](RichMessagesBot/) | Demonstrates rich markup/blocks, buttons, documents, drafts, queries, 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"` |
| [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"` |
| [StickerInfoBot](StickerInfoBot/) † | Looks up sticker-set metadata for stickers and custom emoji. | `./gradlew :StickerInfoBot:jvm_launcher:run --args="<BOT_TOKEN>"` |

View File

@@ -3,11 +3,13 @@
RichMessagesBot is a long-polling showcase of Telegram rich messages. It sends
rich content from HTML, Markdown, and the typed `InputRichMessageBlocks` DSL;
streams drafts; edits rich text; handles incoming rich messages; and supplies
rich content from inline and guest queries.
rich content from inline and guest queries. Bot API 10.3 coverage includes rich
buttons, compact tables, expandable quotations, document blocks, document
references, direct document uploads, and stoppable drafts.
## Commands
The bot installs seven commands in Telegram's default command menu. Three
The bot installs eight commands in Telegram's default command menu. Three
additional handlers can be invoked by typing their commands manually.
| Command | In menu | Demonstration |
@@ -18,21 +20,24 @@ additional handlers can be invoked by typing their commands manually.
| `/rich_markdown_blocks` | No | Sends and logs the full fixture as a typed block tree, including first-class media blocks, captions, collages, and slideshows. |
| `/rich_markdown_medialess_blocks` | No | Sends and logs the same typed block tree without its media section. |
| `/rich_blocks` | Yes | Sends a smaller, directly constructed block tree with headings, formatted paragraphs, ordered and unordered checkbox lists, a divider, preformatted Kotlin, and a quotation. |
| `/rich_draft` | Yes | Streams three Markdown revisions under draft ID `1`, one second apart, then sends a normal final rich message. |
| `/rich_blocks_draft` | Yes | Streams two draft-only `thinking()` blocks under draft ID `2`, one second apart, then sends a normal typed-block answer. |
| `/rich_10_3` | Yes | Sends an inline `RichTextButton`, three aligned `InputRichBlockButtons` rows with URL, callback, inline-query, copy-text, and disabled actions, a compact table, an expandable quotation, and a document block. |
| `/rich_draft` | Yes | In a private chat, streams three Markdown revisions with `canStop` and `keepOnStop`, one second apart, then sends a normal final rich message unless generation is stopped. |
| `/rich_blocks_draft` | Yes | In a private chat, streams two draft-only `thinking()` blocks with `canStop` and `keepOnStop`, then sends a normal typed-block answer unless generation is stopped. |
| `/rich_edit` | Yes | Sends a Markdown rich message, waits two seconds, and replaces its rich content with `EditChatMessageRichText`. |
| `/wait_rich` | Yes | Prompts for a rich message, waits for the next matching content, and reports its block count. |
The two draft examples finalize by sending a new normal rich message; they do not
turn the draft itself into the final message. They use distinct fixed IDs (`1`
and `2`); concurrent runs of the same command in one chat reuse that command's
ID.
The two draft examples allocate a unique draft ID and subscribe to
`waitMessageGenerationStopped` before sending the first revision. They finalize
by sending a new normal rich message only if no matching stop update arrives;
when stopped, they leave the retained draft untouched and log the event.
## Other triggers
| Trigger | Behavior |
| --- | --- |
| Any photo | Reuses the received Telegram file ID without downloading it. The bot first sends HTML whose `tg://photo?id=userphoto` reference is resolved by `InputRichMessageMedia`, then sends the same photo as a typed `photo()` block. |
| Any document | Reuses the received file ID through a `tg://document?id=userdocument` HTML reference, then downloads the file and sends it again as a multipart upload nested in a typed `document()` block. Large documents are therefore held in memory. |
| A `rich_10_3_callback` rich button | Answers the callback with a notification confirming that the rich-message button was received. |
| Any incoming rich message | Logs right-to-left state and every parsed block, replies with the block count, and resends the rich message with `createResend`. The `onlyRichMessageContentMessages` flow also logs its block count. |
| Any inline query | Returns uncached HTML and Markdown articles whose selected messages use `InputRichMessageContent`. |
| A text guest request containing `/rich_guest` | Returns one inline article containing the full Markdown fixture. This is a substring check, not a registered bot command. Non-text guest requests and text without that exact case-sensitive substring are ignored by this handler. |
@@ -50,9 +55,9 @@ Telegram file ID and assigning an alias for a `tg://photo?id=...` reference.
The source also shows the two library shapes used for rich media: an
`InputRichMessageMedia` mapping for markup references and first-class photo,
video, audio, voice-note, animation, collage, and slideshow blocks. The library
can collect multipart files nested in a rich-message tree as `attach://` uploads,
although this example's running handlers use URLs or an existing file ID.
video, audio, voice-note, animation, document, collage, and slideshow blocks.
The document trigger demonstrates the library collecting a multipart file nested
in a rich-message tree as an `attach://` upload.
## Telegram setup and permissions
@@ -60,7 +65,7 @@ although this example's running handlers use URLs or an existing file ID.
control.
2. Start a private chat with the bot, or add it to a chat and allow it to send
messages and media.
3. To test ordinary photo and rich-message triggers in a group, ensure Telegram
3. To test ordinary photo, document, and rich-message triggers in a group, ensure Telegram
delivers non-command messages to the bot, for example by disabling Group
Privacy Mode or making the bot an administrator.
4. Enable Inline Mode in BotFather to exercise the inline-query results.

View File

@@ -5,14 +5,18 @@ import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessage
import dev.inmo.tgbotapi.extensions.api.send.sendRichMessageDraft
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageGenerationStopped
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitRichMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onDocument
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGuestRequestMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onRichMessage
import dev.inmo.tgbotapi.extensions.utils.baseSentMessageUpdateOrNull
@@ -21,16 +25,19 @@ import dev.inmo.tgbotapi.extensions.utils.onlyRichMessageContentMessages
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.requests.edit.text.EditChatMessageRichText
import dev.inmo.tgbotapi.requests.abstracts.InputFile
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputRichMessageContent
import dev.inmo.tgbotapi.types.InlineQueryId
import dev.inmo.tgbotapi.types.TelegramDate
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
import dev.inmo.tgbotapi.types.media.TelegramMediaAnimation
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
@@ -40,19 +47,31 @@ import dev.inmo.tgbotapi.types.rich.InputRichMessageHTML
import dev.inmo.tgbotapi.types.rich.InputRichMessageMarkdown
import dev.inmo.tgbotapi.types.rich.InputRichMessageMedia
import dev.inmo.tgbotapi.types.rich.RichBlockCaption
import dev.inmo.tgbotapi.types.rich.RichBlockButtonAlignment
import dev.inmo.tgbotapi.types.rich.RichBlockTableCellAlign
import dev.inmo.tgbotapi.types.rich.RichBlockTableCellVAlign
import dev.inmo.tgbotapi.types.rich.RichMessageButton
import dev.inmo.tgbotapi.types.rich.RichMessageButtonStyle
import dev.inmo.tgbotapi.types.rich.RichTextPlain
import dev.inmo.tgbotapi.types.rich.buildRichText
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CopyTextButtonData
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.SwitchInlineQueryChosenChat
import dev.inmo.tgbotapi.types.toChatId
import dev.inmo.tgbotapi.utils.DraftIdAllocator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withTimeoutOrNull
private val richDraftIds = DraftIdAllocator()
/**
* Runs a long-polling showcase of the rich-message APIs introduced in Telegram Bot API 10.1 and 10.2.
* Runs a long-polling showcase of the rich-message APIs introduced in Telegram Bot API 10.1 through 10.3.
*
* Outgoing [dev.inmo.tgbotapi.types.rich.InputRichMessage] values use one of three representations:
* [InputRichMessageHTML], [InputRichMessageMarkdown], or a typed [InputRichMessageBlocks] tree of
@@ -60,7 +79,9 @@ import kotlinx.coroutines.flow.mapNotNull
* [sendRichMessageDraft] revisions sharing a draft ID (including draft-only `thinking()` blocks), and edits
* through [EditChatMessageRichText]. Media is shown both as [InputRichMessageMedia] references such as
* `tg://photo?id=...` and as typed blocks; [dev.inmo.tgbotapi.requests.send.SendRichMessage] also turns
* multipart files inside an input tree into `attach://` uploads.
* multipart files inside an input tree into `attach://` uploads. Bot API 10.3 button rows, inline rich-text buttons,
* compact tables, expandable quotations and document blocks are demonstrated by `/rich_10_3` and the document
* trigger.
*
* Incoming [dev.inmo.tgbotapi.types.message.content.RichMessageContent] and user-selected content covers
* [onRichMessage], [waitRichMessage], [onlyRichMessageContentMessages], photo reuse, and
@@ -692,20 +713,40 @@ suspend fun main(vararg args: String) {
// sendRichMessageDraft: stream partial rich messages sharing one draftId, then finalize
// with a full sendRichMessage. Emulates streaming of an AI-generated reply.
onCommand("rich_draft") {
val chatId = it.chat.id.toChatId()
val draftId = 1L
onCommand("rich_draft", initialFilter = { it.chat is PrivateChat }) { origin ->
val chatId = origin.chat.id.toChatId()
val draftId = richDraftIds.allocate()
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
waitMessageGenerationStopped()
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
.first()
}
val parts = listOf(
"Thinking",
"Thinking about *rich* messages",
"Thinking about *rich* messages and how to _stream_ them"
)
try {
parts.forEach { part ->
sendRichMessageDraft(chatId, draftId, InputRichMessageMarkdown(part))
delay(1000)
sendRichMessageDraft(
chatId,
draftId.long,
InputRichMessageMarkdown(part),
canStop = true,
keepOnStop = true,
)
val stopped = withTimeoutOrNull(1000L) { stoppedUpdate.await() }
if (stopped != null) {
println("Stopped rich draft ${stopped.draftId.long} in ${stopped.chat.id}")
return@onCommand
}
// finalize the streamed draft with the real message
}
// 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)
}
}
// EditChatMessageRichText: send a rich message, then edit it with new rich content
@@ -772,20 +813,135 @@ 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
// to stream a model's reasoning before the finalized rich message is sent via sendRichMessage.
onCommand("rich_blocks_draft") {
val chatId = it.chat.id.toChatId()
val draftId = 2L
onCommand("rich_blocks_draft", initialFilter = { it.chat is PrivateChat }) { origin ->
val chatId = origin.chat.id.toChatId()
val draftId = richDraftIds.allocate()
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
waitMessageGenerationStopped()
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
.first()
}
try {
listOf("Analyzing your request", "Composing a structured answer").forEach { step ->
sendRichMessageDraft(
chatId,
draftId,
InputRichMessageBlocks { thinking(step) }
draftId.long,
InputRichMessageBlocks { thinking(step) },
canStop = true,
keepOnStop = true,
)
delay(1000)
val stopped = withTimeoutOrNull(1000L) { stoppedUpdate.await() }
if (stopped != null) {
println("Stopped rich block draft ${stopped.draftId.long} in ${stopped.chat.id}")
return@onCommand
}
// finalize the streamed draft with the real (non-thinking) blocks
}
// Finalize only if the user did not stop generation; a normal message removes a retained draft.
sendRichMessage(
chatId,
InputRichMessageBlocks {
@@ -793,6 +949,10 @@ suspend fun main(vararg args: String) {
paragraph("Here is the finalized, structured reply.")
}
)
} finally {
stoppedUpdate.cancel()
richDraftIds.free(draftId)
}
}
// Rich message media: send me a photo and it gets embedded into a rich message two ways.
@@ -828,6 +988,40 @@ suspend fun main(vararg args: String) {
)
}
// A received document demonstrates both tg://document?id= references and a direct multipart upload.
onDocument { message ->
val reusedDocument = TelegramMediaDocument(message.content.media.fileId)
sendRichMessage(
message.chat.id,
InputRichMessageHTML(
"""
<h2>Your document, referenced from HTML</h2>
<tg-document src="tg://document?id=userdocument"></tg-document>
""".trimIndent(),
media = listOf(
InputRichMessageMedia(id = "userdocument", media = reusedDocument)
)
)
)
val uploadedDocument = TelegramMediaDocument(
downloadFile(message.content).asMultipartFile(
message.content.media.fileName ?: "document.bin"
)
)
sendRichMessage(
message.chat.id,
InputRichMessageBlocks {
h2("Your document, uploaded as a new file")
paragraph("SendRichMessage collects the MultipartFile nested in this document block.")
document(
uploadedDocument,
RichBlockCaption(RichTextPlain("Direct attach:// document upload")),
)
}
)
}
// waitRichMessage expectation: wait for the user to send a rich message
onCommand("wait_rich") {
reply(it, "Send me a rich message now")
@@ -913,6 +1107,7 @@ suspend fun main(vararg args: String) {
BotCommand("rich_html", "Send a rich message described with HTML"),
BotCommand("rich_markdown", "Send a rich message described with Markdown"),
BotCommand("rich_blocks", "Send a rich message built from the InputRichBlocks DSL"),
BotCommand("rich_10_3", "Show Bot API 10.3 rich blocks and buttons"),
BotCommand("rich_draft", "Stream a rich message draft, then finalize it"),
BotCommand("rich_blocks_draft", "Stream a blocks draft with thinking(), then finalize it"),
BotCommand("rich_edit", "Send a rich message and edit it with new rich content"),

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.
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, and delete-story 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, delete-story, and Bot API 10.3's send-welcome-messages 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.
@@ -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.
- 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. 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. To grant `can_send_welcome_messages`, the bot must possess that right itself. It cannot grant rights it does not possess.
- 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.

View File

@@ -116,6 +116,7 @@ suspend fun main(args: Array<String>) {
val editStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_edit_stories"
val deleteStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_delete_stories"
val postStoriesToggleAdminRightsData = "${adminRightsDataPrefix}_post_stories"
val sendWelcomeMessagesToggleAdminRightsData = "${adminRightsDataPrefix}_send_welcome_messages"
suspend fun BehaviourContext.getUserChatPermissions(chatId: ChatId, userId: UserId): ChatPermissions? {
val chatMember = getChatMember(chatId, userId)
@@ -182,6 +183,12 @@ suspend fun main(args: Array<String>) {
row {
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 {
dataButton("Promote to admin", "$postMessagesToggleAdminRightsData ${channelId.chatId} ${userId.chatId}")
}
@@ -415,6 +422,7 @@ suspend fun main(args: Array<String>) {
canEditStories = asAdmin ?.canEditStories.toggleIfData(editStoriesToggleAdminRightsData),
canDeleteStories = asAdmin ?.canDeleteStories.toggleIfData(deleteStoriesToggleAdminRightsData),
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
telegram_bot_api_version=36.1.0
telegram_bot_api_version=37.0.0
micro_utils_version=0.29.1
serialization_version=1.10.0
ktor_version=3.4.1