Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions CommunitiesBot/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand Down
29 changes: 22 additions & 7 deletions CommunitiesBot/src/main/kotlin/CommunitiesBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
35 changes: 24 additions & 11 deletions DraftsBot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,41 @@

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

- `/test_draft_flow` publishes progressively longer 50-character prefixes every
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.

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.
- `/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
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.
Expand All @@ -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.
2 changes: 1 addition & 1 deletion DraftsBot/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ buildscript {
apply plugin: 'kotlin'
apply plugin: 'application'

mainClassName="TopicsHandlingKt"
mainClassName="DraftsBotKt"


dependencies {
Expand Down
68 changes: 65 additions & 3 deletions DraftsBot/src/main/kotlin/DraftsBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*
Expand All @@ -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> {
Expand All @@ -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> {
Expand All @@ -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)
Expand Down
19 changes: 14 additions & 5 deletions EphemeralMessagesBot/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading
Loading