Skip to content
33 changes: 30 additions & 3 deletions BoostsInfoBot/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
# UserChatShared
# BoostsInfoBot

Showing info about boosts
A long-polling example bot that shows the boosts a user has added to a channel. It demonstrates Telegram's channel-request reply-keyboard button, the resulting `chat_shared` service message, the `getUserChatBoosts` Bot API method, and `chat_boost` updates.

## Behavior

1. Open a private chat with the bot and send `/start` (the command takes no arguments).
2. The bot replies with a **Click me :)** keyboard button. Pressing it opens Telegram's channel picker. The picker is restricted to channels where the bot is already a member.
3. After a channel is selected, Telegram sends its identifier to the bot in a `chat_shared` service message. The bot accepts only the response associated with its channel-request button (request ID `1`).
4. The bot calls `getUserChatBoosts` for the selected channel and the user who selected it. It replies with each boost's added and expiration dates plus the unformatted boost object.

If that user has no boosts in the channel, the bot says so. If Telegram rejects the request or another error occurs while obtaining the boosts, it replies with `Unable to take info about boosts in shared chat`.

Separately, every `chat_boost` update received while the bot is running is printed as an unformatted object to standard output. These updates represent boosts that were added or changed; removed-boost updates are not handled by this example. This console output is produced whether or not debug logging is enabled.

## Telegram setup and permissions

- Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
- Use `/start` in a private chat. Telegram's request-chat keyboard buttons are available only in private chats.
- Before selecting a channel, add the bot to it and promote it to administrator. The button requires the bot to be a member, but it does not request administrator rights. Telegram requires administrator rights both for `getUserChatBoosts` and for receiving `chat_boost` updates.
- The query returns only boosts added by the user interacting with the bot, not every boost on the selected channel.
- The example uses long polling and automatically deletes any existing webhook for the bot token at startup. Do not run another long-polling consumer for the same token at the same time.

## Launch

From the repository root, pass the bot token as the first application argument:

```bash
./gradlew :BoostsInfoBot:run --args="<BOT_TOKEN>"
```

An optional second argument, exactly `debug`, routes the library's default KSLog output to standard output:

```bash
../gradlew run --args="BOT_TOKEN"
./gradlew :BoostsInfoBot:run --args="<BOT_TOKEN> debug"
```
11 changes: 11 additions & 0 deletions BoostsInfoBot/src/main/kotlin/BoostsInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ import dev.inmo.tgbotapi.utils.regular
import korlibs.time.DateFormat
import korlibs.time.format

/**
* Starts the BoostsInfoBot example using long polling.
*
* The `/start` command sends a channel-request keyboard button that accepts channels where this bot is already a
* member. When Telegram returns the matching `chat_shared` service message, the bot calls [getUserChatBoosts] for
* the selected channel and the requesting user, then replies with that user's boosts. Incoming `chat_boost` updates
* are also printed to standard output.
*
* @param args the bot token as the first element and, optionally, `debug` as the second element to format and print
* default KSLog messages to standard output
*/
suspend fun main(args: Array<String>) {
val isDebug = args.getOrNull(1) == "debug"

Expand Down
77 changes: 77 additions & 0 deletions BotSubscriptionsBot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# BotSubscriptionsBot

Demonstrates the [`subscription`](https://core.telegram.org/bots/api#update) update added in Telegram Bot API
10.2. Telegram sends a [`BotSubscriptionUpdated`](https://core.telegram.org/bots/api#botsubscriptionupdated) when a
user cancels a recurring payment subscription to the bot, re-enables a canceled subscription, or a subscription
payment fails.

This example only observes subscription changes. It does not create an invoice or start a subscription.

## Behavior

At startup, the bot calls `getMe` and prints its own information. It then receives updates through long polling and
prints every received update object to standard output.

For each subscription update, the bot demonstrates three tgbotapi interfaces:

- `onBotSubscriptionUpdated` handles `BotSubscriptionUpdated` directly. It prints the user ID, invoice payload, and
typed state, then makes a best-effort attempt to notify that user in a private chat. A send failure is logged and
does not stop polling.
- `botSubscriptionUpdatedUpdatesFlow` exposes the underlying `BotSubscriptionUpdatedUpdate`; this example prints its
update ID, user ID, and state. Consequently, the same event appears in the typed-handler, subscription-flow, and
generic all-update logs.
- `waitBotSubscriptionUpdated().first()` waits for one matching event in the `/wait_subscription` command handler.

The known tgbotapi states are `Active`, `Canceled`, and `Failed`. Unknown state strings are preserved as `Unknown`, so
the example remains compatible if Telegram adds another state.

## Command

- `/wait_subscription` — a standalone command with no arguments. It replies that it is waiting, then waits without a
timeout for the next subscription update and replies in the command's chat with that update's state and invoice
payload. The update is not restricted to the command sender, so this unprotected diagnostic command should not be
copied into a production bot as-is.

There is no `/start` handler and the bot ignores other commands apart from printing their received update objects.

## Setup

1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
2. Use a complete payment implementation for that same bot to create a recurring Telegram Stars (`XTR`) invoice link
with [`createInvoiceLink`](https://core.telegram.org/bots/api#createinvoicelink) and a `subscription_period`
(currently 2,592,000 seconds, or 30 days), then let a user subscribe. This example has neither an invoice creator nor
a `pre_checkout_query` handler, so it cannot establish a new subscription by itself; it is intended to observe state
changes for subscriptions created through that payment flow.
3. Have each subscriber start the bot and leave its private chat unblocked if you want the direct status notification
to succeed.

No group or channel membership and no administrator permissions are required for bot payment subscriptions. If you
run `/wait_subscription` in a group, the bot only needs to receive the command and be allowed to send its replies.

These events concern recurring payments toward the bot. They are different from paid channel subscription invite
links.

## Arguments

The bot token is required and must be the first argument. The remaining optional flags are exact, case-sensitive
strings and can be supplied in either order:

| Argument | Effect |
| --- | --- |
| `BOT_TOKEN` | Token of the bot to run. |
| `debug` | Sends tgbotapi/KSLog diagnostic output to standard output. |
| `testServer` | Uses Telegram's Bot API test environment (`/test`) instead of the production environment. |

## Launch

From the repository root:

```bash
./gradlew :BotSubscriptionsBot:run --args="BOT_TOKEN"
```

For example, to enable both optional modes:

```bash
./gradlew :BotSubscriptionsBot:run --args="BOT_TOKEN debug testServer"
```
21 changes: 21 additions & 0 deletions BotSubscriptionsBot/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}

dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

apply plugin: 'kotlin'
apply plugin: 'application'

mainClassName="BotSubscriptionsBotKt"


dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}
100 changes: 100 additions & 0 deletions BotSubscriptionsBot/src/main/kotlin/BotSubscriptionsBot.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitBotSubscriptionUpdated
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBotSubscriptionUpdated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.types.payments.BotSubscriptionUpdated
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first

/**
* Runs a long-polling demonstration of bot payment-subscription updates introduced in Telegram Bot API 10.2.
*
* Telegram sends a `subscription` update carrying a [BotSubscriptionUpdated] when a user cancels a recurring
* payment subscription to the bot, re-enables a canceled subscription, or a subscription payment fails. This
* example consumes those updates; it does not create recurring invoices.
*
* Key concepts demonstrated:
* - [onBotSubscriptionUpdated] — trigger whose handler receives a [BotSubscriptionUpdated] (`user`,
* `invoicePayload`, `state`) and makes a best-effort status notification to the subscriber
* - [BotSubscriptionUpdated.State] — the typed sealed state: [BotSubscriptionUpdated.State.Active],
* [BotSubscriptionUpdated.State.Canceled], [BotSubscriptionUpdated.State.Failed] (data objects) and the
* [BotSubscriptionUpdated.State.Unknown] value-class fallback for any future state
* - `botSubscriptionUpdatedUpdatesFlow` — the raw update flow of
* [dev.inmo.tgbotapi.types.update.BotSubscriptionUpdatedUpdate] (available directly because a
* BehaviourContext is a `FlowsUpdatesFilter`); each emission's payload is its `data`
* - [waitBotSubscriptionUpdated] — expectation returning a flow of [BotSubscriptionUpdated]; the
* `/wait_subscription` handler takes its next value without a timeout and replies in the command's chat
*
* The first command-line argument is always treated as the bot token. Later arguments equal to `debug` and
* `testServer` enable console diagnostic logging and Telegram's Bot API test environment, respectively.
*
* @param args bot token followed by optional, case-sensitive `debug` and `testServer` flags
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
val isDebug = args.any { it == "debug" }
val isTestServer = args.any { it == "testServer" }

if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}

telegramBotWithBehaviourAndLongPolling(
botToken,
CoroutineScope(Dispatchers.IO),
testServer = isTestServer,
) {
val me = getMe()
println("Bot info: $me")

// subscription update: react to the typed BotSubscriptionUpdated.State
onBotSubscriptionUpdated { update ->
val user = update.user
val payload = update.invoicePayload
val stateText = when (val state = update.state) {
BotSubscriptionUpdated.State.Active -> "active ✅"
BotSubscriptionUpdated.State.Canceled -> "canceled ❌"
BotSubscriptionUpdated.State.Failed -> "payment failed ⚠️"
// Unknown is a value class carrying the raw state name — future-proof fallback
is BotSubscriptionUpdated.State.Unknown -> "unknown (${state.name})"
}
println("Subscription update from ${user.id}: payload=$payload, state=${update.state.name}")

// notify the subscriber (only works if they have an open chat with the bot)
runCatchingLogging {
send(user.id, "Your subscription (payload: $payload) is now: $stateText")
}
}

// Raw flow variant of the same updates. BehaviourContext : FlowsUpdatesFilter, so the flow is
// available directly; each emission is a BotSubscriptionUpdatedUpdate whose payload is `.data`.
botSubscriptionUpdatedUpdatesFlow.subscribeSafelyWithoutExceptions(this) { update ->
println("[flow] update ${update.updateId}: user=${update.data.user.id}, state=${update.data.state.name}")
}

// waitBotSubscriptionUpdated expectation: suspend until the next subscription update
onCommand("wait_subscription") {
reply(it, "Waiting for the next subscription update...")
val update = waitBotSubscriptionUpdated().first()
reply(it, "Subscription update: state=${update.state.name}, payload=${update.invoicePayload}")
}

allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it)
}
}.second.join()
}
74 changes: 71 additions & 3 deletions BusinessConnectionsBot/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,77 @@
# BusinessConnectionBotBot
# Business Connections Bot

When bot connected or disconnected to the business chat, it will notify this chat
This example demonstrates how a bot can manage a connected Telegram Business account. It handles business-connection updates, mirrors business messages, exposes inline actions for marking messages as read or deleting them, and exercises account, Stars, gifts, stories, and checklist APIs.

This is a feature demonstration, not a production-ready bot. Several commands change the connected account or transfer its Stars, and the bot keeps connection IDs only in memory.

## Telegram setup and rights

1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
2. Enable Business/Secretary Mode for the bot in BotFather. Telegram's name for the setting can vary by client version.
3. Start this example, then connect the bot to a Telegram Business account and allow it to manage the desired private chats. The account owner should also open a private chat with the bot; all commands below are accepted only there.
4. Grant the business rights needed by the features you want to try:

| Business right | Used by |
| --- | --- |
| Reply/send messages (`can_reply`) | Mirroring and replying to business messages and resending checklists |
| Read messages (`can_read_messages`) | **Read message** inline button |
| Delete all messages (`can_delete_all_messages`) | **Delete message** for incoming customer messages |
| Delete sent messages (`can_delete_sent_messages`) | Deleting messages sent by the bot itself |
| Edit name (`can_edit_name`) | `/set_business_account_name` |
| Edit username (`can_edit_username`) | `/set_business_account_username` |
| Edit bio (`can_edit_bio`) | `/set_business_account_bio` |
| Edit profile photo (`can_edit_profile_photo`) | Both profile-photo commands |
| View gifts and Stars (`can_view_gifts_and_stars`) | Balance and gift-list commands |
| Transfer Stars (`can_transfer_stars`) | `/transfer_business_account_stars` |
| Manage stories (`can_manage_stories`) | `/post_story` and `/delete_story` |

The account-management methods used here do not require the connected account to have Telegram Premium as of Bot API 9.0. Sending checklists still depends on the account and client being able to create them. Enable Bot-to-Bot Communication Mode in BotFather if you want to exercise the special reply path for a bot that contacts the managed business account.

See Telegram's [business-bot overview](https://core.telegram.org/bots/features#business-mode) and [`BusinessBotRights`](https://core.telegram.org/bots/api#businessbotrights) for the platform rules. The Bot API generally restricts business replies and reads to private chats active in the last 24 hours.

## Launch

From the repository root, pass the token as the first application argument:

```bash
../gradlew run --args="BOT_TOKEN"
./gradlew :BusinessConnectionsBot:run --args="<BOT_TOKEN>"
```

Pass the literal `debug` as the optional second argument to print verbose library logs:

```bash
./gradlew :BusinessConnectionsBot:run --args="<BOT_TOKEN> debug"
```

The bot prints its own `getMe` result, discards updates accumulated before startup, and then starts long polling. Because business connection IDs are cached only in memory, run the bot before creating/enabling the connection. After a restart, disable and re-enable the connection if owner commands do not respond.

## Automatic business update handling

- When a business connection is enabled or disabled, the bot records/removes its IDs and notifies the account owner in their private chat.
- A text business message starting with `/pin` or `/unpin` pins or unpins the accessible message it replies to.
- Other new business messages are resent to the sender's chat and receive a short diagnostic reply. Incoming customer messages also produce a notification in the business owner's bot chat with **Read message** and **Delete message** buttons.
- When the sender is another bot, the example sends a bot-to-bot diagnostic reply and skips the owner notification.
- Edited business messages are resent with an edit diagnostic. Deleted-business-message updates are reported to the account owner with the affected chat and message IDs.
- A received checklist is resent to the same chat on behalf of the business account when the connection ID can be resolved.

The inline **Read message** button calls `readBusinessMessage`. **Delete message** calls `deleteBusinessMessages`; for an incoming customer message, this requires the right to delete all managed-chat messages.

## Private-chat commands

These commands must be sent by the connected account owner in their private chat with the bot. Except for `/get_business_account_info`, handlers silently stop when that chat is not associated with an in-memory business connection.

| Command | Behavior |
| --- | --- |
| `/get_business_account_info` | Prints the current `BusinessConnection` as formatted JSON, or reports that no connection is known. |
| `/set_business_account_name <first_name> [last_name]` | Changes the connected account's first name and optional last name. Each name is parsed as one whitespace-separated argument. |
| `/set_business_account_username <username>` | Changes the account username to the single supplied argument. |
| `/get_business_account_star_balance` | Prints the account's current Telegram Stars balance. |
| `/transfer_business_account_stars <count>` | Transfers an integer number of Stars from the business account to the bot. Telegram accepts values from 1 through 10,000; this example leaves range validation to Telegram. |
| `/get_business_account_gifts` | Fetches every page of owned gifts and prints their Kotlin representations, splitting long output across messages. |
| `/set_business_account_bio <text>` | Saves the current bio, sets the complete text after the command as the new bio, waits 15 seconds, and attempts to restore the saved value. An empty text clears it temporarily. |
| `/set_business_account_profile_photo` | Reply to a photo with this command to set it as the main profile photo; after 15 seconds the bot removes that photo. |
| `/set_business_account_profile_photo_public` | Reply to a photo to set it as the public profile photo; after 15 seconds the bot removes it. |
| `/post_story` | Reply to a photo, video, or live photo to post it as a six-hour story with a fixed test caption and link area. |
| `/delete_story` | Reply to a story message to delete that story. Telegram only permits the bot to delete stories it posted for the business account. |

The profile-photo cleanup removes the newly current photo; it does not upload a saved copy of the previous photo. Telegram may promote the previous main photo after removal. Also note that the success text from `/post_story` currently mentions `/remove_story`; the implemented deletion command is `/delete_story`.
Loading
Loading