mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI-examples.git
synced 2026-09-02 14:19:05 +00:00
Compare commits
1 Commits
renovate/k
...
readmes
| Author | SHA1 | Date | |
|---|---|---|---|
| f9c131d5e1 |
@@ -1,36 +1,42 @@
|
||||
# BoostsInfoBot
|
||||
|
||||
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.
|
||||
A bot that retrieves and displays the boost information for a chat.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
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.
|
||||
On `/start`, the bot sends a reply keyboard with a *Request Channel* button. When the user selects
|
||||
a channel, the bot calls `getUserChatBoosts` and replies with a formatted list of all active boosts
|
||||
for that user in the selected chat, including the start and expiration dates of each boost.
|
||||
|
||||
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`.
|
||||
## Arguments
|
||||
|
||||
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.
|
||||
| Position | Value | Sample | Description |
|
||||
|----------|-------|--------|-------------|
|
||||
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||
|
||||
## Telegram setup and permissions
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
- 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.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Sends the channel-request keyboard |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Reply keyboard with a `RequestChat` button configured for channels
|
||||
- Retrieves user boost list via `getUserChatBoosts`
|
||||
- Formats each boost with its add date and expiration date
|
||||
- Handles `ChatShared` service messages to extract the target chat ID
|
||||
- Runs via long polling
|
||||
|
||||
## 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 :BoostsInfoBot:run --args="<BOT_TOKEN> debug"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
@@ -16,17 +16,6 @@ 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"
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# 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"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,77 +1,58 @@
|
||||
# Business Connections Bot
|
||||
# BusinessConnectionsBot
|
||||
|
||||
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.
|
||||
A comprehensive bot that demonstrates the Telegram Business Account API, including message
|
||||
management, profile editing, star transfers, story posting, and gift listing.
|
||||
|
||||
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.
|
||||
## Functionality
|
||||
|
||||
## Telegram setup and rights
|
||||
The bot connects to a business account. When a business connection is established it maps the
|
||||
business chat ID to the owner's personal chat so that management commands can be used in the
|
||||
personal chat. Messages received via the business connection are forwarded to the owner.
|
||||
Typing `PIN` or `UNPIN` in a business message pins or unpins it. A wide set of management commands
|
||||
is available in the owner's PM.
|
||||
|
||||
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:
|
||||
## Arguments
|
||||
|
||||
| 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` |
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
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.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
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.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/get_business_account_info` | Print account name, username, bio, and other details |
|
||||
| `/set_business_account_name` | Set the account's first and last name (prompts for input) |
|
||||
| `/set_business_account_username` | Set the account's username (prompts for input) |
|
||||
| `/set_business_account_bio` | Set the account bio (auto-resets to the old value after 15 seconds) |
|
||||
| `/set_business_account_profile_photo` | Set a private profile photo (send a photo in reply) |
|
||||
| `/set_business_account_profile_photo_public` | Set a public profile photo (send a photo in reply) |
|
||||
| `/get_business_account_star_balance` | Show the current star balance of the business account |
|
||||
| `/transfer_business_account_stars` | Transfer stars from the business account to the bot |
|
||||
| `/get_business_account_gifts` | List all gifts received by the business account |
|
||||
| `/post_story` | Post a story with a link area (send a photo in reply) |
|
||||
| `/delete_story` | Delete the most recently posted story |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- `BusinessConnection` event handling: maps business chat IDs to personal owner chats
|
||||
- Forwards business messages to the owner's PM
|
||||
- PIN / UNPIN keyword detection to pin or unpin messages in the business chat
|
||||
- Business message deletion tracking
|
||||
- Mutex-protected concurrent access to the chat mapping
|
||||
- Story creation with `InputStoryContentPhoto` and `StoryAreaTypeLink`
|
||||
- Checklist content support
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root, pass the token as the first application argument:
|
||||
|
||||
```bash
|
||||
./gradlew :BusinessConnectionsBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew 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`.
|
||||
|
||||
@@ -3,7 +3,6 @@ import dev.inmo.kslog.common.LogLevel
|
||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||
import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import dev.inmo.micro_utils.common.Percentage
|
||||
import dev.inmo.tgbotapi.types.chat.PreviewBot
|
||||
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountStarBalance
|
||||
@@ -28,8 +27,7 @@ import dev.inmo.tgbotapi.extensions.api.stories.deleteStory
|
||||
import dev.inmo.tgbotapi.extensions.api.stories.postStory
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
||||
import dev.inmo.tgbotapi.extensions.utils.chatContentMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.chatMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.commonMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.extendedPrivateChatOrThrow
|
||||
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
|
||||
@@ -46,15 +44,13 @@ import dev.inmo.tgbotapi.types.MessageId
|
||||
import dev.inmo.tgbotapi.types.RawChatId
|
||||
import dev.inmo.tgbotapi.types.business_connection.BusinessConnectionId
|
||||
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.LivePhotoContent
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.PhotoContent
|
||||
import dev.inmo.tgbotapi.types.message.content.StoryContent
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.types.message.content.VideoContent
|
||||
import dev.inmo.tgbotapi.types.message.content.VisualMediaGroupPartContent
|
||||
import dev.inmo.tgbotapi.types.stories.InputStoryContent
|
||||
import dev.inmo.tgbotapi.types.stories.InputStoryContent.*
|
||||
import dev.inmo.tgbotapi.types.stories.StoryArea
|
||||
import dev.inmo.tgbotapi.types.stories.StoryAreaPosition
|
||||
import dev.inmo.tgbotapi.types.stories.StoryAreaType
|
||||
@@ -70,16 +66,6 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Starts the long-polling Telegram Business API demonstration bot.
|
||||
*
|
||||
* The bot flushes queued updates before registering its handlers and stores business connection IDs only in memory.
|
||||
* Consequently, an already connected account may need to disable and re-enable its connection after a restart before
|
||||
* owner-side commands can resolve that connection.
|
||||
*
|
||||
* @param args the bot token followed optionally by the literal `debug`, which enables verbose library logging
|
||||
* @throws NoSuchElementException when the bot token is missing
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val botToken = args.first()
|
||||
val isDebug = args.getOrNull(1) == "debug"
|
||||
@@ -134,15 +120,6 @@ suspend fun main(args: Array<String>) {
|
||||
if (businessContentMessage.sentByBusinessConnectionOwner) {
|
||||
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
|
||||
} else {
|
||||
// Since TG Bot API 9.0: business bots can reply to other bots in business context
|
||||
// when bot-to-bot communication is enabled for both bots
|
||||
if (businessContentMessage.from is PreviewBot) {
|
||||
reply(
|
||||
to = sent,
|
||||
text = "Replying to bot ${businessContentMessage.from.firstName} in business context (bot-to-bot reply)",
|
||||
)
|
||||
return@ifBusinessContentMessage
|
||||
}
|
||||
reply(
|
||||
to = sent,
|
||||
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
|
||||
@@ -226,8 +203,6 @@ suspend fun main(args: Array<String>) {
|
||||
}
|
||||
)
|
||||
}
|
||||
// Since TG Bot API 9.0: the following account management commands no longer require
|
||||
// the connected user to have a Telegram Premium subscription.
|
||||
onCommandWithArgs("set_business_account_name", initialFilter = { it.chat is PrivateChat }) { it, args ->
|
||||
val firstName = args[0]
|
||||
val secondName = args.getOrNull(1)
|
||||
@@ -374,9 +349,9 @@ suspend fun main(args: Array<String>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
suspend fun handleSetProfilePhoto(it: ChatContentMessage<TextContent>, isPublic: Boolean) {
|
||||
suspend fun handleSetProfilePhoto(it: CommonMessage<TextContent>, isPublic: Boolean) {
|
||||
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
|
||||
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<PhotoContent>()
|
||||
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>()
|
||||
if (replyTo == null) {
|
||||
reply(it) {
|
||||
+"Reply to photo for using of this command"
|
||||
@@ -436,7 +411,7 @@ suspend fun main(args: Array<String>) {
|
||||
|
||||
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
|
||||
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
|
||||
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
|
||||
if (replyTo == null) {
|
||||
reply(it) {
|
||||
+"Reply to photo or video for using of this command"
|
||||
@@ -449,16 +424,12 @@ suspend fun main(args: Array<String>) {
|
||||
postStory(
|
||||
businessConnectionId,
|
||||
when (replyTo.content) {
|
||||
is PhotoContent -> Photo(
|
||||
is PhotoContent -> InputStoryContent.Photo(
|
||||
file.multipartFile()
|
||||
)
|
||||
is VideoContent -> Video(
|
||||
is VideoContent -> InputStoryContent.Video(
|
||||
file.multipartFile()
|
||||
)
|
||||
is LivePhotoContent -> Video(
|
||||
file.multipartFile(),
|
||||
isAnimation = true
|
||||
)
|
||||
},
|
||||
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
|
||||
areas = listOf(
|
||||
@@ -494,7 +465,7 @@ suspend fun main(args: Array<String>) {
|
||||
|
||||
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
|
||||
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
||||
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<StoryContent>()
|
||||
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<StoryContent>()
|
||||
if (replyTo == null) {
|
||||
reply(it) {
|
||||
+"Reply to photo or video for using of this command"
|
||||
@@ -529,4 +500,4 @@ suspend fun main(args: Array<String>) {
|
||||
)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,41 @@
|
||||
# ChatAvatarSetter
|
||||
|
||||
This Kotlin/JVM example changes a chat's avatar to the photo sent in that chat. It receives updates by long
|
||||
polling and demonstrates the Telegram Bot API [`setChatPhoto`](https://core.telegram.org/bots/api#setchatphoto)
|
||||
method with a multipart file upload.
|
||||
A bot that updates a group or channel's avatar using a photo sent to the bot.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
- There is no command: every photo message received by the bot is a trigger.
|
||||
- The bot downloads the photo, uploads it as `sample.jpg`, and uses it as the avatar of the same chat.
|
||||
- After a successful update, it replies `Done`. If `setChatPhoto` fails, it logs the exception and replies
|
||||
`Something went wrong (see logs)`.
|
||||
- Non-photo messages are ignored. In particular, an image sent as a document is not a photo message.
|
||||
|
||||
The implementation does not restrict who may trigger it. In a group or supergroup, any member whose photo
|
||||
message reaches the bot can cause an avatar-change attempt.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
2. Add the bot to the group, supergroup, or channel whose avatar it should manage.
|
||||
3. Promote the bot to administrator and grant it the **Change chat info** permission
|
||||
(`can_change_info`). It must also be able to send messages in a group or posts in a channel to deliver its
|
||||
status reply.
|
||||
4. Send a photo directly in that chat.
|
||||
|
||||
Telegram does not allow `setChatPhoto` to change private-chat photos. The target chat is always taken from the
|
||||
incoming photo message, so a photo sent to the bot privately cannot be used to update another chat.
|
||||
When the bot receives a photo message, it downloads the highest-resolution version of the photo
|
||||
and sets it as the chat photo for the chat the message was sent from. If the operation fails (e.g.,
|
||||
due to missing admin rights), the bot sends an error message back to the user.
|
||||
|
||||
## Arguments
|
||||
|
||||
| Position | Argument | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `BOT_TOKEN` | Yes | Bot API token issued by BotFather. |
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
None.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Downloads the largest available photo size from the incoming message
|
||||
- Calls `setChatPhoto` to apply the downloaded image as the chat's avatar
|
||||
- Returns a user-facing error message if the update fails
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root, run:
|
||||
|
||||
```bash
|
||||
./gradlew :ChatAvatarSetter:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The process continues polling until it is stopped.
|
||||
> **Note:** The bot must be an administrator with *Change group info* permission in the target chat.
|
||||
|
||||
@@ -10,15 +10,6 @@ import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts a long-polling bot that uses each incoming photo as the avatar of the chat where it was sent.
|
||||
*
|
||||
* The bot downloads the photo and uploads it with `setChatPhoto`. It replies `Done` when the avatar is updated;
|
||||
* if that API call fails, it logs the exception and replies with an error message. The target must be a
|
||||
* non-private chat where the bot is an administrator allowed to change chat information.
|
||||
*
|
||||
* @param args command-line arguments whose first value is the Bot API token
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val bot = telegramBot(args.first())
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# ChatManagementBot
|
||||
|
||||
This long-polling example demonstrates chat-management features introduced in Telegram Bot API 10.0. It inspects a member's `can_react_to_messages` permission, includes other bot administrators in an administrator query, deletes reactions, and logs content messages received from other bots.
|
||||
|
||||
At startup, the bot prints its name, username, and `canReadAllGroupMessages` value returned by `getMe`. The latter indicates whether Group Privacy Mode is disabled; it does not enable bot-to-bot communication by itself.
|
||||
|
||||
## Commands and triggers
|
||||
|
||||
| Command or trigger | Behavior |
|
||||
| --- | --- |
|
||||
| A member becomes restricted or their restrictions change | Prints the member's new `canReactToMessages` value twice: directly from the restricted member and through the `ChatPermissions` interface. |
|
||||
| `/retrieveRights` | Reply to a user-authored message. The bot calls `getChatMember` for that user and replies with their `canReactToMessages` value. It reports `null` when the returned member state is not restricted. |
|
||||
| `/admins` | In a group, supergroup, or channel, lists the chat administrators. It passes `retrieveOtherBots = true`, the library equivalent of Telegram's `return_bots = true`, so other bot administrators are included. |
|
||||
| `/deleteReaction` | Reply to a user-authored message in a group or supergroup. Removes that user's reaction from the replied-to message. |
|
||||
| `/deleteAllReactions` | Reply to a user-authored message in a group or supergroup. Removes up to 10,000 recent reactions made by that user in the current chat. |
|
||||
| A content message from another bot arrives | Prints the sender and content to standard output; messages from this bot itself are ignored. |
|
||||
|
||||
No command reads positional arguments. The commands that operate on a user take that user from the replied-to message. Command failures from Telegram, including missing permissions, are left to the library's normal error handling.
|
||||
|
||||
This is an API example, not a production moderation bot: it does not check whether the person invoking a reaction-deletion command is an administrator.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
2. Add the bot to the group or supergroup used for the examples and promote it to administrator. Telegram only sends `chat_member` updates about other members to administrators, and `getChatMember` is only guaranteed to work for other users when the bot is an administrator.
|
||||
3. Grant the bot the **Delete messages** (`can_delete_messages`) administrator right to use either reaction-deletion command.
|
||||
4. To exercise the other-bot message handler, enable **Bot-to-Bot Communication Mode** for the receiving bot in @BotFather. For ordinary messages that are neither an addressed command nor a direct reply, the receiving bot must also be a group administrator and have **Group Privacy Mode** disabled. Re-add the bot after changing Group Privacy Mode so the change takes effect.
|
||||
|
||||
See Telegram's documentation for [`chat_member` updates and chat-management methods](https://core.telegram.org/bots/api) and [bot-to-bot communication](https://core.telegram.org/api/bots%2Fbot-to-bot).
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root, run:
|
||||
|
||||
```bash
|
||||
./gradlew :ChatManagementBot:run --args="<BOT_TOKEN> [debug] [testServer]"
|
||||
```
|
||||
|
||||
The bot token must be the first argument. The optional, case-sensitive flags may follow it in either order:
|
||||
|
||||
- `debug` sends the library's logging to standard output.
|
||||
- `testServer` uses Telegram's Bot API test environment.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :ChatManagementBot:run --args="123456:ABCDEF debug"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="ChatManagementBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
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.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChatAdministrators
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember
|
||||
import dev.inmo.tgbotapi.extensions.api.send.deleteAllUserMessageReactions
|
||||
import dev.inmo.tgbotapi.extensions.api.send.deleteUserMessageReaction
|
||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.chatMemberGotRestrictedFilter
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.chatMemberGotRestrictionsChangedFilter
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatMemberUpdated
|
||||
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.utils.plus
|
||||
import dev.inmo.tgbotapi.extensions.utils.fromUserChatMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.fromUserMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.publicChatOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.requireRestrictedChatMember
|
||||
import dev.inmo.tgbotapi.extensions.utils.restrictedMemberChatMemberOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.specialRightsChatMemberOrNull
|
||||
import dev.inmo.tgbotapi.types.chat.CommonBot
|
||||
import dev.inmo.tgbotapi.types.chat.ChatPermissions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Runs a long-polling demonstration of the chat-management features introduced in Telegram Bot API 10.0.
|
||||
*
|
||||
* The bot logs changes to a restricted member's `canReactToMessages` permission, exposes commands for querying
|
||||
* member rights and administrators, removes a user's reactions, and logs content messages received from other
|
||||
* bots. Reaction deletion requires the bot's `can_delete_messages` administrator right. Receiving unrestricted
|
||||
* bot-authored group messages additionally requires Bot-to-Bot Communication Mode, administrator status, and
|
||||
* disabled Group Privacy Mode; `canReadAllGroupMessages` only reports the last of those settings.
|
||||
*
|
||||
* @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags. `debug` routes
|
||||
* library logs to standard output; `testServer` selects Telegram's Bot API test environment.
|
||||
*/
|
||||
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: ${me.firstName} (@${me.username?.username})")
|
||||
|
||||
// canReadAllGroupMessages (can_read_all_group_messages) reports whether Group Privacy Mode is disabled.
|
||||
// Bot-to-bot delivery has additional requirements described in the entry-point KDoc and README.
|
||||
println("canReadAllGroupMessages: ${me.canReadAllGroupMessages}")
|
||||
|
||||
// Feature 1: can_react_to_messages in ChatMemberRestricted and ChatPermissions
|
||||
// RestrictedMemberChatMember implements ChatPermissions, so canReactToMessages
|
||||
// appears in both types as required by the Telegram Bot API spec
|
||||
onChatMemberUpdated(
|
||||
initialFilter = chatMemberGotRestrictedFilter + chatMemberGotRestrictionsChangedFilter
|
||||
) { update ->
|
||||
val restricted = update.newChatMemberState.restrictedMemberChatMemberOrNull()
|
||||
?: return@onChatMemberUpdated
|
||||
println("Restriction update for ${update.member.firstName}:")
|
||||
// canReactToMessages as ChatMemberRestricted field
|
||||
println(" canReactToMessages (ChatMemberRestricted): ${restricted.canReactToMessages}")
|
||||
// same field via ChatPermissions — RestrictedMemberChatMember : ChatPermissions
|
||||
val permissions: ChatPermissions = restricted
|
||||
println(" canReactToMessages (ChatPermissions): ${permissions.canReactToMessages}")
|
||||
}
|
||||
|
||||
// Feature 1: can_react_to_messages in ChatMemberRestricted and ChatPermissions
|
||||
// RestrictedMemberChatMember implements ChatPermissions, so canReactToMessages
|
||||
// appears in both types as required by the Telegram Bot API spec
|
||||
onCommand(
|
||||
"retrieveRights"
|
||||
) { message ->
|
||||
val replyMessage = message.replyTo ?.fromUserChatMessageOrNull() ?: run {
|
||||
reply(message) { +"This command works only in groups/supergroups/channels" }
|
||||
return@onCommand
|
||||
}
|
||||
val chatMember = getChatMember(message.chat.id, replyMessage.user.id)
|
||||
val chatPermissions = chatMember.restrictedMemberChatMemberOrNull()
|
||||
|
||||
val canReactToMessages = chatPermissions ?.canReactToMessages
|
||||
reply(message) { +"Can react to messages: $canReactToMessages" }
|
||||
}
|
||||
|
||||
// Feature 2: return_bots parameter in getChatAdministrators
|
||||
// retrieveOtherBots = true corresponds to return_bots = true in the Telegram API
|
||||
onCommand("admins") { message ->
|
||||
val chat = message.chat.publicChatOrNull() ?: run {
|
||||
reply(message) { +"This command works only in groups/supergroups/channels" }
|
||||
return@onCommand
|
||||
}
|
||||
val admins = getChatAdministrators(chat, retrieveOtherBots = true)
|
||||
reply(message) {
|
||||
+"Administrators (retrieveOtherBots=true, includes bots):\n"
|
||||
admins.forEach { admin ->
|
||||
val kind = if (admin.user is CommonBot) "bot" else "user"
|
||||
+"• ${admin.user.firstName} [$kind]\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feature 4: deleteMessageReaction
|
||||
// Deletes a specific reaction by the replied message's author on that message
|
||||
onCommand("deleteReaction") { message ->
|
||||
val replied = message.replyTo ?.fromUserChatMessageOrNull() ?: run {
|
||||
reply(message) { +"Reply to a message to remove that user's reaction from it" }
|
||||
return@onCommand
|
||||
}
|
||||
deleteUserMessageReaction(replied, replied.user.id)
|
||||
reply(message) { +"Deleted reaction by ${replied.user.firstName} on the replied message" }
|
||||
}
|
||||
|
||||
// Feature 3: deleteAllMessageReactions
|
||||
// Deletes up to 10,000 recent reactions that the replied message's author has left in this chat
|
||||
onCommand("deleteAllReactions") { message ->
|
||||
val replied = message.replyTo?.fromUserMessageOrNull() ?: run {
|
||||
reply(message) { +"Reply to a message to clear all reactions of that user in this chat" }
|
||||
return@onCommand
|
||||
}
|
||||
deleteAllUserMessageReactions(message.chat, replied.user.id)
|
||||
reply(message) { +"Deleted all reactions by ${replied.user.firstName} in this chat" }
|
||||
}
|
||||
|
||||
// Feature 5: messages from other bots in groups
|
||||
// This handler logs bot-authored content messages that Telegram delivers to this bot.
|
||||
onContentMessage(
|
||||
initialFilter = { msg ->
|
||||
val user = msg.fromUserMessageOrNull()?.user
|
||||
user is CommonBot && user.id != me.id
|
||||
}
|
||||
) { message ->
|
||||
val sender = message.fromUserMessageOrNull()?.user
|
||||
println("Message from other bot received (canReadAllGroupMessages=${me.canReadAllGroupMessages}):")
|
||||
println(" sender: ${sender?.firstName} (@${(sender as? CommonBot)?.username?.username})")
|
||||
println(" content: ${message.content}")
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -1,75 +1,42 @@
|
||||
# ChecklistsBot
|
||||
|
||||
This example shows how to receive Telegram checklist messages and checklist service events with
|
||||
the TelegramBotAPI behaviour builder. It mirrors each checklist as a formatted text reply; it does
|
||||
not create or edit checklists and stores no state.
|
||||
A bot that handles Telegram premium checklist messages and tracks task completion events.
|
||||
|
||||
## Behaviour
|
||||
## Functionality
|
||||
|
||||
The bot uses long polling and installs these handlers:
|
||||
Listens for messages containing a checklist. When a checklist message is received, the bot sends
|
||||
a formatted reply showing all tasks with their completion status. It also reacts to task-level
|
||||
events: when a task is marked as done or a new task is added to an existing checklist, the bot
|
||||
sends an update reply referencing the affected task.
|
||||
|
||||
| Incoming update | Response |
|
||||
| --- | --- |
|
||||
| A message containing a checklist | Replies to that message with the checklist's current contents. |
|
||||
| Tasks marked as done or not done | Replies to the checklist-status service message with the full current checklist. If at least one task was newly marked done, the reply includes the first such task's ID as `checklist_task_id`; an event containing only newly reopened tasks gets a normal message-level reply. |
|
||||
| Tasks added to a checklist | Replies to the original checklist and targets the first newly added task with `checklist_task_id`, then includes the full current checklist. |
|
||||
## Arguments
|
||||
|
||||
The text snapshot preserves the title and task text entities. Each task is rendered on its own line
|
||||
as `• [x] task` when it has a completion date or `• [ ] task` otherwise; the marker is formatted as
|
||||
code and the task text as bold.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
There are no bot commands. On startup the bot prints the result of `getMe`, and it prints every raw
|
||||
update it receives. The optional `debug` flag additionally sends TelegramBotAPI diagnostic logs to
|
||||
standard output.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
## Telegram setup
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
2. Start the example, open a private chat with the bot, and send it a checklist. Telegram Premium is
|
||||
currently required to create a checklist in Telegram clients. Use the checklist's own options if
|
||||
other participants should be allowed to add tasks or change their completion state.
|
||||
3. For group testing, ensure the bot may send messages and can receive the original checklist.
|
||||
Ordinary checklist messages are hidden by the default Group Privacy Mode, so make the bot a group
|
||||
administrator or disable privacy with BotFather and re-add the bot to the group.
|
||||
## Bot Commands
|
||||
|
||||
No Telegram Business connection or business-bot right is required: the example only receives
|
||||
checklists and sends ordinary text replies. Telegram restricts the `sendChecklist` Bot API method,
|
||||
which this example does not use, to connected business accounts.
|
||||
None.
|
||||
|
||||
## Run
|
||||
## Capabilities
|
||||
|
||||
From the repository root:
|
||||
- Detects `ChecklistContent` messages (Telegram Premium feature)
|
||||
- Formats checklist tasks with ✅ (completed) and ⬜ (pending) indicators
|
||||
- Handles `ChecklistTasksDone` events — replies when a task is marked complete
|
||||
- Handles `ChecklistTasksAdded` events — replies when new tasks are appended
|
||||
- Uses rich text message building for formatted output
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :ChecklistsBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The token must be first. The remaining optional flags are exact, case-sensitive strings and may be
|
||||
supplied in either order:
|
||||
|
||||
| Argument | Required | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `<BOT_TOKEN>` | Yes | Bot token; it must be the first argument. |
|
||||
| `debug` | No | Enables verbose TelegramBotAPI logging on standard output. |
|
||||
| `testServer` | No | Uses Telegram's Bot API test environment (`/test`) instead of production. |
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :ChecklistsBot:run --args="<BOT_TOKEN> debug testServer"
|
||||
```
|
||||
|
||||
Do not run another webhook or long-polling consumer with the same token while this example is
|
||||
running.
|
||||
|
||||
## API concepts demonstrated
|
||||
|
||||
- `onChecklistContent` for checklist content messages.
|
||||
- `onChecklistTasksDone` for service messages that contain both completed and reopened task IDs.
|
||||
- `onChecklistTasksAdded` for task-addition service messages.
|
||||
- Entity-aware output with `buildEntities`, `code`, and `bold`.
|
||||
- Replies to a specific checklist item through `checklist_task_id`.
|
||||
|
||||
See Telegram's [Checklist and ChecklistTask objects](https://core.telegram.org/bots/api#checklist)
|
||||
and [checklist launch announcement](https://telegram.org/blog/checklists-suggested-posts#checklists)
|
||||
for the underlying platform behaviour.
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||
@@ -32,7 +33,7 @@ import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
||||
import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
|
||||
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.ChecklistContent
|
||||
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
@@ -46,17 +47,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Starts a long-polling bot that renders incoming checklists as formatted text and reports
|
||||
* checklist task additions and completion-state changes with task-aware replies.
|
||||
*
|
||||
* The first argument is the required bot token. The optional, case-sensitive `debug` and
|
||||
* `testServer` arguments enable verbose library logging and Telegram's Bot API test environment,
|
||||
* respectively. The bot identity and every received update are printed to standard output in all
|
||||
* modes.
|
||||
*
|
||||
* @param args bot token followed by optional `debug` and `testServer` flags
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -76,6 +66,7 @@ suspend fun main(vararg args: String) {
|
||||
CoroutineScope(Dispatchers.Default),
|
||||
testServer = isTestServer,
|
||||
) {
|
||||
// start here!!
|
||||
val me = getMe()
|
||||
println(me)
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Behavior, commands, and triggers
|
||||
|
||||
At startup, the bot calls `getMe` and prints its bot information. It also prints every received update to standard output.
|
||||
|
||||
| Command or trigger | Behavior |
|
||||
| --- | --- |
|
||||
| `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` | 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." |
|
||||
|
||||
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.
|
||||
- `getChat(...).community` exposes the nullable community on `ExtendedChat` without a subtype cast.
|
||||
- `waitCommunityChatAddedEventsMessages` and `waitCommunityChatRemovedEventsMessages` expose typed event-message flows.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
1. Create a bot with BotFather and obtain its token.
|
||||
2. Add it to the target chat before changing that chat's community membership if you want to observe both service events.
|
||||
3. Allow it to send messages so notifications and command replies succeed.
|
||||
|
||||
The bot does not create or modify communities. It calls no administrator-only method and does not inspect arbitrary user messages, so it needs neither administrator rights nor disabled Group Privacy Mode. The user changing community membership still needs the appropriate Telegram rights. API failures are left to the library's normal error handling.
|
||||
|
||||
## Arguments
|
||||
|
||||
The token is required as the first argument. Optional flags are exact and case-sensitive, may follow in either order, and unknown extra arguments are ignored.
|
||||
|
||||
| Argument | Effect |
|
||||
| --- | --- |
|
||||
| `BOT_TOKEN` | Bot token; omitting it fails before polling starts. |
|
||||
| `debug` | Sends tgbotapi/KSLog diagnostics to standard output. |
|
||||
| `testServer` | Uses Telegram's Bot API test environment. |
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :CommunitiesBot:run --args="BOT_TOKEN"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="CommunitiesBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.LogLevel
|
||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||
import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.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.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.onCommunityChatRemoved
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.filter
|
||||
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.
|
||||
*
|
||||
* @param args the bot token followed by the optional, case-sensitive `debug` and `testServer` flags; unknown trailing
|
||||
* arguments are ignored
|
||||
* @throws NoSuchElementException when the required bot token is absent
|
||||
*/
|
||||
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")
|
||||
|
||||
// community_chat_added: the chat was added to a community
|
||||
onCommunityChatAdded { message ->
|
||||
val community = message.chatEvent.community
|
||||
println("Chat ${message.chat.id} was added to community '${community.name}' (id=${community.id.long})")
|
||||
send(message.chat.id, "This chat has joined the community: ${community.name}")
|
||||
|
||||
// community is exposed on ExtendedChat itself (ChatFullInfo.community) — no cast needed
|
||||
val extended = getChat(message.chat.id)
|
||||
println("getChat().community = ${extended.community?.name} / ${extended.community?.id?.long}")
|
||||
}
|
||||
|
||||
// community_chat_removed: a fieldless event — the chat left its community
|
||||
onCommunityChatRemoved { message ->
|
||||
println("Chat ${message.chat.id} was removed from its community")
|
||||
send(message.chat.id, "This chat has left its community")
|
||||
}
|
||||
|
||||
// Inspect the current chat's community on demand
|
||||
onCommand("community") {
|
||||
val community = getChat(it.chat.id).community
|
||||
reply(
|
||||
it,
|
||||
if (community != null) {
|
||||
"Community: ${community.name} (id=${community.id.long})"
|
||||
} else {
|
||||
"This chat is not part of any community"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Suspend until the next community-added event message from this chat.
|
||||
onCommand("wait_community_added") { origin ->
|
||||
reply(origin, "Waiting for this chat to be added to a community...")
|
||||
val event = waitCommunityChatAddedEventsMessages().filter { it.sameChat(origin) }.first().chatEvent
|
||||
reply(origin, "Chat added to community: ${event.community.name} (id=${event.community.id.long})")
|
||||
}
|
||||
|
||||
// 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...")
|
||||
waitCommunityChatRemovedEventsMessages().filter { it.sameChat(origin) }.first()
|
||||
reply(origin, "Chat removed from its community (${origin.chat.id})")
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -1,48 +1,47 @@
|
||||
# CustomBot
|
||||
|
||||
CustomBot is a diagnostics-heavy playground for experimenting with TelegramBotAPI's behaviour
|
||||
builder. It uses long polling, prints the result of `getMe` at startup, logs every received update,
|
||||
and prints every Bot API request and result. It is intended as an example to modify, not as a
|
||||
production bot.
|
||||
A bot that demonstrates custom middleware, custom subcontext data, and several utility features
|
||||
of the TelegramBotAPI library.
|
||||
|
||||
## Commands and updates
|
||||
## Functionality
|
||||
|
||||
| Command or update | Behaviour |
|
||||
| --- | --- |
|
||||
| `/start` | Prints the captured update, context data, and `getChat` result. It fetches profile audios for the current private-chat ID in pages of two, replying with one audio or a two-audio playlist for each non-empty page. |
|
||||
| `/additional_command` | Demonstrates handler-specific subcontext initialization by storing the command message and printing it with the captured update. It sends no reply. |
|
||||
| `/getMyStarBalance` | Replies with the bot's current Telegram Stars balance. |
|
||||
| Channel direct-messages configuration changed | Prints the event to standard output. |
|
||||
Shows how to attach a logging middleware to every API request and how to store arbitrary data in
|
||||
a per-update subcontext. Additionally demonstrates retrieving and sending a user's profile audio
|
||||
playlist and querying the bot's own star balance.
|
||||
|
||||
The commands take no arguments. Use `/start` in a private chat: the example deliberately uses the
|
||||
chat ID as the user ID for `getUserProfileAudios`. No administrator rights are needed for the
|
||||
private-chat commands. The channel event is only observable when Telegram delivers that update for
|
||||
a channel in which the bot participates.
|
||||
## Arguments
|
||||
|
||||
## Run
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
Create a bot, obtain its token, and run this command from the repository root:
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Retrieves the sender's profile audio files and sends them as an audio media group |
|
||||
| `/additional_command` | Demo command that accesses and prints custom subcontext data |
|
||||
| `/getMyStarBalance` | Queries and replies with the bot's current Telegram Star balance |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Custom request middleware that logs every outgoing API call
|
||||
- Custom `BehaviourContext` subcontext with arbitrary stored data
|
||||
- Profile audio retrieval via `getUserProfilePhotos`-style API for audio
|
||||
- Audio media group sending (batched uploads)
|
||||
- Star balance query via `getStarTransactions`
|
||||
- Channel direct-message configuration tracking via `ChatBoostUpdated` events
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :CustomBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The token must be the first application argument. Two optional, case-sensitive flags may follow in
|
||||
either order:
|
||||
|
||||
- `debug` enables TelegramBotAPI library logs on standard output. The bot's explicit request,
|
||||
result, and update logging is active even without this flag.
|
||||
- `testServer` selects Telegram's Bot API test environment instead of production.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :CustomBot:run --args="<BOT_TOKEN> debug testServer"
|
||||
```
|
||||
|
||||
## API concepts demonstrated
|
||||
|
||||
- Global and handler-specific `BehaviourContextData` initialization.
|
||||
- Request/result middleware and subscription to `allUpdatesFlow`.
|
||||
- Paginated `getUserProfileAudios` calls and audio/media-group replies.
|
||||
- Command and channel-direct-message event handlers.
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||
@@ -22,7 +22,7 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
|
||||
import dev.inmo.tgbotapi.types.media.AudioMediaGroupMemberTelegramMedia
|
||||
import dev.inmo.tgbotapi.types.media.toTelegramMediaAudio
|
||||
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -31,18 +31,12 @@ private var BehaviourContextData.update: Update?
|
||||
get() = get("update") as? Update
|
||||
set(value) = set("update", value)
|
||||
|
||||
private var BehaviourContextData.commonMessage: ChatContentMessage<*>?
|
||||
get() = get("commonMessage") as? ChatContentMessage<*>
|
||||
private var BehaviourContextData.commonMessage: CommonMessage<*>?
|
||||
get() = get("commonMessage") as? CommonMessage<*>
|
||||
set(value) = set("commonMessage", value)
|
||||
|
||||
/**
|
||||
* Runs a diagnostics-oriented TelegramBotAPI playground using long polling.
|
||||
*
|
||||
* The bot logs every update and Bot API result, demonstrates global and handler-specific context
|
||||
* initialization, exposes commands for profile audios and the bot's Star balance, and observes
|
||||
* channel direct-messages configuration changes.
|
||||
*
|
||||
* @param args bot token followed by optional, case-sensitive `debug` and `testServer` flags
|
||||
* This place can be the playground for your code.
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
@@ -135,7 +129,7 @@ suspend fun main(vararg args: String) {
|
||||
println(it.chatEvent)
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
# DeepLinksBot
|
||||
|
||||
An example long-polling bot that creates deep links to itself and demonstrates two
|
||||
ways to consume their payloads with the TelegramBotAPI behaviour builder.
|
||||
A bot that generates and handles Telegram deep links.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
- On startup, the bot fetches and prints its own account details. It stops if the
|
||||
account has no username, because a username is required to build a deep link.
|
||||
- `/start` without arguments returns a short usage hint.
|
||||
- A text message containing no bot-command entity is used as the payload of a new
|
||||
deep link, which the bot returns to the sender.
|
||||
- A `/start <payload>` deep link is observed by both an `onDeepLink` trigger and a
|
||||
`waitDeepLinks` waiter. Their replies identify which API received the payload.
|
||||
- The waiter also prints the registered command handlers for demonstration and
|
||||
debugging purposes.
|
||||
Generates a deep link to the bot when the user sends any text message. When a deep link is followed
|
||||
(i.e., the `/start` command is received with a payload), the bot confirms what payload was received.
|
||||
|
||||
Messages containing a bot command are excluded from link generation. The bot has
|
||||
no persistence and runs until the process is stopped.
|
||||
## Arguments
|
||||
|
||||
## Requirements
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
- A Telegram bot token supplied as the first command-line argument.
|
||||
- A username configured for the bot account.
|
||||
- No special administrator permissions; the bot only needs to receive messages
|
||||
and send replies in the chat where it is used.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
## Run
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
From the repository root:
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Displays a help/welcome message; also handles deep-link payloads |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Requires a registered bot username (validates that `getMe` returns a username)
|
||||
- Generates a `t.me/<username>?start=<payload>` deep link from any incoming text message
|
||||
- Subscribes to deep-link follow events with `waitDeepLinks()` and confirms the received payload
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :DeepLinksBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
Replace `BOT_TOKEN` with the token for the bot. Additional command-line arguments
|
||||
are ignored. Omitting the token causes startup to fail before polling begins.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
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.behaviour_builder.expectations.waitDeepLinks
|
||||
@@ -10,14 +10,7 @@ import dev.inmo.tgbotapi.extensions.utils.formatting.makeTelegramDeepLink
|
||||
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
|
||||
|
||||
/**
|
||||
* Runs a long-polling bot that turns non-command text into a deep link to itself
|
||||
* and acknowledges payloads received through `/start`.
|
||||
*
|
||||
* The bot account must have a username so that its deep links can be constructed.
|
||||
*
|
||||
* @param args the bot token as the first argument; any remaining arguments are ignored
|
||||
* @throws NoSuchElementException when no bot token is supplied
|
||||
* @throws IllegalStateException when the bot account has no username
|
||||
* This bot will send you deeplink to this bot when you send some text message and react on the `start` button
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
@@ -43,7 +36,7 @@ suspend fun main(vararg args: String) {
|
||||
onDeepLink { (it, deepLink) ->
|
||||
reply(it, "Ok, I got deep link \"${deepLink}\" in trigger")
|
||||
}
|
||||
waitDeepLinks().subscribeLoggingDropExceptions(this) { (it, deepLink) ->
|
||||
waitDeepLinks().subscribeSafelyWithoutExceptions(this) { (it, deepLink) ->
|
||||
reply(it, "Ok, I got deep link \"${deepLink}\" in waiter")
|
||||
println(triggersHolder.handleableCommandsHolder.handleable)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,40 @@
|
||||
# DraftsBot
|
||||
|
||||
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.
|
||||
A bot that demonstrates the message-draft flow API by progressively revealing text to the user.
|
||||
|
||||
## Commands
|
||||
## Functionality
|
||||
|
||||
- `/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.
|
||||
On `/test_draft_flow`, the bot sends a series of draft text updates to the user, each building
|
||||
on the previous one, before committing the final message. This illustrates how to use the draft
|
||||
message API to stream partial content before finalising it.
|
||||
|
||||
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.
|
||||
## Arguments
|
||||
|
||||
## Setup
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
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.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
The first program argument is required and must be the bot token. Omitting it
|
||||
causes startup to fail; any later arguments are ignored.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Run
|
||||
## Bot Commands
|
||||
|
||||
From the repository root, run:
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/test_draft_flow` | Starts a draft-message flow that progressively reveals text |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Uses the `draftFlow` / `sendDraftMessage` API to emit incremental text updates
|
||||
- Demonstrates the difference between draft (editable intermediate state) and final message
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :DraftsBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew 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.
|
||||
|
||||
@@ -3,6 +3,7 @@ import dev.inmo.kslog.common.w
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.bot.TelegramBot
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||
@@ -37,19 +38,10 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
/** 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.
|
||||
"""
|
||||
|
||||
/**
|
||||
* Starts DraftsBot with long polling and registers the draft demonstration commands.
|
||||
*
|
||||
* The first element of [args] must be the bot token; subsequent elements are ignored.
|
||||
* This function remains suspended until the polling job completes.
|
||||
*
|
||||
* @throws NoSuchElementException when no bot token is supplied
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
telegramBotWithBehaviourAndLongPolling(
|
||||
args.first(),
|
||||
@@ -83,29 +75,8 @@ suspend fun main(vararg args: String) {
|
||||
send(it.chat, testText)
|
||||
}
|
||||
|
||||
// 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") {
|
||||
sendMessageDraftFlowWithTexts(
|
||||
it.chat.id,
|
||||
flow<String> {
|
||||
emit("") // empty draft — clears / initializes typing indicator with no content
|
||||
delay(1500L)
|
||||
val step = 50
|
||||
var currentLength = step
|
||||
while (isActive && testText.length > currentLength) {
|
||||
delay(500L)
|
||||
emit(testText.take(currentLength))
|
||||
currentLength += step
|
||||
}
|
||||
},
|
||||
)
|
||||
send(it.chat, testText)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# EphemeralMessagesBot
|
||||
|
||||
Demonstrates 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root, pass the token as the first application argument:
|
||||
|
||||
```bash
|
||||
./gradlew :EphemeralMessagesBot:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
Optional, case-sensitive arguments may follow the token:
|
||||
|
||||
- `debug` enables formatted library logging on standard output.
|
||||
- `testServer` connects the bot to Telegram's test server.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :EphemeralMessagesBot:run --args="BOT_TOKEN debug testServer"
|
||||
```
|
||||
|
||||
The token is required; starting without it fails before polling begins. Stop the bot with `Ctrl+C`.
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="EphemeralMessagesBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.LogLevel
|
||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||
import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||
import dev.inmo.tgbotapi.extensions.api.deleteEphemeralMessage
|
||||
import dev.inmo.tgbotapi.extensions.api.edit.text.editEphemeralMessageText
|
||||
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.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.types.buttons.dataButton
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
|
||||
import dev.inmo.tgbotapi.types.BotCommand
|
||||
import dev.inmo.tgbotapi.types.ephemeralReplyReceiverUserIdOrNull
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.PossiblyEphemeralMessage
|
||||
import korlibs.time.seconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* 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 [reply] and an explicit [replyToEphemeral].
|
||||
*
|
||||
* [args] must start with the bot token. The optional exact values `debug` and `testServer` respectively
|
||||
* enable console logging and select Telegram's test server.
|
||||
*
|
||||
* @throws NoSuchElementException when [args] does not contain a bot token
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
val isDebug = args.any { it == "debug" }
|
||||
val isTestServer = args.any { it == "testServer" }
|
||||
|
||||
if (isDebug) {
|
||||
setDefaultKSLog(
|
||||
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
telegramBotWithBehaviourAndLongPolling(
|
||||
botToken,
|
||||
testServer = isTestServer,
|
||||
) {
|
||||
val me = getMe()
|
||||
println("Bot info: $me")
|
||||
|
||||
// Post (in a group) a message with an inline button. Tapping it triggers an ephemeral reply that is
|
||||
// visible only to the user who tapped.
|
||||
onCommand("ephemeral") {
|
||||
reply(
|
||||
it,
|
||||
"Tap the button — the reply will be ephemeral (visible only to you).",
|
||||
replyMarkup = flatInlineKeyboard {
|
||||
dataButton("Reveal a secret", "reveal")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 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).
|
||||
onMessageDataCallbackQuery(Regex("reveal")) { query ->
|
||||
val chatId = query.message.chat.id
|
||||
val receiverUserId = query.from.id
|
||||
|
||||
val sent = sendTextMessage(
|
||||
chatId,
|
||||
"🔒 ${query.from.firstName}, here is your personal secret: 42",
|
||||
receiverUserId = receiverUserId,
|
||||
callbackQueryId = query.id,
|
||||
)
|
||||
|
||||
// Only the group-family Common*ContentMessage types implement PossiblyEphemeralMessage, so the
|
||||
// sent ephemeral message exposes its ephemeralMessageId through that interface.
|
||||
val ephemeralMessageId = (sent as? PossiblyEphemeralMessage)?.ephemeralMessageId
|
||||
if (ephemeralMessageId != null) {
|
||||
delay(3.seconds)
|
||||
// editEphemeralMessageText: address the ephemeral message by chatId + receiverUserId + ephemeralMessageId
|
||||
editEphemeralMessageText(chatId, receiverUserId, ephemeralMessageId, "🔓 Revealed: the answer is 42")
|
||||
delay(3.seconds)
|
||||
// deleteEphemeralMessage: same addressing (there is also a PossiblyEphemeralMessage overload)
|
||||
deleteEphemeralMessage(chatId, receiverUserId, ephemeralMessageId)
|
||||
}
|
||||
}
|
||||
|
||||
// Incoming ephemeral messages: detect them via PossiblyEphemeralMessage, then answer them.
|
||||
onContentMessage { message ->
|
||||
val ephemeral = (message as? PossiblyEphemeralMessage)?.takeIf { it.ephemeralMessageId != null }
|
||||
?: return@onContentMessage
|
||||
|
||||
// reply smart-branch: because `message` is ephemeral, this reply is sent ephemeral to the same
|
||||
// receiver automatically — no ephemeral parameters needed here.
|
||||
reply(message, "Got your ephemeral message — I am replying ephemerally too.")
|
||||
|
||||
// The explicit equivalent, addressing the ephemeral message by hand:
|
||||
val receiverUserId = ephemeral.ephemeralReplyReceiverUserIdOrNull
|
||||
if (receiverUserId != null) {
|
||||
replyToEphemeral(
|
||||
message.chat.id,
|
||||
receiverUserId,
|
||||
ephemeral.ephemeralMessageId!!,
|
||||
"Explicit ephemeral reply via replyToEphemeral",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setMyCommands(
|
||||
// isEphemeral marks a command whose response is an ephemeral (personal) message
|
||||
BotCommand("ephemeral", "Post a button that reveals an ephemeral (personal) message", isEphemeral = true),
|
||||
)
|
||||
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
# FSMBot
|
||||
|
||||
FSMBot demonstrates the finite-state-machine (FSM) support provided by
|
||||
[MicroUtils](https://github.com/InsanusMokrassar/MicroUtils) and TelegramBotAPI's
|
||||
behaviour builder.
|
||||
A demonstration of the Finite State Machine (FSM) pattern provided by the
|
||||
[MicroUtils](https://github.com/InsanusMokrassar/MicroUtils) library.
|
||||
|
||||
## Behaviour
|
||||
## Functionality
|
||||
|
||||
1. Send `/start` to begin a conversation chain for the current chat.
|
||||
2. The bot asks for content and waits in the same forum topic/thread in which the
|
||||
chain was started.
|
||||
3. Each content message is copied back to the chat, then the bot waits again.
|
||||
4. Send `/stop` in that topic/thread to end the chain and receive a confirmation.
|
||||
Implements a simple two-state FSM. After `/start` is sent, the bot enters
|
||||
`ExpectContentOrStopState` and re-sends every message it receives back to the user.
|
||||
This continues until the user sends `/stop`, at which point the FSM transitions to
|
||||
`StopState` and content forwarding ends.
|
||||
|
||||
FSM state is held in memory, so active chains are lost when the process stops.
|
||||
Incoming updates and state-handling errors are printed to standard output.
|
||||
## Arguments
|
||||
|
||||
## Commands
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
- `/start` — start or restart the content-resending chain.
|
||||
- `/stop` — stop the active chain while the bot is waiting for content.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
The bot does not register its command menu automatically; commands can be typed
|
||||
directly or configured separately with BotFather.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Requirements and permissions
|
||||
## Bot Commands
|
||||
|
||||
- A bot token obtained from BotFather.
|
||||
- A compatible JDK for the repository's Gradle wrapper.
|
||||
- Permission to send messages and the content types being copied in the target chat.
|
||||
- No administrator rights are required. For use in groups, disable privacy mode if
|
||||
the bot must receive arbitrary non-command messages rather than only commands and
|
||||
other updates Telegram exposes to privacy-enabled bots.
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Starts the FSM loop — bot begins echoing content back to the user |
|
||||
| `/stop` | Ends the FSM loop — bot stops echoing |
|
||||
|
||||
## Run
|
||||
## Capabilities
|
||||
|
||||
From the repository root:
|
||||
- Two-state FSM: `ExpectContentOrStopState` → `StopState`
|
||||
- `ExpectContentOrStopState` uses `expectContentOrCommands()` to filter messages
|
||||
- Erroneous FSM states are caught and handled gracefully
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :FSMBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
`BOT_TOKEN` is the required first positional argument. The bot uses long polling;
|
||||
no webhook or additional configuration is needed.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dev.inmo.micro_utils.coroutines.awaitFirst
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.fsm.common.State
|
||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitAnyContentMessage
|
||||
@@ -13,7 +13,7 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameThread
|
||||
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.utils.botCommand
|
||||
import dev.inmo.tgbotapi.utils.firstOf
|
||||
@@ -23,25 +23,10 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/** State hierarchy for a chat-scoped content-resending conversation. */
|
||||
sealed interface BotState : State
|
||||
|
||||
/**
|
||||
* Waits for content or a `/stop` command in the thread of [sourceMessage].
|
||||
*
|
||||
* @property context Chat whose FSM chain owns this state.
|
||||
* @property sourceMessage Message that determines the forum topic/thread to observe.
|
||||
*/
|
||||
data class ExpectContentOrStopState(override val context: IdChatIdentifier, val sourceMessage: ChatContentMessage<TextContent>) : BotState
|
||||
|
||||
/** Terminal state that acknowledges the end of the chain in [context]. */
|
||||
data class ExpectContentOrStopState(override val context: IdChatIdentifier, val sourceMessage: CommonMessage<TextContent>) : BotState
|
||||
data class StopState(override val context: IdChatIdentifier) : BotState
|
||||
|
||||
/**
|
||||
* Starts the FSM-based resender using the bot token in the first command-line argument.
|
||||
*
|
||||
* The bot runs with long polling until its coroutine is cancelled.
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -112,7 +97,7 @@ suspend fun main(args: Array<String>) {
|
||||
startChain(ExpectContentOrStopState(it.chat.id, it.withContentOrNull() ?: return@onContentMessage))
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
|
||||
@@ -1,42 +1,48 @@
|
||||
# FilesLoaderBot
|
||||
|
||||
FilesLoaderBot downloads media received through Telegram, stores it on the local
|
||||
filesystem, and sends the downloaded media back to the same chat. It uses long
|
||||
polling and logs every received update to standard output.
|
||||
A bot that downloads any media file sent to it and then re-uploads it back to the chat.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
- `/start` asks the user to send media.
|
||||
- Any received photo, animation, live photo, video, sticker, document, audio,
|
||||
voice message, video note, or supported media group is handled.
|
||||
- The file is saved under the filename returned by Telegram. On success, the bot
|
||||
replies with the absolute saved path and then uploads the media back to the chat.
|
||||
- Media groups are downloaded to temporary files and returned as a media group.
|
||||
- Download failures are printed to standard error.
|
||||
For every message containing a file (photo, video, audio, document, sticker, animation, voice,
|
||||
video note, etc.), the bot downloads the file to a local directory, then sends the file back to
|
||||
the chat. Media groups are expanded and each file is re-sent individually. While processing, the
|
||||
bot sends an appropriate "upload" chat action (e.g., *uploading video*, *uploading photo*).
|
||||
|
||||
The bot does not restrict users or chats. Run it with a dedicated output directory
|
||||
and appropriate filesystem quotas if it is exposed beyond trusted users.
|
||||
## Arguments
|
||||
|
||||
## Setup
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
| 2 *(optional)* | `/path/to/dir` | Directory where files are saved (defaults to `/tmp/`) |
|
||||
|
||||
Create a bot and provide its token as the first command-line argument. The process
|
||||
must have network access and permission to create and write to the output directory.
|
||||
The directory is created when absent and defaults to `/tmp/` when omitted.
|
||||
Optional flags (any order after the token):
|
||||
|
||||
No Telegram administrator rights are required in a private chat. For group use,
|
||||
configure the bot so that it receives the media messages you expect it to process.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Run
|
||||
## Bot Commands
|
||||
|
||||
From the repository root:
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Sends a usage instruction message |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Supports all Telegram media types: photos, videos, audio, documents, stickers, animations, voice messages, video notes
|
||||
- Handles media groups by iterating over each item and re-uploading it individually
|
||||
- Sends contextually appropriate chat actions during upload (typing, upload_video, upload_audio, etc.)
|
||||
- Logs the local file path after each download
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :FilesLoaderBot:run --args='<BOT_TOKEN>'
|
||||
./gradlew :FilesLoaderBot:run --args='<BOT_TOKEN> /absolute/output/directory'
|
||||
# Default directory (/tmp/)
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
|
||||
# Custom directory
|
||||
../gradlew run --args="BOT_TOKEN /path/to/save/dir"
|
||||
```
|
||||
|
||||
Arguments:
|
||||
|
||||
1. `BOT_TOKEN` (required): the Telegram bot token.
|
||||
2. `OUTPUT_DIRECTORY` (optional): the local destination directory; defaults to
|
||||
`/tmp/`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
|
||||
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
|
||||
import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo
|
||||
@@ -10,7 +10,6 @@ import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
||||
import dev.inmo.tgbotapi.types.actions.*
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||
import dev.inmo.tgbotapi.types.message.content.*
|
||||
@@ -20,10 +19,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Starts the long-polling media downloader and echo bot.
|
||||
*
|
||||
* [args] must contain the bot token and may contain an output directory as its
|
||||
* second item. The directory is created when necessary and defaults to `/tmp/`.
|
||||
* This bot will download incoming files
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val botToken = args.first()
|
||||
@@ -50,7 +46,6 @@ suspend fun main(args: Array<String>) {
|
||||
val action = when (content) {
|
||||
is PhotoContent -> UploadPhotoAction
|
||||
is AnimationContent,
|
||||
is LivePhotoContent,
|
||||
is VideoContent -> UploadVideoAction
|
||||
is StickerContent -> ChooseStickerAction
|
||||
is MediaGroupContent<*> -> UploadPhotoAction
|
||||
@@ -79,7 +74,7 @@ suspend fun main(args: Array<String>) {
|
||||
)
|
||||
is MediaGroupContent<*> -> replyWithMediaGroup(
|
||||
it,
|
||||
content.group.mapNotNull {
|
||||
content.group.map {
|
||||
when (val innerContent = it.content) {
|
||||
is AudioContent -> TelegramMediaAudio(
|
||||
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||
@@ -93,10 +88,6 @@ suspend fun main(args: Array<String>) {
|
||||
is VideoContent -> TelegramMediaVideo(
|
||||
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||
)
|
||||
is LivePhotoContent -> TelegramMediaLivePhoto(
|
||||
downloadFileToTemp(innerContent.media).asMultipartFile(),
|
||||
innerContent.media.photo ?.fileId ?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -116,16 +107,10 @@ suspend fun main(args: Array<String>) {
|
||||
it,
|
||||
outFile.asMultipartFile()
|
||||
)
|
||||
|
||||
is LivePhotoContent -> replyWithLivePhoto(
|
||||
it,
|
||||
outFile.asMultipartFile(),
|
||||
content.media.photo ?.fileId ?: error("Unable to resend live photo files without their photos")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||
}.second.join()
|
||||
}
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
# ForwardInfoSenderBot
|
||||
|
||||
This example uses long polling to inspect the forward metadata of every content message
|
||||
delivered to the bot and replies with a short description of its source.
|
||||
A bot that analyses the origin of forwarded messages and prints detailed information about the forwarder.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
There are no bot commands. Send or forward any content message that Telegram delivers to
|
||||
the bot. The reply depends on the message's `forwardInfo`:
|
||||
For every message that was forwarded to the bot, it inspects the forward metadata and sends back a
|
||||
formatted reply describing who or what originally sent the message: a regular user, a bot, a channel,
|
||||
or an anonymous/hidden sender. Premium status, user IDs, and usernames are included where available.
|
||||
|
||||
- messages without forward metadata produce `There is no forward info`;
|
||||
- anonymous forwards show the sender signature;
|
||||
- user and bot forwards show the sender type, numeric ID, name, and username when present;
|
||||
- channel forwards show the channel title, linked when the channel has a public username;
|
||||
- supergroup forwards show the group title;
|
||||
- messages sent on behalf of a channel show that channel's title.
|
||||
## Arguments
|
||||
|
||||
The response uses Telegram text entities to format identifiers and source names. The bot
|
||||
can only report metadata that Telegram includes in the received message.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Setup and permissions
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
Obtain a bot token and make sure the bot can receive the messages you want to inspect and
|
||||
send replies in that chat. It does not request administrator privileges or use persistent
|
||||
storage. Telegram's bot privacy and chat permissions still determine which group messages
|
||||
are delivered and whether the reply can be sent.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Run from the repository root:
|
||||
## Bot Commands
|
||||
|
||||
None.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Identifies forwarder type: regular user, bot, public channel, anonymous group admin, or hidden user
|
||||
- Displays premium user status, numeric IDs, and usernames using `code` and hyperlink entities
|
||||
- Re-sends the original message content alongside the metadata reply
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :ForwardInfoSenderBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The token is the required first application argument. Additional arguments are ignored;
|
||||
omitting the token makes startup fail. The process keeps polling until it is stopped.
|
||||
|
||||
@@ -13,11 +13,8 @@ import dev.inmo.tgbotapi.utils.regular
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Starts a long-polling bot that replies to each received content message with its forward-source metadata.
|
||||
*
|
||||
* Messages without forward metadata receive a corresponding fallback response.
|
||||
*
|
||||
* @param args the bot token as the required first element; any remaining elements are ignored
|
||||
* This bot will always return message about forwarder. In cases when sent message was not a forward message it will
|
||||
* send suitable message
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -1,46 +1,44 @@
|
||||
# GiftsBot
|
||||
|
||||
Demonstrates the paginated owned-gift APIs by listing gifts for the chat in which the command is received.
|
||||
A bot that retrieves and displays all gifts received by a user, chat, or business account.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
At startup the bot prints its `getMe` result, then receives updates through long polling. While handling `/start`, it
|
||||
shows a typing action, retrieves every page of gifts, and chooses the request from the command chat type:
|
||||
On `/start`, the bot fetches gifts from multiple sources (user gifts, chat gifts, business account
|
||||
gifts) and sends a formatted summary to the user. Each gift is described with its type (regular or
|
||||
unique, standard or business-owned) and relevant metadata.
|
||||
|
||||
- a business chat uses the business connection ID and requests the connected business account's gifts;
|
||||
- a private chat requests that user's gifts;
|
||||
- a public or unknown chat type requests that chat's gifts.
|
||||
## Arguments
|
||||
|
||||
Regular gifts are shown with their ID, optional text, and Stars cost. Unique gifts are shown with their optional ID,
|
||||
name, model, and number. Long results are split into multiple Telegram messages; an empty result produces
|
||||
`This chat have no any gifts`.
|
||||
| Position | Value | Sample | Description |
|
||||
|----------|-------|--------|-------------|
|
||||
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||
|
||||
## Command
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
- `/start` — lists the owned gifts selected by the current chat type. It must be the only command in the message and
|
||||
takes no arguments.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Other commands and non-command messages are ignored.
|
||||
## Bot Commands
|
||||
|
||||
## Setup and permissions
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Fetch and display all gifts for the requesting user |
|
||||
|
||||
1. Create a bot with BotFather and obtain its token.
|
||||
2. For private-chat use, have the user start the bot so it can receive `/start` and reply.
|
||||
3. For group or channel use, add the bot and allow it to receive the command and send messages in that chat.
|
||||
4. For business-chat use, enable the bot's Business/Secretary mode, connect it to the business account, and grant the
|
||||
**View gifts and Stars** (`can_view_gifts_and_stars`) business right.
|
||||
## Capabilities
|
||||
|
||||
The example performs no access checks or error recovery, so Telegram API or permission errors end that command
|
||||
handler.
|
||||
- Retrieves user gifts via `getUserGifts`
|
||||
- Retrieves chat gifts via `getChatGifts`
|
||||
- Retrieves business account gifts via `getBusinessAccountGifts`
|
||||
- Distinguishes between regular gifts and unique gifts
|
||||
- Distinguishes between standard (user-owned) and business-owned gifts
|
||||
- Paginates through the full gift list
|
||||
- Runs via long polling
|
||||
|
||||
## Arguments and launch
|
||||
|
||||
The first application argument is the required bot token. Optional, exact, case-sensitive flags may follow in either
|
||||
order: `debug` prints KSLog diagnostics to standard output, and `testServer` selects Telegram's Bot API test server.
|
||||
Unknown trailing arguments are ignored.
|
||||
|
||||
From the repository root:
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :GiftsBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@ import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.LogLevel
|
||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||
import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
||||
import dev.inmo.tgbotapi.extensions.api.gifts.getChatGiftsFlow
|
||||
@@ -25,17 +26,6 @@ import dev.inmo.tgbotapi.utils.buildEntities
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts a long-polling bot whose standalone `/start` command lists all owned gifts for the current chat.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags; unknown trailing
|
||||
* arguments are ignored
|
||||
* @throws NoSuchElementException when the required bot token is absent
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -115,7 +105,7 @@ suspend fun main(vararg args: String) {
|
||||
}
|
||||
}
|
||||
|
||||
// allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||
// allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||
// println(it)
|
||||
// }
|
||||
}.second.join()
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
# GiveawaysBot
|
||||
|
||||
A long-polling example that prints giveaway-related Telegram updates to standard output.
|
||||
A bot that monitors and logs all giveaway lifecycle events in chats.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
At startup, the bot calls `getMe` and prints its own user information. It then prints
|
||||
updates matched by these TelegramBotAPI handlers:
|
||||
Listens for Telegram giveaway service messages and logs each event to standard output. No
|
||||
interactive commands are provided; the bot is purely an observer/logger for giveaway activity.
|
||||
|
||||
- `onGiveawayCreated` — a giveaway was created;
|
||||
- `onGiveawayCompleted` — a giveaway was completed;
|
||||
- `onGiveawayWinners` — the giveaway winners were published;
|
||||
- `onGiveawayContent` — a message contains giveaway content.
|
||||
## Arguments
|
||||
|
||||
The bot sends no replies and defines no bot commands.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Setup and permissions
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
1. Obtain a bot token and keep it private.
|
||||
2. Add the bot to every chat whose giveaway updates it should observe, with enough
|
||||
access for Telegram to deliver those updates.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
The example does not call admin-only methods, store data, or configure a webhook.
|
||||
## Bot Commands
|
||||
|
||||
## Run
|
||||
None.
|
||||
|
||||
From the repository root:
|
||||
## Capabilities
|
||||
|
||||
- Detects and logs giveaway creation events (`GiveawayCreated`)
|
||||
- Detects and logs giveaway completion events with results (`Giveaway` with results)
|
||||
- Detects and logs winner announcement messages (`GiveawayWinners`)
|
||||
- Detects content messages that contain giveaway information (`GiveawayPublicResults`)
|
||||
- All events are printed to stdout for inspection
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :GiveawaysBot:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is always the required bot token. Optional, case-sensitive flags
|
||||
may follow it in either order:
|
||||
|
||||
- `debug` enables TelegramBotAPI diagnostic logging on standard output;
|
||||
- `testServer` connects to Telegram's Bot API test environment.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :GiveawaysBot:run --args="BOT_TOKEN debug testServer"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCompleted
|
||||
@@ -13,12 +13,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts a long-polling bot that prints its profile and giveaway-related updates.
|
||||
*
|
||||
* The first argument must be the bot token. The optional, case-sensitive `debug`
|
||||
* and `testServer` flags enable diagnostic logging and the Bot API test environment.
|
||||
*
|
||||
* @param args bot token followed by any optional flags
|
||||
* This place can be the playground for your code.
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
@@ -55,7 +50,7 @@ suspend fun main(vararg args: String) {
|
||||
println(it)
|
||||
}
|
||||
|
||||
// allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||
// allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
||||
// println(it)
|
||||
// }
|
||||
}.second.join()
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# GuestQueryBot
|
||||
|
||||
Demonstrates guest queries through long polling in chats where the bot is not a member.
|
||||
|
||||
## Behavior
|
||||
|
||||
At startup, the bot calls `getMe` and prints its bot information and the value of
|
||||
`supportsGuestQueries`.
|
||||
|
||||
For each guest request, it prints the query ID, caller, chat, and content, then
|
||||
answers with an inline article whose message contains:
|
||||
|
||||
```text
|
||||
Guest mode reply
|
||||
Query ID: <guest-query-id>
|
||||
```
|
||||
|
||||
For ordinary content messages carrying guest-call metadata, the bot also replies
|
||||
with the initiating user's name and/or the public chat's title. Every received
|
||||
update is printed to standard output. The bot defines no commands.
|
||||
|
||||
## Setup and permissions
|
||||
|
||||
1. Create a bot and obtain its token; keep the token private.
|
||||
2. Enable guest queries in BotFather so that `supports_guest_queries` is enabled.
|
||||
3. For ordinary messages outside guest mode, add the bot to the relevant chat and
|
||||
allow it to send messages there.
|
||||
|
||||
The guest-query flow does not require the bot to be a chat member. This example
|
||||
uses no admin-only methods and does not configure a webhook.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :GuestQueryBot:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required bot token. Optional, case-sensitive flags may
|
||||
follow it in either order:
|
||||
|
||||
- `debug` enables formatted library logging on standard output;
|
||||
- `testServer` connects to Telegram's Bot API test environment.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :GuestQueryBot:run --args="BOT_TOKEN debug testServer"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="GuestQueryBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGuestRequestMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.guest_bot_caller_chat
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.guest_bot_caller_user
|
||||
import dev.inmo.tgbotapi.extensions.utils.publicChatOrNull
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent
|
||||
import dev.inmo.tgbotapi.types.InlineQueryId
|
||||
import dev.inmo.tgbotapi.utils.buildEntities
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts the long-polling guest-query example.
|
||||
*
|
||||
* The first element of [args] must be the bot token. The optional, case-sensitive
|
||||
* values `debug` and `testServer` enable diagnostic logging and Telegram's test
|
||||
* environment, respectively. Guest requests receive an inline article response;
|
||||
* regular content messages with guest-caller metadata receive an acknowledgement.
|
||||
*/
|
||||
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")
|
||||
// supportsGuestQueries reflects the supports_guest_queries field from the Telegram API
|
||||
println("Supports guest queries: ${me.supportsGuestQueries}")
|
||||
|
||||
onGuestRequestMessage { message ->
|
||||
println("=== Guest message received ===")
|
||||
// guestQueryId is the unique ID required to answer this guest query
|
||||
println(" guestQueryId: ${message.guestQueryId}")
|
||||
println(" from: ${message.from}")
|
||||
println(" chat: ${message.chat}")
|
||||
println(" content: ${message.content}")
|
||||
|
||||
// reply() on GuestMessage calls answerGuestQuery internally and returns SentGuestMessage
|
||||
val sentGuestMessage = reply(
|
||||
message,
|
||||
InlineQueryResultArticle(
|
||||
id = InlineQueryId(message.guestQueryId.string),
|
||||
title = "Guest reply",
|
||||
inputMessageContent = InputTextMessageContent(
|
||||
buildEntities {
|
||||
+"Guest mode reply"
|
||||
+"\nQuery ID: "
|
||||
+message.guestQueryId.string
|
||||
}
|
||||
),
|
||||
description = "Reply to guest query from ${message.from.firstName}"
|
||||
)
|
||||
)
|
||||
// SentGuestMessage contains the inline_message_id of the sent reply
|
||||
println(" SentGuestMessage: $sentGuestMessage")
|
||||
}
|
||||
|
||||
onContentMessage {
|
||||
println(it)
|
||||
val userCalledGuestMessage = it.guest_bot_caller_user
|
||||
val chatCalledGuestMessage = it.guest_bot_caller_chat ?.publicChatOrNull()
|
||||
if (userCalledGuestMessage != null) {
|
||||
reply(it) {
|
||||
+"User called guest bot: ${userCalledGuestMessage.lastName + " " + userCalledGuestMessage.firstName}"
|
||||
}
|
||||
}
|
||||
if (chatCalledGuestMessage != null) {
|
||||
reply(it) {
|
||||
+"Chat called guest bot: ${chatCalledGuestMessage.title}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -1,43 +1,39 @@
|
||||
# HelloBot
|
||||
|
||||
HelloBot is a small long-polling example that greets the chat or sender when a
|
||||
message addresses the bot by username.
|
||||
A minimal bot that responds whenever someone mentions the bot's username in a chat.
|
||||
|
||||
## Trigger and replies
|
||||
## Functionality
|
||||
|
||||
There are no slash commands. The bot handles a content message only when its
|
||||
text contains the bot's full username. This is a case-sensitive substring
|
||||
check; messages without text or without the username are ignored.
|
||||
Listens for any message that contains the bot's username mention. When triggered, replies with
|
||||
`Oh, hi, ` followed by a mention of the sender (or the group/channel name for non-private chats).
|
||||
Uses MarkdownV2 formatting and adapts the reply text based on the chat type.
|
||||
|
||||
- In a private chat, it replies with a MarkdownV2 text mention of the user.
|
||||
- In a group or supergroup, it greets the group and links its title to a public
|
||||
username or invite link when one is available.
|
||||
- For a message sent to a group on behalf of a channel, it greets the sender
|
||||
channel instead.
|
||||
- In a channel, it greets the channel and includes the sender chat when Telegram
|
||||
supplies one.
|
||||
- In a business chat, it mentions the underlying private-chat user.
|
||||
## Arguments
|
||||
|
||||
Every received update is also printed to standard output for demonstration and
|
||||
debugging.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Setup
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
1. Create a bot with BotFather and keep its token private.
|
||||
2. Add the bot to each chat where it should respond. Explicit username mentions
|
||||
work with Telegram's normal group privacy mode.
|
||||
3. For channel posts, make the bot a channel administrator and allow it to post
|
||||
messages. Admin access may also make a private group invite link available;
|
||||
otherwise the group reply falls back to an unlinked title.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Run
|
||||
## Bot Commands
|
||||
|
||||
From the repository root, pass the token as the first positional argument:
|
||||
None. The bot is triggered by username mentions, not commands.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Detects mentions of the bot username in all chat types (private, group, supergroup, channel, business)
|
||||
- Builds a MarkdownV2-formatted reply that links back to the sender
|
||||
- For public chats the reply contains a clickable mention link; for private chats it uses a text mention
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :HelloBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The token is required. Additional command-line arguments are ignored. The bot
|
||||
runs until the process is stopped and uses long polling, so no webhook setup is
|
||||
needed.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
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
|
||||
@@ -18,13 +18,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts HelloBot with long polling and logs every received update.
|
||||
*
|
||||
* Content messages are handled when their text contains the bot's full
|
||||
* username. The reply varies for private, group, channel, and business chats.
|
||||
*
|
||||
* @param args the bot token at index `0`; any remaining arguments are ignored
|
||||
* @throws NoSuchElementException when no bot token is supplied
|
||||
* The main purpose of this bot is just to answer "Oh, hi, " and add user mention here
|
||||
*/
|
||||
@OptIn(PreviewFeature::class)
|
||||
suspend fun main(vararg args: String) {
|
||||
@@ -83,6 +77,6 @@ suspend fun main(vararg args: String) {
|
||||
MarkdownV2
|
||||
)
|
||||
}
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||
}.second.join()
|
||||
}
|
||||
|
||||
@@ -1,51 +1,41 @@
|
||||
# InlineQueriesBot
|
||||
|
||||
This Kotlin Multiplatform example answers inline queries with generated article results. It uses long polling and can be
|
||||
launched on the JVM or as a Kotlin/Native executable.
|
||||
A multiplatform bot that answers inline queries with paginated article results.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
For every inline query, the bot:
|
||||
Responds to inline queries by returning a page of article results. Each result includes a
|
||||
description and a deep-link button. Navigation between pages is handled via the query offset
|
||||
(next/previous buttons encoded in the result set).
|
||||
|
||||
- treats the numeric `offset` as a page number, defaulting to page `0` when the offset is absent or invalid;
|
||||
- returns a full page of numbered articles whose message text includes the user's query;
|
||||
- disables caching and marks the answer as personal;
|
||||
- provides the next numeric offset for pagination; and
|
||||
- adds a button that opens a `/start` deep link for the current page. The bot replies with that deep-link parameter.
|
||||
## Arguments
|
||||
|
||||
The bot also prints its own account information at startup, logs received updates, and prints polling exceptions.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Setup
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
Create a bot and obtain its token, then enable inline mode for it in BotFather (for example, with `/setinline`). Keep the
|
||||
token private.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Both launchers require the bot token as the first command-line argument. Starting either launcher without an argument
|
||||
fails immediately; additional arguments are ignored.
|
||||
## Bot Commands
|
||||
|
||||
## Launch from the repository root
|
||||
None. The bot is driven by inline queries (type `@BotUsername` in any chat).
|
||||
|
||||
### JVM
|
||||
## Capabilities
|
||||
|
||||
- Answers inline queries with `InlineQueryResultArticle` items
|
||||
- Offset-based pagination: each result page encodes the next-page offset in the answer
|
||||
- Each result includes a deep-link `InlineKeyboardButton` back to the bot
|
||||
- Multiplatform module with a shared `commonMain` implementation and a JVM launcher entry point
|
||||
- Requires *Inline Mode* to be enabled in BotFather settings
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :InlineQueriesBot:runJvm --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
### Kotlin/Native
|
||||
|
||||
The shared native configuration supports Linux x64/Arm64 and Windows x64 hosts. Build the debug executable with Gradle,
|
||||
then pass the token directly to the produced program:
|
||||
|
||||
```bash
|
||||
./gradlew :InlineQueriesBot:linkDebugExecutableNative
|
||||
./InlineQueriesBot/build/bin/native/debugExecutable/InlineQueriesBot.kexe "<BOT_TOKEN>"
|
||||
```
|
||||
|
||||
On Windows, run `InlineQueriesBot\build\bin\native\debugExecutable\InlineQueriesBot.exe "<BOT_TOKEN>"` after the same
|
||||
Gradle link task.
|
||||
|
||||
## Source sets
|
||||
|
||||
- `commonMain` contains `doInlineQueriesBot`, including the long-polling behavior and inline-query/deep-link handlers.
|
||||
- `jvmMain` provides the suspending JVM entry point.
|
||||
- `nativeMain` provides the native entry point and calls the shared suspending function with `runBlocking`.
|
||||
|
||||
@@ -14,7 +14,8 @@ import dev.inmo.tgbotapi.types.inlineQueryAnswerResultsLimit
|
||||
import dev.inmo.tgbotapi.utils.buildEntities
|
||||
|
||||
/**
|
||||
* Starts the inline-query bot with [token] and suspends until long polling stops.
|
||||
* Thi bot will create inline query answers. You
|
||||
* should enable inline queries in bot settings
|
||||
*/
|
||||
suspend fun doInlineQueriesBot(token: String) {
|
||||
val bot = telegramBot(token)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/** JVM entry point; [args] must contain the bot token as its first element. */
|
||||
suspend fun main(args: Array<String>) {
|
||||
doInlineQueriesBot(args.first())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/** Kotlin/Native entry point; [args] must contain the bot token as its first element. */
|
||||
fun main(args: Array<String>) {
|
||||
runBlocking {
|
||||
doInlineQueriesBot(args.first())
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# JoinRequestQueriesBot
|
||||
|
||||
A long-polling example for processing chat join-request queries as a chat's guard bot.
|
||||
|
||||
## Behavior
|
||||
|
||||
At startup, the bot calls `getMe` and prints its bot information and
|
||||
`supportsJoinRequestQueries` value. For every chat join request it prints the
|
||||
requesting user, chat, bio, query ID, and the chat's configured guard bot.
|
||||
|
||||
Only requests containing a query ID are processed. Without a Web App URL, the bot:
|
||||
|
||||
- answers with `Queue` when the user's bio is missing or blank, leaving the
|
||||
decision to other administrators;
|
||||
- answers with `Approve` when the user has a nonblank bio.
|
||||
|
||||
When an HTTPS Web App URL is supplied, the bot sends that Web App for verification
|
||||
instead of answering the query. Requests without a query ID are only logged. Every
|
||||
received update is printed to standard output, and the bot defines no commands.
|
||||
|
||||
## Setup and permissions
|
||||
|
||||
1. Create a bot, obtain its token, and keep the token private.
|
||||
2. Configure the bot as the guard bot of the chat whose requests it should handle.
|
||||
3. Grant it the administrator permission to invite users (`can_invite_users`).
|
||||
4. If using the Web App flow, provide an HTTPS verification URL.
|
||||
|
||||
The example uses long polling and does not configure a webhook. Be aware that its
|
||||
default flow automatically approves query-backed requests with a nonblank bio.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :JoinRequestQueriesBot:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required bot token. An optional Web App URL is recognized
|
||||
only as the second argument and must begin with `https://`. The case-sensitive flags
|
||||
`debug` and `testServer` enable formatted logging and Telegram's test environment;
|
||||
they may follow the token and Web App URL.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
./gradlew :JoinRequestQueriesBot:run --args="BOT_TOKEN https://example.com/verify debug"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="JoinRequestQueriesBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.invite_links.answerChatJoinRequestQuery
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.invite_links.sendChatJoinRequestWebApp
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatJoinRequest
|
||||
import dev.inmo.tgbotapi.requests.chat.invite_links.ChatJoinRequestQueryResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts the long-polling join-request-query example.
|
||||
*
|
||||
* The first element of [args] must be the bot token. When the second element is an
|
||||
* `https://` URL, query-backed requests are handed to that Web App. Otherwise, the
|
||||
* bot queues requests with a blank bio and approves those with a nonblank bio.
|
||||
* The optional exact values `debug` and `testServer` enable diagnostic logging and
|
||||
* Telegram's test environment, respectively.
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
val isDebug = args.any { it == "debug" }
|
||||
val isTestServer = args.any { it == "testServer" }
|
||||
// pass a https url as the second argument to demonstrate sendChatJoinRequestWebApp
|
||||
val webAppUrl = args.getOrNull(1) ?.takeIf { it.startsWith("https://") }
|
||||
|
||||
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")
|
||||
// supportsJoinRequestQueries reflects the supports_join_request_queries field from the Telegram API
|
||||
println("Supports join request queries: ${me.supportsJoinRequestQueries}")
|
||||
|
||||
onChatJoinRequest { request ->
|
||||
println("=== Chat join request received ===")
|
||||
println(" from: ${request.from}")
|
||||
println(" chat: ${request.chat}")
|
||||
println(" bio: ${request.bio}")
|
||||
// queryId is non-null only when the request arrives as a query to this bot as the guard bot
|
||||
println(" queryId: ${request.queryId}")
|
||||
|
||||
// guardBot is the bot processing join request queries in this chat (admins-only field)
|
||||
val guardBot = runCatching { getChat(request.chat).guardBot }.getOrNull()
|
||||
println(" guardBot: $guardBot")
|
||||
|
||||
val queryId = request.queryId
|
||||
if (queryId == null) {
|
||||
println(" -> request has no queryId, this bot is not the guard bot here")
|
||||
return@onChatJoinRequest
|
||||
}
|
||||
|
||||
if (webAppUrl != null) {
|
||||
// sendChatJoinRequestWebApp: hand the user a Web App (e.g. captcha) instead of deciding now
|
||||
sendChatJoinRequestWebApp(request, webAppUrl)
|
||||
println(" -> sent join request Web App: $webAppUrl")
|
||||
return@onChatJoinRequest
|
||||
}
|
||||
|
||||
// answerChatJoinRequestQuery with one of the ChatJoinRequestQueryResult variants:
|
||||
// Approve — allow the user to join
|
||||
// Decline — disallow the user to join
|
||||
// Queue — leave the decision to other administrators
|
||||
// Unknown — any future result not yet known to the library
|
||||
val result = if (request.bio.isNullOrBlank()) {
|
||||
// no bio -> let other admins decide
|
||||
ChatJoinRequestQueryResult.Queue
|
||||
} else {
|
||||
// has a bio -> approve
|
||||
ChatJoinRequestQueryResult.Approve
|
||||
}
|
||||
answerChatJoinRequestQuery(request, result)
|
||||
println(" -> answered with: ${result.name}")
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -25,11 +25,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
|
||||
/**
|
||||
* Parses pagination callback data whose first two space-separated fields are the page and total page count.
|
||||
*
|
||||
* @return the parsed page and count, or `null` when either field is missing or is not an integer
|
||||
*/
|
||||
fun String.parsePageAndCount(): Pair<Int, Int>? {
|
||||
val (pageString, countString) = split(" ").takeIf { it.count() > 1 } ?: return null
|
||||
return Pair(
|
||||
@@ -38,15 +33,6 @@ fun String.parsePageAndCount(): Pair<Int, Int>? {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the pagination controls used by command replies and inline-query results.
|
||||
*
|
||||
* The controls include nearby page callbacks, first/last-page jumps when applicable, a button that copies the
|
||||
* corresponding `/inline` command, and a button that starts inline mode for a user-selected chat.
|
||||
*
|
||||
* @param page the current page; callers should keep it within `1..count`
|
||||
* @param count the total number of pages; callers should pass a positive value
|
||||
*/
|
||||
fun InlineKeyboardBuilder.includePageButtons(page: Int, count: Int) {
|
||||
val numericButtons = listOfNotNull(
|
||||
page - 1,
|
||||
@@ -92,15 +78,6 @@ fun InlineKeyboardBuilder.includePageButtons(page: Int, count: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and runs the shared KeyboardsBot behavior using long polling.
|
||||
*
|
||||
* The bot serves `/inline` pagination keyboards, edits them in response to callback queries, answers compatible
|
||||
* inline queries, offers an `/inline` reply-keyboard button for unhandled commands, and logs every received update.
|
||||
*
|
||||
* @param token the Telegram bot token
|
||||
* @param print receives the bot information returned by the startup `getMe` request
|
||||
*/
|
||||
@OptIn(PreviewFeature::class)
|
||||
suspend fun activateKeyboardsBot(
|
||||
token: String,
|
||||
|
||||
@@ -4,12 +4,6 @@ import org.w3c.dom.*
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
/**
|
||||
* Installs the browser launch form after `DOMContentLoaded`.
|
||||
*
|
||||
* Every submission reads the token from `bot_token`, appends a result container under `bots_container`, and launches
|
||||
* [activateKeyboardsBot]. The result of its startup `getMe` request is rendered in that new container.
|
||||
*/
|
||||
fun main() {
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
|
||||
@@ -1,66 +1,47 @@
|
||||
# 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 bot (JVM + JS) that demonstrates inline keyboard pagination and various button types.
|
||||
|
||||
## Bot behavior
|
||||
## Functionality
|
||||
|
||||
At startup, the bot calls `getMe`, reports the returned bot information through the platform launcher, registers `/inline` with Telegram, and starts long polling. Every received update is also printed to the JVM terminal or browser developer console.
|
||||
On `/inline <page> <count>`, the bot sends an inline keyboard built from `count` items starting
|
||||
at `page`. The keyboard includes previous/next navigation buttons, copy-text buttons, styled
|
||||
action buttons, and an inline-query chosen-chat button. Callback queries from the buttons navigate
|
||||
between pages.
|
||||
|
||||
### Commands
|
||||
## Arguments
|
||||
|
||||
| Command | Result |
|
||||
| --- | --- |
|
||||
| `/inline` | Opens page `1` of a `10`-page inline keyboard. |
|
||||
| `/inline <count>` | Opens page `1` with the supplied total page count. |
|
||||
| `/inline <page> <count>` | Opens the supplied page with the supplied total page count. |
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
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.
|
||||
Optional flags (any order):
|
||||
|
||||
The generated inline keyboard contains:
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
- numbered buttons for the current page and any adjacent pages that are within `1..count`;
|
||||
- styled jump buttons for moving toward the first or last page when applicable;
|
||||
- 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.
|
||||
## Bot Commands
|
||||
|
||||
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.
|
||||
| Command | Arguments | Description |
|
||||
|---------|-----------|-------------|
|
||||
| `/inline` | `<page> <count>` | Send a paginated inline keyboard starting at `page` with `count` items per page |
|
||||
|
||||
Any command not handled above, including `/start`, receives a one-time reply keyboard containing a styled `/inline` button. Ordinary non-command messages are ignored.
|
||||
## Capabilities
|
||||
|
||||
### Inline mode
|
||||
|
||||
With inline mode enabled, a query beginning with a page and count, such as `@YourBot 2 10`, returns one **Send buttons** article. Sending that result posts an inline-mode message with the same pagination keyboard.
|
||||
|
||||
## Telegram setup
|
||||
|
||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
2. Enable inline mode for the bot with BotFather's `/setinline` command. Direct `/inline` commands work without it, but inline queries and **Send somebody page** require it.
|
||||
3. Run only one launcher for a token at a time. The bot receives updates through long polling and automatically removes an existing webhook when it starts.
|
||||
|
||||
The browser launcher handles the token in client-side code. Use it only from a trusted local page, do not expose the page publicly with a token filled in, and close the page when the bot should stop.
|
||||
- Multi-page inline keyboard navigation (previous / next buttons encoded as callback data)
|
||||
- Copy-text buttons (`CopyTextButton`)
|
||||
- Styled action buttons: Primary, Success, Danger colour variants
|
||||
- Inline query chosen-chat button (`SwitchInlineQueryChosenChat`)
|
||||
- Answers inline queries that originate from the keyboard buttons
|
||||
- Shared `commonMain` library with JVM and JS launchers
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
Run the commands below from the repository root.
|
||||
|
||||
### JVM
|
||||
|
||||
The first argument is the required bot token. An optional argument exactly equal to `debug` enables formatted KSLog output; the token must remain first.
|
||||
|
||||
```bash
|
||||
./gradlew :KeyboardsBot:jvm_launcher:run --args="<BOT_TOKEN>"
|
||||
./gradlew :KeyboardsBot:jvm_launcher:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
```bash
|
||||
./gradlew :KeyboardsBot:jvm_launcher:run --args="<BOT_TOKEN> debug"
|
||||
```
|
||||
|
||||
### Browser/JS
|
||||
|
||||
Start the Kotlin/JS browser development run:
|
||||
|
||||
```bash
|
||||
./gradlew :KeyboardsBot:KeyboardsBotLib:jsBrowserDevelopmentRun
|
||||
```
|
||||
|
||||
Enter the bot token in the displayed form and press **Start bot**. The page displays the result of `getMe`; raw updates and other console output appear in the browser developer console. Each form submission starts another bot instance, so submit the token only once.
|
||||
|
||||
@@ -5,12 +5,6 @@ import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Runs [activateKeyboardsBot] on the JVM and prints its startup bot information to standard output.
|
||||
*
|
||||
* @param args the bot token as the first element and, optionally, `debug` in a later element to enable formatted
|
||||
* KSLog output
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val isDebug = args.any { it == "debug" }
|
||||
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
# LinkPreviewsBot
|
||||
|
||||
A long-polling example that resends text-bearing content with every demonstrated
|
||||
`LinkPreviewOptions` variant.
|
||||
A bot that demonstrates all `LinkPreviewOptions` variants by replying with multiple messages, each
|
||||
using a different link preview style.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
The bot handles every content message. It searches the message's text entities for
|
||||
the first plain URL or text-link entity. When one is found, it sends the same text
|
||||
and entities to the same chat seven times:
|
||||
When the user sends a message containing a URL, the bot extracts the URL and sends several reply
|
||||
messages, each with a different `LinkPreviewOptions` configuration: disabled, small preview above
|
||||
text, large preview above text, small preview below text, large preview below text, and the default
|
||||
(no explicit options).
|
||||
|
||||
- with link previews disabled;
|
||||
- with a large preview above the text;
|
||||
- with a large preview below the text;
|
||||
- with a small preview above the text;
|
||||
- with a small preview below the text;
|
||||
- with Telegram's default preview size above the text;
|
||||
- with Telegram's default preview size below the text.
|
||||
## Arguments
|
||||
|
||||
The detected URL is selected explicitly for each enabled preview. If the content is
|
||||
not text-bearing or contains no URL entity, the bot replies that only content with
|
||||
a URL is supported. It defines and registers no commands.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Setup and permissions
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
1. Create a bot, obtain its token, and keep the token private.
|
||||
2. Start a private chat with it, or add it to a chat where previews should be tested.
|
||||
3. Ensure Telegram delivers the relevant content messages and the bot may send
|
||||
messages in that chat.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
The example uses no administrator-only methods and does not configure a webhook.
|
||||
## Bot Commands
|
||||
|
||||
## Run
|
||||
None.
|
||||
|
||||
From the repository root:
|
||||
## Capabilities
|
||||
|
||||
- Extracts URLs from the text entities of incoming messages
|
||||
- Sends one reply per `LinkPreviewOptions` variant:
|
||||
- Preview disabled
|
||||
- Small image, positioned above text
|
||||
- Large image, positioned above text
|
||||
- Small image, positioned below text
|
||||
- Large image, positioned below text
|
||||
- Default (Telegram-chosen behaviour)
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :LinkPreviewsBot:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required bot token. The optional, case-sensitive second
|
||||
argument `debug` enables formatted library logging on standard output. Arguments
|
||||
after the second are ignored.
|
||||
|
||||
```bash
|
||||
./gradlew :LinkPreviewsBot:run --args="BOT_TOKEN debug"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
@@ -15,12 +15,7 @@ import dev.inmo.tgbotapi.types.message.content.TextedContent
|
||||
import dev.inmo.tgbotapi.utils.regular
|
||||
|
||||
/**
|
||||
* Starts a long-polling bot that demonstrates link-preview layouts.
|
||||
*
|
||||
* The first element of [args] must be the bot token. An optional exact `debug`
|
||||
* value in the second position enables diagnostic logging. For each text-bearing
|
||||
* content message, the bot uses its first URL entity to send one disabled-preview
|
||||
* copy and large, small, and default previews both above and below the text.
|
||||
* This bot will reply with the same
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
# LiveLocationsBot
|
||||
|
||||
A long-polling example that sends, updates, and stops a live-location message.
|
||||
A bot that sends a live location and updates it periodically until the user cancels.
|
||||
|
||||
## Commands and behavior
|
||||
## Functionality
|
||||
|
||||
- `/start` begins a live-location sequence in the command's chat.
|
||||
- `Cancel`, an inline button on the live-location message, stops that sequence.
|
||||
On `/start`, the bot sends a live location message with an inline *Cancel* button. A coroutine then
|
||||
updates the location every 3 seconds with a slightly changing coordinate. When the user presses
|
||||
*Cancel* the update loop is stopped and the live location is closed.
|
||||
|
||||
The generated coordinates are synthetic: the first update uses latitude and
|
||||
longitude `(0.0, 0.0)`, and both values increase by `1.0` for each later update.
|
||||
The bot emits an update immediately and then every three seconds.
|
||||
## Arguments
|
||||
|
||||
While the sequence runs, the bot tracks its current location message. It accepts
|
||||
only callback data equal to `cancel` from that same message. After a matching button
|
||||
press, it cancels the update job, stops the live location, and removes the button.
|
||||
| Position | Value | Sample | Description |
|
||||
|----------|-------|--------|-------------|
|
||||
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||
|
||||
Every received update is printed to standard output. The `/start` handler is not
|
||||
separately registered in Telegram's command menu.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
## Setup and permissions
|
||||
| Value | Sample | Description |
|
||||
|-------|--------|-------------|
|
||||
| `debug` | `debug` | Enable verbose debug logging |
|
||||
| `testServer` | `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
1. Create a bot, obtain its token, and keep the token private.
|
||||
2. Start a private chat with the bot, or add it to a group where the demo should run.
|
||||
3. Allow the bot to send location messages in that chat.
|
||||
## Bot Commands
|
||||
|
||||
The coordinates do not come from the user's device. The example needs no
|
||||
administrator-only methods and does not configure a webhook.
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Sends a live location message and starts the position update loop |
|
||||
|
||||
## Run
|
||||
## Capabilities
|
||||
|
||||
From the repository root:
|
||||
- Sends an initial live location using `sendLiveLocation`
|
||||
- Updates the location every 3 seconds via `editLiveLocation` in a background coroutine
|
||||
- Inline keyboard with a *Cancel* callback button
|
||||
- Handles the cancel callback to stop the update loop and close the live location
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :LiveLocationsBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required bot token. Omitting it causes startup to fail;
|
||||
additional arguments are ignored. Stop the process with `Ctrl+C`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.EditLiveLocationInfo
|
||||
import dev.inmo.tgbotapi.extensions.api.edit.location.live.stopLiveLocation
|
||||
import dev.inmo.tgbotapi.extensions.api.handleLiveLocation
|
||||
@@ -18,12 +18,7 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
/**
|
||||
* Starts the long-polling live-location example.
|
||||
*
|
||||
* The first element of [args] must be the bot token; later elements are ignored.
|
||||
* Each `/start` command begins a synthetic location at `(0.0, 0.0)`, advances both
|
||||
* coordinates every three seconds, and stops when the current message's `Cancel`
|
||||
* button is pressed.
|
||||
* This bot will send you live location and update it from time to time
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
@@ -66,6 +61,7 @@ suspend fun main(vararg args: String) {
|
||||
stopLiveLocation(it, replyMarkup = null)
|
||||
}
|
||||
}
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||
}.second.join()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# LivePhotosBot
|
||||
|
||||
This long-polling example demonstrates receiving, sending, grouping, editing, and selling Telegram Live Photos. It also shows how a regular photo and video from one album can be reused as the two parts of a Live Photo.
|
||||
|
||||
## Behavior, commands, and triggers
|
||||
|
||||
The bot defines no commands. It prints every incoming update to standard output in addition to the trigger-specific output below.
|
||||
|
||||
| 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`. |
|
||||
| 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 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.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
1. Create a bot with BotFather and obtain its token.
|
||||
2. Add the bot to the chat where you want to exercise the example and allow it to send messages and media.
|
||||
3. In a group, make the bot an administrator or disable Group Privacy Mode so it receives ordinary, non-command media albums and Live Photos.
|
||||
4. To complete the standalone Live Photo flow, use a channel and grant the bot permission to post there: Telegram restricts `sendPaidMedia` to channel chats. In other chat types, the initial resend may succeed but the paid-media request can fail before the edit runs.
|
||||
|
||||
The program does not request or validate permissions itself. API errors use the library's normal handling.
|
||||
|
||||
## Arguments
|
||||
|
||||
The first argument is the required bot token. Optional flags are exact and case-sensitive, can follow the token in either order, and unknown extra arguments are ignored.
|
||||
|
||||
| Argument | Effect |
|
||||
| --- | --- |
|
||||
| `<BOT_TOKEN>` | Bot token. Omitting it fails before polling starts. |
|
||||
| `debug` | Prints formatted KSLog diagnostics to standard output. |
|
||||
| `testServer` | Uses Telegram's Bot API test environment. |
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :LivePhotosBot:run --args="<BOT_TOKEN>"
|
||||
```
|
||||
|
||||
For example, to enable both optional flags:
|
||||
|
||||
```bash
|
||||
./gradlew :LivePhotosBot:run --args="<BOT_TOKEN> debug testServer"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="LivePhotosBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.edit.media.editMessageMedia
|
||||
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
|
||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||
import dev.inmo.tgbotapi.extensions.api.send.replyWithLivePhoto
|
||||
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.onEditedLivePhoto
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onLivePhoto
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onLivePhotoGallery
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMediaGroupMessages
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPaidMediaInfoContent
|
||||
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.photoContentOrNull
|
||||
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.types.message.content.LivePhotoContent
|
||||
import dev.inmo.tgbotapi.types.message.payments.PaidMedia
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramPaidMediaLivePhoto
|
||||
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaLivePhoto
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaContent
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
|
||||
import dev.inmo.tgbotapi.types.message.content.VideoContent
|
||||
import dev.inmo.tgbotapi.utils.RiskFeature
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts the long-polling example for receiving, sending, grouping, editing, and selling Live Photos.
|
||||
*
|
||||
* @param args bot token followed by the optional, case-sensitive `debug` and `testServer` flags; unknown trailing
|
||||
* arguments are ignored
|
||||
*/
|
||||
@OptIn(RiskFeature::class)
|
||||
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
|
||||
) {
|
||||
|
||||
// Demonstrates: LivePhoto class (LivePhotoFile), live_photo field in Message, sendLivePhoto,
|
||||
// InputMediaLivePhoto (TelegramMediaLivePhoto), InputPaidMediaLivePhoto (TelegramPaidMediaLivePhoto),
|
||||
// editMessageMedia with live photo
|
||||
onLivePhoto { message ->
|
||||
// message.content is LivePhotoContent — this is the live_photo field of Message
|
||||
val content: LivePhotoContent = message.content
|
||||
|
||||
// content.media is LivePhotoFile — the LivePhoto class (photo + short video in one file)
|
||||
val livePhotoFile = content.media
|
||||
println("=== Live photo received ===")
|
||||
println(" fileId: ${livePhotoFile.fileId}")
|
||||
println(" fileUniqueId: ${livePhotoFile.fileUniqueId}")
|
||||
println(" width: ${livePhotoFile.width}")
|
||||
println(" height: ${livePhotoFile.height}")
|
||||
println(" duration: ${livePhotoFile.duration}s")
|
||||
println(" photo (thumb): ${livePhotoFile.photo?.fileId}")
|
||||
println(" mimeType: ${livePhotoFile.mimeType}")
|
||||
println(" fileSize: ${livePhotoFile.fileSize}")
|
||||
println(" caption: ${content.text}")
|
||||
|
||||
// sendLivePhoto: resend the received live photo back using LivePhotoFile overload
|
||||
val sent = sendLivePhoto(
|
||||
chatId = message.chat.id,
|
||||
livePhoto = livePhotoFile,
|
||||
text = "Resent via sendLivePhoto"
|
||||
)
|
||||
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)"
|
||||
)
|
||||
|
||||
// editMessageMedia with InputMediaLivePhoto (TelegramMediaLivePhoto):
|
||||
// edit the previously sent message to replace it with itself via TelegramMediaLivePhoto
|
||||
val sentAsMedia = sent.withContentOrNull<LivePhotoContent>()
|
||||
if (sentAsMedia != null) {
|
||||
editMessageMedia(
|
||||
message = sentAsMedia,
|
||||
// TelegramMediaLivePhoto is InputMediaLivePhoto
|
||||
media = TelegramMediaLivePhoto(
|
||||
file = livePhotoFile.fileId,
|
||||
photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId,
|
||||
text = "Edited via editMessageMedia with TelegramMediaLivePhoto"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates: sendMediaGroup with live photos, InputMediaLivePhoto (TelegramMediaLivePhoto)
|
||||
onLivePhotoGallery { mediaGroupContent ->
|
||||
println("=== Live photo gallery received (${mediaGroupContent.group.size} items) ===")
|
||||
mediaGroupContent.group.forEach { groupMember ->
|
||||
val livePhotoFile = groupMember.content.media
|
||||
println(" - fileId: ${livePhotoFile.fileId}, ${livePhotoFile.width}x${livePhotoFile.height}")
|
||||
}
|
||||
|
||||
// sendMediaGroup with TelegramMediaLivePhoto (InputMediaLivePhoto)
|
||||
sendMediaGroup(
|
||||
chatId = mediaGroupContent.group.first().sourceMessage.chat.id,
|
||||
media = mediaGroupContent.group.map { groupMember ->
|
||||
val livePhotoFile = groupMember.content.media
|
||||
// TelegramMediaLivePhoto is InputMediaLivePhoto — used here in sendMediaGroup
|
||||
TelegramMediaLivePhoto(
|
||||
file = livePhotoFile.fileId,
|
||||
photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Demonstrates: PaidMediaLivePhoto (PaidMedia.LivePhoto) in received paid media content
|
||||
onPaidMediaInfoContent { message ->
|
||||
val paidMedia = message.content.paidMediaInfo.media
|
||||
val livePhotos = paidMedia.filterIsInstance<PaidMedia.LivePhoto>()
|
||||
if (livePhotos.isNotEmpty()) {
|
||||
println("=== Paid media with live photos received ===")
|
||||
livePhotos.forEach { paidLivePhoto ->
|
||||
// paidLivePhoto is PaidMedia.LivePhoto — PaidMediaLivePhoto class
|
||||
val livePhotoFile = paidLivePhoto.livePhoto
|
||||
println(" - fileId: ${livePhotoFile.fileId}, ${livePhotoFile.width}x${livePhotoFile.height}")
|
||||
println(" duration: ${livePhotoFile.duration}s")
|
||||
}
|
||||
reply(message, "Received ${livePhotos.size} paid live photo(s)")
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates: live_photo field in edited messages (EditedMessage with LivePhotoContent)
|
||||
onEditedLivePhoto { message ->
|
||||
println("=== Edited live photo received ===")
|
||||
println(" fileId: ${message.content.media.fileId}")
|
||||
println(" caption: ${message.content.text}")
|
||||
}
|
||||
|
||||
onMediaGroupMessages {
|
||||
val photo = it.content.group.firstNotNullOfOrNull {
|
||||
it.content.photoContentOrNull()
|
||||
} ?: return@onMediaGroupMessages
|
||||
val video = it.content.group.firstNotNullOfOrNull {
|
||||
it.content.videoContentOrNull()
|
||||
} ?: return@onMediaGroupMessages
|
||||
replyWithLivePhoto(
|
||||
it,
|
||||
video.media.fileId,
|
||||
photo.media.fileId
|
||||
)
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -1,53 +1,47 @@
|
||||
# ManagedBotsBot
|
||||
|
||||
A long-polling playground for creating and administering managed bots, inspecting a
|
||||
user's personal-channel messages, and trying bot-to-bot messages.
|
||||
A bot that demonstrates the Managed Bots API: creating child bots and replacing their tokens.
|
||||
|
||||
## Commands and triggers
|
||||
## Functionality
|
||||
|
||||
- `/start` prints the triggering update, context data, and full chat information;
|
||||
it sends no reply.
|
||||
- `/canManageBots` replies whether `getMe` reports that this bot can manage bots.
|
||||
- `/keyboard` sends a one-time keyboard for creating a managed bot with suggested
|
||||
name `SampleName` and username `@some_sample_bot`.
|
||||
- `/replaceToken`, when sent as a reply to a managed-bot-created service message,
|
||||
replaces that bot's token and replies with the new token.
|
||||
- `/get_bot_access_settings <botId>` shows whether access is restricted and lists
|
||||
allowed users when present.
|
||||
- `/set_bot_access_settings <botId> [userId ...]` restricts access to the supplied
|
||||
numeric user IDs; omitting user IDs opens access to everyone.
|
||||
- `/get_personal_messages` lists up to ten messages from the current private-chat
|
||||
user's linked personal channel.
|
||||
- `/send_to_bot @username [text]` sends text to another bot; omitted text defaults
|
||||
to `Hello from bot-to-bot communication!`.
|
||||
Allows the operator to check whether the bot supports managed bots, create new managed bots via a
|
||||
keyboard button, and replace an existing managed bot's token. When a managed bot is created its
|
||||
token is sent back to the operator. The bot also demonstrates custom middleware and subcontext usage.
|
||||
|
||||
Managed-bot-created and managed-bot-updated events report the bot and numeric ID,
|
||||
then retrieve and send its token and access settings. Every update and every API
|
||||
request result is also printed to standard output. Commands are not registered in
|
||||
Telegram's command menu.
|
||||
## Arguments
|
||||
|
||||
## Setup, permissions, and safety
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
1. Create a bot, obtain its token, and verify `/canManageBots` replies `Yes`.
|
||||
2. Use a private test chat for the managed-bot and personal-channel examples.
|
||||
3. Link a personal channel before using `/get_personal_messages`.
|
||||
4. For `/send_to_bot`, enable bot-to-bot communication for both bots in BotFather.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
This example exposes managed-bot tokens in chat and logs API results. Use disposable
|
||||
test bots, keep the chat and process output private, and rotate any exposed token.
|
||||
No chat-administrator permission is requested by the code.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Run
|
||||
## Bot Commands
|
||||
|
||||
The intended command from the repository root is:
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/canManageBots` | Check whether this bot has the ability to create managed bots |
|
||||
| `/keyboard` | Send a reply keyboard with a *Create managed bot* button |
|
||||
| `/replaceToken` | Replace the token of a managed bot (send as reply to the bot's token message) |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Queries bot capabilities via `getMe` extended fields
|
||||
- Creates a managed child bot via the `BotKeyboardButton` with `RequestBot` type
|
||||
- Receives the new bot's info in a `BotShared` service message
|
||||
- Replaces a managed bot's token via `replaceStickerInSet` (token replacement API)
|
||||
- Handles `ManagedBotUpdated` events for tracking child bot status changes
|
||||
- Custom request middleware for logging
|
||||
- Custom `BehaviourContext` subcontext
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :ManagedBotsBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required token. Optional exact flags may follow it in any
|
||||
order: `debug` enables formatted logging, and `testServer` uses Telegram's test API.
|
||||
|
||||
Known issue: `build.gradle` currently sets `mainClassName` to `CustomBotKt`, while
|
||||
this source's entry point is `ManagedBotsBotKt`; the `run` task cannot start until
|
||||
that Gradle setting is corrected.
|
||||
|
||||
@@ -5,20 +5,15 @@ 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.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.getUserPersonalChatMessages
|
||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||
import dev.inmo.tgbotapi.extensions.api.managed_bots.getManagedBotAccessSettings
|
||||
import dev.inmo.tgbotapi.extensions.api.managed_bots.getManagedBotToken
|
||||
import dev.inmo.tgbotapi.extensions.api.managed_bots.replaceManagedBotToken
|
||||
import dev.inmo.tgbotapi.extensions.api.managed_bots.setManagedBotAccessSettings
|
||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||
import dev.inmo.tgbotapi.extensions.api.send.sendMessage
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
||||
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.onCommandWithArgs
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onManagedBotCreated
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onManagedBotUpdated
|
||||
import dev.inmo.tgbotapi.extensions.utils.chatEventMessageOrNull
|
||||
@@ -27,12 +22,10 @@ import dev.inmo.tgbotapi.extensions.utils.managedBotCreatedOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatReplyKeyboard
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.requestManagedBotButton
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.RawChatId
|
||||
import dev.inmo.tgbotapi.types.Username
|
||||
import dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestManagedBot
|
||||
import dev.inmo.tgbotapi.types.buttons.PreparedKeyboardButtonId
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.request.RequestId
|
||||
import dev.inmo.tgbotapi.types.toChatId
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
@@ -43,17 +36,12 @@ private var BehaviourContextData.update: Update?
|
||||
get() = get("update") as? Update
|
||||
set(value) = set("update", value)
|
||||
|
||||
private var BehaviourContextData.commonMessage: ChatContentMessage<*>?
|
||||
get() = get("commonMessage") as? ChatContentMessage<*>
|
||||
private var BehaviourContextData.commonMessage: CommonMessage<*>?
|
||||
get() = get("commonMessage") as? CommonMessage<*>
|
||||
set(value) = set("commonMessage", value)
|
||||
|
||||
/**
|
||||
* Starts the long-polling managed-bot playground.
|
||||
*
|
||||
* The first element of [args] must be the bot token. Optional exact values `debug`
|
||||
* and `testServer` enable diagnostic logging and Telegram's test environment.
|
||||
* The handlers expose managed-bot creation, tokens, access settings, personal-chat
|
||||
* messages, and bot-to-bot messaging; API results and all updates are logged.
|
||||
* This place can be the playground for your code.
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
@@ -126,19 +114,19 @@ suspend fun main(vararg args: String) {
|
||||
}
|
||||
|
||||
onManagedBotCreated {
|
||||
val botChatId = it.chatEvent.bot.id.toChatId()
|
||||
reply(it, "Managed bot created successfully: ${it.chatEvent.bot}\nBot ID: ${botChatId.chatId.long}")
|
||||
val token = getManagedBotToken(botChatId)
|
||||
val accessSettings = getManagedBotAccessSettings(botChatId)
|
||||
reply(it, "Token: $token; Access settings: $accessSettings")
|
||||
reply(it, "Managed bot created successfully: ${it.chatEvent.bot}")
|
||||
val token = getManagedBotToken(
|
||||
it.chatEvent.bot.id.toChatId()
|
||||
)
|
||||
reply(it, "Token: $token")
|
||||
}
|
||||
|
||||
onManagedBotUpdated {
|
||||
val botChatId = it.bot.id.toChatId()
|
||||
send(it.user, "Managed bot has been updated: ${it.bot}\nBot ID: ${botChatId.chatId.long}")
|
||||
val token = getManagedBotToken(botChatId)
|
||||
val accessSettings = getManagedBotAccessSettings(botChatId)
|
||||
send(it.user, "Token: $token; Access settings: $accessSettings")
|
||||
send(it.user, "Managed bot has been updated: ${it.bot}")
|
||||
val token = getManagedBotToken(
|
||||
it.bot.id.toChatId()
|
||||
)
|
||||
send(it.user, "Token: $token")
|
||||
}
|
||||
|
||||
onCommand("replaceToken") {
|
||||
@@ -148,71 +136,6 @@ suspend fun main(vararg args: String) {
|
||||
reply(it, "Token in replace update: ${replaceManagedBotToken(managedBotCreated.bot.id.toChatId())}")
|
||||
}
|
||||
|
||||
// getManagedBotAccessSettings — show BotAccessSettings: who can access the given managed bot
|
||||
// Usage: /get_bot_access_settings <botId>
|
||||
onCommandWithArgs("get_bot_access_settings") { message, args ->
|
||||
val botId = args.firstOrNull()?.toLongOrNull()?.let(::RawChatId)?.toChatId()
|
||||
?: run { reply(message, "Usage: /get_bot_access_settings <botId>\n(Bot ID shown after /keyboard → create bot)"); return@onCommandWithArgs }
|
||||
val settings = runCatching { getManagedBotAccessSettings(botId) }.getOrElse {
|
||||
reply(message, "Error: ${it.message}"); return@onCommandWithArgs
|
||||
}
|
||||
reply(message, buildString {
|
||||
append("Access settings for managed bot $botId:\n")
|
||||
append(" isAccessRestricted: ${settings.isAccessRestricted}\n")
|
||||
if (settings.addedUsers != null) {
|
||||
append(" allowedUsers: ${settings.addedUsers!!.joinToString { "${it.firstName} (${it.id})" }}")
|
||||
} else {
|
||||
append(" allowedUsers: all (unrestricted)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// setManagedBotAccessSettings — restrict access to a list of user IDs, or open to all
|
||||
// Usage: /set_bot_access_settings <botId> [userId1 userId2 ...]
|
||||
// Omit userIds to open access to all users (addedUserIds = null)
|
||||
onCommandWithArgs("set_bot_access_settings") { message, args ->
|
||||
val botId = args.firstOrNull()?.toLongOrNull()?.let(::RawChatId)?.toChatId()
|
||||
?: run { reply(message, "Usage: /set_bot_access_settings <botId> [userId1 userId2 ...]"); return@onCommandWithArgs }
|
||||
val allowedIds = args.drop(1).mapNotNull { it.toLongOrNull()?.let(::RawChatId)?.toChatId() }
|
||||
val addedUserIds: List<ChatId>? = allowedIds.ifEmpty { null }
|
||||
runCatching {
|
||||
setManagedBotAccessSettings(botId, addedUserIds)
|
||||
}.onSuccess {
|
||||
reply(message, if (addedUserIds == null) "Access opened to all users." else "Access restricted to ${addedUserIds.size} user(s).")
|
||||
}.onFailure {
|
||||
reply(message, "Error: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// getUserPersonalChatMessages — get recent messages from the user's personal channel
|
||||
// Works only if the user has a personal channel linked to their account
|
||||
onCommand("get_personal_messages") {
|
||||
val msg = it
|
||||
val userId = msg.chat.id.toChatId()
|
||||
val messages = runCatching { getUserPersonalChatMessages(userId, limit = 10) }.getOrElse { e ->
|
||||
reply(msg, "Error: ${e.message}"); return@onCommand
|
||||
}
|
||||
reply(msg, "Personal channel messages (${messages.size}):\n" +
|
||||
messages.joinToString("\n") { m -> " [${m.messageId}] ${m.content::class.simpleName}" }
|
||||
.ifEmpty { " (none)" }
|
||||
)
|
||||
}
|
||||
|
||||
// Bot-to-bot communication: send a message to another bot by @username
|
||||
// Since TG Bot API 9.0: works if both bots have bot-to-bot communication enabled in BotFather
|
||||
onCommandWithArgs("send_to_bot") { message, args ->
|
||||
val usernameArg = args.firstOrNull() ?: run { reply(message, "Usage: /send_to_bot @username [text]"); return@onCommandWithArgs }
|
||||
val targetUsername = Username.prepare(usernameArg)
|
||||
val text = args.drop(1).joinToString(" ").ifEmpty { "Hello from bot-to-bot communication!" }
|
||||
runCatching {
|
||||
sendMessage(targetUsername, text)
|
||||
}.onSuccess {
|
||||
reply(message, "Message sent to $targetUsername")
|
||||
}.onFailure {
|
||||
reply(message, "Failed to send to $targetUsername: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||
println(it)
|
||||
}
|
||||
|
||||
@@ -1,49 +1,42 @@
|
||||
# MemberUpdatedWatcherBot
|
||||
|
||||
A long-polling example that watches Telegram `my_chat_member` and `chat_member` updates, logs membership transitions, and posts human-readable notifications in the affected chat. It has no commands and does not respond to ordinary messages.
|
||||
A bot that monitors all `ChatMemberUpdated` events and sends descriptive notifications to the chat.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
The bot handles these transitions:
|
||||
Watches for every member status change in all chats the bot is a member of: bot additions,
|
||||
admin promotions and demotions, user joins and leaves, and permission restriction changes.
|
||||
For each event the bot sends a human-readable message describing what changed.
|
||||
|
||||
- **Joined:** logs the old and new member-state types and sends `Welcome <first name>`.
|
||||
- **Left or removed:** logs the transition and sends `Goodbye <first name>`.
|
||||
- **Promoted:** logs the new administrator title and announces it. A promotion also matches the administrator-permissions-change handler, so it produces a second permissions-change notification.
|
||||
- **Demoted:** logs the transition and announces that the user was demoted back to member.
|
||||
- **Administrator permissions/title changed:** logs and sends the old and new member-state types.
|
||||
- **Newly restricted or restrictions changed:** logs and sends the old and new member-state types. Removing all restrictions is not handled separately.
|
||||
## Arguments
|
||||
|
||||
The bot also identifies updates about itself:
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
- when added, it asks the chat to grant it administrator permissions;
|
||||
- when promoted, it confirms that it can now watch other users;
|
||||
- when demoted, it warns that it can no longer watch other users.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
The general handlers do not exclude the bot's own updates. Adding, promoting, or demoting the bot can therefore also produce the corresponding generic welcome, promotion, permissions-change, or demotion messages.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Event details are always written to standard output with the `ChatMemberUpdates` log tag. Debug mode additionally routes the library's default KSLog output to standard output.
|
||||
## Bot Commands
|
||||
|
||||
## Telegram setup, permissions, and privacy
|
||||
None.
|
||||
|
||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
2. Add it to the group or supergroup that should be watched.
|
||||
3. Promote it to administrator. Telegram exposes updates about the bot's own membership without this step, but delivers `chat_member` updates about other users only to administrators. The long-polling setup requests that update type.
|
||||
4. Ensure the bot can send messages in the chat. If using the example in a channel, it also needs permission to post messages.
|
||||
## Capabilities
|
||||
|
||||
BotFather privacy mode may remain enabled: privacy mode controls which messages a bot receives in groups, not member-status updates. The bot does not need access to ordinary group messages.
|
||||
|
||||
The example uses long polling and automatically removes an existing webhook at startup. Run only one update consumer for the bot token at a time.
|
||||
- Detects when the bot itself is added to or removed from a chat and sends a greeting/farewell
|
||||
- Detects when the bot is promoted to or demoted from administrator
|
||||
- Detects when any user joins or leaves the chat
|
||||
- Detects when any user is promoted to or demoted from administrator
|
||||
- Detects granular permission changes (e.g., restrictions added or lifted)
|
||||
- Uses the `ChatMemberUpdated` extension functions introduced in TelegramBotAPI 18.0.0
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root, pass the bot token as the first application argument:
|
||||
|
||||
```bash
|
||||
./gradlew :MemberUpdatedWatcherBot:run --args="<BOT_TOKEN>"
|
||||
```
|
||||
|
||||
Add an argument exactly equal to `debug` after the token to enable formatted library logging. Other additional arguments are ignored.
|
||||
|
||||
```bash
|
||||
./gradlew :MemberUpdatedWatcherBot:run --args="<BOT_TOKEN> debug"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
@@ -12,15 +12,6 @@ import dev.inmo.tgbotapi.types.chat.member.*
|
||||
import dev.inmo.tgbotapi.utils.*
|
||||
|
||||
|
||||
/**
|
||||
* Runs a long-polling bot that logs selected chat-member transitions and announces them in the affected chat.
|
||||
*
|
||||
* Updates about the bot itself produce setup/status messages, while joins, departures, promotions, demotions,
|
||||
* administrator changes, and restriction changes produce member notifications.
|
||||
*
|
||||
* @param args the bot token as the first element and, optionally, `debug` in a later element to enable formatted
|
||||
* default KSLog output
|
||||
*/
|
||||
@OptIn(PreviewFeature::class)
|
||||
suspend fun main(args: Array<String>) {
|
||||
val token = args.first()
|
||||
@@ -104,4 +95,4 @@ suspend fun main(args: Array<String>) {
|
||||
send(it.chat.id, message)
|
||||
}
|
||||
}.join()
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,39 @@
|
||||
# MyBot
|
||||
|
||||
A long-polling example that prints information about the bot and lets Telegram users replace or remove its global profile photo.
|
||||
A bot that monitors messages containing its username and responds with a contextual link or mention.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
At startup, the application prints the results of `getMe` and `getChat` for the bot itself to standard output. It then handles two commands:
|
||||
|
||||
- `/setMyProfilePhoto` replies with `ok, send me new photo` and waits for the first photo sent in the same chat. It streams draft progress messages while downloading the photo to a temporary file, uploads it as a static bot profile photo, and replies when the change is complete. The photo may come from any user in that chat; it is not restricted to the user who sent the command.
|
||||
- `/removeMyProfilePhoto` removes the bot's current profile photo and confirms success. On failure, it prints the exception and sends a generic error reply.
|
||||
|
||||
There is no `/start` handler, and ordinary messages are ignored unless the bot is waiting for a photo after `/setMyProfilePhoto`.
|
||||
|
||||
## Setup and permissions
|
||||
|
||||
- Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
- No chat-administrator permission is required to change the bot's own profile photo. In a group, the bot still needs permission to send replies.
|
||||
- Prefer using this example in a private chat. Telegram's draft-message API is intended for private chats, and BotFather privacy mode can prevent an unrelated group photo from reaching the bot unless it is sent as a reply or privacy mode is disabled.
|
||||
- This example performs no authorization checks: anyone who can reach the commands can change or remove the bot's profile photo globally. Do not expose a production bot without adding access control.
|
||||
- Long polling automatically removes an existing webhook at startup. Run only one update consumer for the token at a time.
|
||||
Watches every incoming message for a mention of the bot's username. When found, it replies with a
|
||||
MarkdownV2-formatted message that includes a link or text mention pointing back to the originating
|
||||
chat or user, adapting the reply to the type of chat the message came from.
|
||||
|
||||
## Arguments
|
||||
|
||||
The first argument is always the required bot token. Optional arguments can follow it in any order:
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
- `debug` enables formatted default KSLog output.
|
||||
- `testServer` makes the behavior and long-polling client use Telegram's Bot API test environment. The initial `getMe` and `getChat` diagnostics currently use a separate default-server client.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
Other arguments are ignored. Argument matching is case-sensitive.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
None. The bot reacts to username mentions, not to commands.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Handles all chat types: private, group, supergroup, channel, business connection chats, channel groups
|
||||
- For public chats builds a `t.me/<username>` hyperlink; for private chats uses an inline text mention
|
||||
- Prints information about the originating chat using `getChat`
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
The intended command from the repository root is:
|
||||
|
||||
```bash
|
||||
./gradlew :MyBot:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
For example, to enable both optional modes:
|
||||
|
||||
```bash
|
||||
./gradlew :MyBot:run --args="<BOT_TOKEN> debug testServer"
|
||||
```
|
||||
|
||||
> **Known issue:** `MyBot/build.gradle` still declares the old `GetMeBotKt` main class, while the current source produces `MyBotKt`. Consequently, the `run` task cannot start until that build setting is corrected. It is left unchanged here because this example update is documentation-only.
|
||||
|
||||
@@ -26,13 +26,7 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Runs the MyBot profile-photo example using long polling.
|
||||
*
|
||||
* Startup bot information is printed to standard output. The bot then handles commands that replace its profile
|
||||
* photo from the next photo received in the same chat or remove its current profile photo.
|
||||
*
|
||||
* @param args the bot token first, followed optionally by `debug` for formatted KSLog output and/or `testServer` for
|
||||
* the Telegram Bot API test environment
|
||||
* This is one of the easiest bots - it will just print information about itself
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -1,57 +1,48 @@
|
||||
# PollsBot
|
||||
|
||||
A long-polling showcase for regular polls, quizzes, poll media, targeting, and
|
||||
poll-related updates. It registers all nine commands in Telegram's command menu.
|
||||
A bot that demonstrates creation and management of Telegram polls (anonymous, public, and quiz).
|
||||
|
||||
## Commands
|
||||
## Functionality
|
||||
|
||||
| Command | Behavior |
|
||||
| --- | --- |
|
||||
| `/anonymous` | Sends an anonymous poll with ten numbered options. |
|
||||
| `/public` | Sends a nonanonymous ten-option poll, allows added options, and hides results until closure. |
|
||||
| `/quiz` | Sends a nonanonymous, shuffled quiz with revoting and hidden results; it randomly collects zero to seven distinct correct options and allows multiple answers when needed. |
|
||||
| `/media_poll` | Adds location media to the question and venue/location media to options; replying to a sticker adds it as another option. |
|
||||
| `/quiz_media` | Asks where the Eiffel Tower is, with location question media, Paris as the answer, and venue explanation media. |
|
||||
| `/members_only` | Sends an anonymous Yes/No poll restricted to members. |
|
||||
| `/country_codes` | Sends an anonymous poll targeted to `US`, `DE`, and `JP`. |
|
||||
| `/single_option` | Sends a nonanonymous poll containing only `Got it`. |
|
||||
| `/link_poll` | Sends a nonanonymous poll whose first two options carry link media. |
|
||||
Creates polls on demand and tracks live answer updates. Users can reply to an existing poll message
|
||||
to add new options or remove the last option. Quiz polls are created with a random correct answer.
|
||||
Custom emoji stickers in poll options are supported.
|
||||
|
||||
`/anonymous`, `/public`, and `/quiz` may contain extra text; the first custom-emoji
|
||||
entity after the command is copied into the poll's text and options.
|
||||
## Arguments
|
||||
|
||||
## Poll lifecycle and triggers
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
The bot keeps an in-memory poll-ID-to-chat map for polls it sends. Poll answers
|
||||
produce a chat notification naming the answering user or voter chat. Poll updates
|
||||
report anonymity, media, member/country restrictions, quiz explanation media, and
|
||||
each option's votes and media.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
Added or deleted poll-option events produce replies containing the option text. A
|
||||
content message associated with a poll-option reply produces `Reply to poll option`
|
||||
on that option. Every received update is printed to standard output.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
There is no command to close a poll. Restarting the bot clears its routing map, so
|
||||
later answers and updates for earlier polls are no longer reported to their chats.
|
||||
## Bot Commands
|
||||
|
||||
## Setup and permissions
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/anonymous` | Create an anonymous poll |
|
||||
| `/public` | Create a public poll; users can add options by replying |
|
||||
| `/quiz` | Create a quiz poll with a randomly chosen correct answer |
|
||||
|
||||
Create a bot, keep its token private, and start it in a private chat or add it to a
|
||||
group where it may send messages and polls. No administrator-only methods or webhook
|
||||
configuration are used. Some poll targeting or media features require a Telegram
|
||||
chat and client that support them.
|
||||
All three commands accept an optional custom emoji ID as an extra argument to use in poll option text.
|
||||
|
||||
## Run
|
||||
## Capabilities
|
||||
|
||||
The intended command from the repository root is:
|
||||
- Mutex-protected in-memory poll registry to safely track concurrent updates
|
||||
- Live poll answer updates via `onPollUpdated` handler
|
||||
- Reply-based option management: reply to a poll message with text to add an option, or reply with `/remove` to delete the last option
|
||||
- Quiz polls: random correct answer selection, answer explanation included
|
||||
- Custom emoji in poll option text
|
||||
- Registers all three commands with Telegram (`setMyCommands`)
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :PollsBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required token. An optional exact `debug` argument in any
|
||||
later position enables formatted logging; other arguments are ignored.
|
||||
|
||||
Known issue: `build.gradle` currently names `HelloBotKt` as the main class, while
|
||||
this source's entry point is `PollsBotKt`; the `run` task cannot start until that
|
||||
Gradle setting is corrected.
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||
import dev.inmo.tgbotapi.extensions.api.send.polls.sendQuizPoll
|
||||
import dev.inmo.tgbotapi.extensions.api.send.polls.sendRegularPoll
|
||||
@@ -16,23 +16,14 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOp
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOptionDeleted
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollUpdates
|
||||
import dev.inmo.tgbotapi.extensions.utils.accessibleMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.chatContentMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.customEmojiTextSourceOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithArgsSources
|
||||
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||
import dev.inmo.tgbotapi.types.BotCommand
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollId
|
||||
import dev.inmo.tgbotapi.types.ReplyParameters
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaLink
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaLocation
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaSticker
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVenue
|
||||
import dev.inmo.tgbotapi.types.message.content.StickerContent
|
||||
import dev.inmo.tgbotapi.types.polls.InputPollOption
|
||||
import dev.inmo.tgbotapi.types.polls.PollAnswer
|
||||
import dev.inmo.tgbotapi.types.polls.QuizPoll
|
||||
import dev.inmo.tgbotapi.utils.buildEntities
|
||||
import dev.inmo.tgbotapi.utils.customEmoji
|
||||
import dev.inmo.tgbotapi.utils.regular
|
||||
@@ -44,13 +35,11 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Starts the long-polling poll-feature showcase.
|
||||
*
|
||||
* The first element of [args] must be the bot token. An optional exact `debug`
|
||||
* value in any later position enables diagnostic logging. The registered commands
|
||||
* create regular polls and quizzes with anonymity, media, audience restrictions,
|
||||
* custom emoji, and single-option variants; update handlers report answers, state
|
||||
* changes, option edits, and replies associated with poll options.
|
||||
* This bot will answer with anonymous or public poll and send message on
|
||||
* any update.
|
||||
*
|
||||
* * Use `/anonymous` to take anonymous regular poll
|
||||
* * Use `/public` to take public regular poll
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
@@ -184,150 +173,6 @@ suspend fun main(vararg args: String) {
|
||||
}
|
||||
}
|
||||
|
||||
// TelegramMediaLocation implements InputPollMedia and InputPollOptionMedia (InputMediaLocation)
|
||||
// TelegramMediaVenue implements InputPollMedia and InputPollOptionMedia (InputMediaVenue)
|
||||
// Both can be used as poll question media or as option media
|
||||
onCommand("media_poll") {
|
||||
val replySticker = it.replyTo ?.contentMessageOrNull() ?.withContentOrNull<StickerContent>() ?.content ?.media
|
||||
val sentPoll = sendRegularPoll(
|
||||
it.chat.id,
|
||||
buildEntities { regular("Which venue would you visit?") },
|
||||
listOfNotNull(
|
||||
// InputPollOptionMedia via TelegramMediaVenue (InputMediaVenue)
|
||||
InputPollOption(
|
||||
media = TelegramMediaVenue(
|
||||
latitude = 48.8566,
|
||||
longitude = 2.3522,
|
||||
title = "Eiffel Tower",
|
||||
address = "Champ de Mars, Paris"
|
||||
)
|
||||
) { regular("Eiffel Tower") },
|
||||
// InputPollOptionMedia via TelegramMediaLocation (InputMediaLocation)
|
||||
InputPollOption(
|
||||
media = TelegramMediaLocation(latitude = 51.5007, longitude = -0.1246)
|
||||
) { regular("Big Ben") },
|
||||
InputPollOption { regular("Neither") },
|
||||
replySticker ?.let {
|
||||
InputPollOption(media = TelegramMediaSticker(replySticker.fileId)) {
|
||||
regular("Your sticker")
|
||||
}
|
||||
}
|
||||
),
|
||||
isAnonymous = false,
|
||||
// InputMediaLocation as InputPollMedia — poll question media
|
||||
media = TelegramMediaLocation(latitude = 48.8566, longitude = 2.3522),
|
||||
replyParameters = ReplyParameters(it)
|
||||
)
|
||||
pollToChatMutex.withLock {
|
||||
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates InputPollMedia on quiz + new QuizPoll.explanationMedia field
|
||||
onCommand("quiz_media") {
|
||||
val sentPoll = sendQuizPoll(
|
||||
it.chat.id,
|
||||
questionEntities = buildEntities { regular("Where is the Eiffel Tower?") },
|
||||
options = listOf(
|
||||
InputPollOption { regular("Paris") },
|
||||
InputPollOption { regular("London") },
|
||||
InputPollOption { regular("Berlin") },
|
||||
),
|
||||
correctOptionIds = listOf(0),
|
||||
explanation = "The Eiffel Tower is in Paris, France.",
|
||||
isAnonymous = false,
|
||||
// InputMediaLocation as InputPollMedia — poll question media (new Poll.media field)
|
||||
media = TelegramMediaLocation(latitude = 48.8566, longitude = 2.3522),
|
||||
// explanationMedia is new on QuizPoll — media shown with quiz explanation
|
||||
explanationMedia = TelegramMediaVenue(
|
||||
latitude = 48.8566,
|
||||
longitude = 2.3522,
|
||||
title = "Eiffel Tower",
|
||||
address = "Champ de Mars, 5 Av. Anatole France, Paris"
|
||||
),
|
||||
replyParameters = ReplyParameters(it)
|
||||
)
|
||||
pollToChatMutex.withLock {
|
||||
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates Poll.membersOnly and the membersOnly sendPoll parameter
|
||||
onCommand("members_only") {
|
||||
val sentPoll = sendRegularPoll(
|
||||
it.chat.id,
|
||||
buildEntities { regular("Members-only poll") },
|
||||
listOf(
|
||||
InputPollOption { regular("Yes") },
|
||||
InputPollOption { regular("No") },
|
||||
),
|
||||
isAnonymous = true,
|
||||
membersOnly = true,
|
||||
replyParameters = ReplyParameters(it)
|
||||
)
|
||||
pollToChatMutex.withLock {
|
||||
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates Poll.countryCodes and the countryCodes sendPoll parameter
|
||||
onCommand("country_codes") {
|
||||
val sentPoll = sendRegularPoll(
|
||||
it.chat.id,
|
||||
buildEntities { regular("Country-targeted poll (US, DE, JP)") },
|
||||
listOf(
|
||||
InputPollOption { regular("Option A") },
|
||||
InputPollOption { regular("Option B") },
|
||||
),
|
||||
isAnonymous = true,
|
||||
countryCodes = listOf("US", "DE", "JP"),
|
||||
replyParameters = ReplyParameters(it)
|
||||
)
|
||||
pollToChatMutex.withLock {
|
||||
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates that minimum poll options count is now 1 (was 2 before)
|
||||
onCommand("single_option") {
|
||||
val sentPoll = sendRegularPoll(
|
||||
it.chat.id,
|
||||
buildEntities { regular("Acknowledge this notice") },
|
||||
listOf(
|
||||
InputPollOption { regular("Got it") },
|
||||
),
|
||||
isAnonymous = false,
|
||||
replyParameters = ReplyParameters(it)
|
||||
)
|
||||
pollToChatMutex.withLock {
|
||||
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrates TelegramMediaLink (InputMediaLink, Bot API 10.1) as poll option media.
|
||||
// Link is the only new poll media type in 10.1 and is allowed only as InputPollOptionMedia.
|
||||
onCommand("link_poll") {
|
||||
val sentPoll = sendRegularPoll(
|
||||
it.chat.id,
|
||||
buildEntities { regular("Pick your favourite resource") },
|
||||
listOf(
|
||||
// InputPollOptionMedia via TelegramMediaLink (InputMediaLink)
|
||||
InputPollOption(
|
||||
media = TelegramMediaLink("https://core.telegram.org/bots/api")
|
||||
) { regular("Bot API docs") },
|
||||
InputPollOption(
|
||||
media = TelegramMediaLink("https://github.com/InsanusMokrassar/ktgbotapi")
|
||||
) { regular("ktgbotapi") },
|
||||
InputPollOption { regular("None of these") },
|
||||
),
|
||||
isAnonymous = false,
|
||||
replyParameters = ReplyParameters(it)
|
||||
)
|
||||
pollToChatMutex.withLock {
|
||||
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
|
||||
}
|
||||
}
|
||||
|
||||
onPollAnswer {
|
||||
val chatId = pollToChat[it.pollId] ?: return@onPollAnswer
|
||||
|
||||
@@ -340,29 +185,14 @@ suspend fun main(vararg args: String) {
|
||||
onPollUpdates {
|
||||
val chatId = pollToChat[it.id] ?: return@onPollUpdates
|
||||
|
||||
// Poll.media — PollMedia attached to the poll question (new field)
|
||||
// Poll.membersOnly — whether poll is restricted to channel members (new field)
|
||||
// Poll.countryCodes — country restriction list (new field)
|
||||
// QuizPoll.explanationMedia — PollMedia attached to quiz explanation (new field)
|
||||
// PollOption.media — PollMedia attached to each option (new field)
|
||||
val pollInfo = buildString {
|
||||
append("[onPollUpdates] anonymous=${it.isAnonymous}")
|
||||
append(" | media=${it.media}")
|
||||
append(" | membersOnly=${it.membersOnly}")
|
||||
append(" | countryCodes=${it.countryCodes}")
|
||||
if (it is QuizPoll) {
|
||||
append(" | explanationMedia=${it.explanationMedia}")
|
||||
}
|
||||
append("\n options:")
|
||||
it.options.forEach { option ->
|
||||
append("\n ${option.text}: votes=${option.votes}, media=${option.media}")
|
||||
}
|
||||
when(it.isAnonymous) {
|
||||
false -> send(chatId, "[onPollUpdates] Public poll updated: ${it.options.joinToString()}")
|
||||
true -> send(chatId, "[onPollUpdates] Anonymous poll updated: ${it.options.joinToString()}")
|
||||
}
|
||||
send(chatId, pollInfo)
|
||||
}
|
||||
|
||||
onPollOptionAdded {
|
||||
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { pollMessage ->
|
||||
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.let { pollMessage ->
|
||||
reply(pollMessage) {
|
||||
+"Poll option added: \n"
|
||||
+it.chatEvent.optionTextSources
|
||||
@@ -370,7 +200,7 @@ suspend fun main(vararg args: String) {
|
||||
}
|
||||
}
|
||||
onPollOptionDeleted {
|
||||
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { pollMessage ->
|
||||
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.let { pollMessage ->
|
||||
reply(pollMessage) {
|
||||
+"Poll option deleted: \n"
|
||||
+it.chatEvent.optionTextSources
|
||||
@@ -380,7 +210,7 @@ suspend fun main(vararg args: String) {
|
||||
|
||||
onContentMessage {
|
||||
val replyPollOptionId = it.replyInfo ?.pollOptionId ?: return@onContentMessage
|
||||
it.replyTo ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { replied ->
|
||||
it.replyTo ?.accessibleMessageOrNull() ?.let { replied ->
|
||||
reply(replied, pollOptionId = replyPollOptionId) {
|
||||
+"Reply to poll option"
|
||||
}
|
||||
@@ -391,16 +221,8 @@ suspend fun main(vararg args: String) {
|
||||
BotCommand("anonymous", "Create anonymous regular poll"),
|
||||
BotCommand("public", "Create non anonymous regular poll"),
|
||||
BotCommand("quiz", "Create quiz poll with random right answer"),
|
||||
BotCommand("media_poll", "Poll with location/venue media on question and options"),
|
||||
BotCommand("quiz_media", "Quiz with media and explanationMedia on question/explanation"),
|
||||
BotCommand("members_only", "Poll restricted to channel members only (membersOnly)"),
|
||||
BotCommand("country_codes", "Poll targeted to US, DE, JP users (countryCodes)"),
|
||||
BotCommand("single_option", "Poll with 1 option (minimum is now 1, not 2)"),
|
||||
BotCommand("link_poll", "Poll with link media (TelegramMediaLink) on options"),
|
||||
)
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||
println(it)
|
||||
}
|
||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
||||
}.second.join()
|
||||
}
|
||||
|
||||
81
README.md
81
README.md
@@ -1,69 +1,34 @@
|
||||
# TelegramBotAPI examples
|
||||
# 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.
|
||||
This repository contains several examples of simple bots which are using TelegramBotAPI
|
||||
|
||||
## Running an example
|
||||
## How to use this repository
|
||||
|
||||
Run commands from the repository root and replace placeholders such as `<BOT_TOKEN>` and `<ADMIN_USER_ID>`. The table uses JVM launchers for multiplatform modules; their module READMEs also document browser and native targets where available.
|
||||
***TO RUN NATIVE TARGETS ON LINUX YOU SHOULD INSTALL CURL LIBRARY. FOR EXAMPLE: `sudo apt install libcurl4-gnutls-dev`***
|
||||
|
||||
The shortcuts include all mode tokens supported by each launcher. Remove `debug` to disable verbose logging and remove `testServer` to use Telegram's production Bot API. These are positional values inside `--args` and intentionally have no leading dashes: the current launchers recognize `debug`, not `--debug`, and `testServer`, not `--testServer`.
|
||||
This repository contains several important things:
|
||||
|
||||
Native targets on Linux require libcurl development files, for example:
|
||||
* Example subprojects
|
||||
* Commits
|
||||
* Structure
|
||||
|
||||
```bash
|
||||
sudo apt install libcurl4-gnutls-dev
|
||||
```
|
||||
### Example subproject
|
||||
|
||||
## Modules
|
||||
Each example subproject contains information about how to run this example and what is it
|
||||
doing. Usually, it is some simple thing like sending "hello" message to the user which
|
||||
wrote to the bot.
|
||||
|
||||
| Module | What it demonstrates | Launch shortcut |
|
||||
| --- | --- | --- |
|
||||
| [BoostsInfoBot](BoostsInfoBot/) | Requests a channel, lists the requesting user's boosts, and logs boost updates. | `./gradlew :BoostsInfoBot:run --args="<BOT_TOKEN> debug"` |
|
||||
| [BotSubscriptionsBot](BotSubscriptionsBot/) | Observes recurring Telegram Stars subscription state updates. | `./gradlew :BotSubscriptionsBot:run --args="<BOT_TOKEN> debug testServer"` |
|
||||
| [BusinessConnectionsBot](BusinessConnectionsBot/) | Manages a connected Business account, messages, Stars, gifts, stories, and checklists. | `./gradlew :BusinessConnectionsBot:run --args="<BOT_TOKEN> debug"` |
|
||||
| [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"` |
|
||||
| [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"` |
|
||||
| [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"` |
|
||||
| [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"` |
|
||||
| [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"` |
|
||||
| [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"` |
|
||||
| [PollsBot](PollsBot/) † | Sends regular polls, quizzes, poll media, and handles poll updates. | `./gradlew :PollsBot:run --args="<BOT_TOKEN> debug"` |
|
||||
| [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"` |
|
||||
| [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>"` |
|
||||
| [StickerSetHandler](StickerSetHandler/) | Creates and manages a sticker set owned by each private-chat user. | `./gradlew :StickerSetHandler:run --args="<BOT_TOKEN>"` |
|
||||
| [SuggestedPosts](SuggestedPosts/) | Handles channel direct messages and the suggested-post lifecycle. | `./gradlew :SuggestedPosts:run --args="<BOT_TOKEN> debug testServer"` |
|
||||
| [TagsBot](TagsBot/) | Sets chat-member tags, delegates tag management, and reads sender tags. | `./gradlew :TagsBot:run --args="<BOT_TOKEN> debug testServer"` |
|
||||
| [TopicsHandling](TopicsHandling/) | Exercises forum-topic and private-chat-topic actions and events. | `./gradlew :TopicsHandling:run --args="<BOT_TOKEN>"` |
|
||||
| [UserChatShared](UserChatShared/) | Requests users or chats through reply keyboards and handles the shared results. | `./gradlew :UserChatShared:run --args="<BOT_TOKEN> debug"` |
|
||||
| [WebApp](WebApp/) | Serves a Compose Web client and demonstrates Telegram Web App integration. | `./gradlew :WebApp:runJvm --args="<BOT_TOKEN> https://webapp.example 8080 debug testServer"` |
|
||||
| [WebHooks](WebHooks/) | Receives Telegram updates through a Ktor webhook server instead of long polling. | `./gradlew :WebHooks:run --args="<BOT_TOKEN> https://bot.example.com debug"` |
|
||||
### Commits
|
||||
|
||||
† These modules currently contain a stale Gradle `mainClassName` mapping, documented in their module README. The shown command is the intended launch command but will not start until that mapping is corrected.
|
||||
Commits can contains some things like migration onto new version (especially it is actual
|
||||
for major version changes), updates according to the new features in versions and
|
||||
different other things which usually more important in context of history or changes
|
||||
between library version
|
||||
|
||||
## Repository as a reference
|
||||
### Structure
|
||||
|
||||
The example structure can be used as a starting point, and the commit history is useful for seeing migrations between TelegramBotAPI versions. For new projects, consider the [Telegram Bot template](https://github.com/InsanusMokrassar/TelegramBotAPI-bot_template) or [Kotlin Multiplatform Project template](https://github.com/InsanusMokrassar/KotlinMultiplatformProjectTemplate).
|
||||
Structure of this repository fully representative (it is the reason why this repo
|
||||
contains multiplatform subprojects) and you can use it as some template (but I am strongly
|
||||
recommend you to use my
|
||||
[TelegramBot template](https://github.com/InsanusMokrassar/TelegramBotAPI-bot_template) or
|
||||
[Multiplatform Project template](https://github.com/InsanusMokrassar/KotlinMultiplatformProjectTemplate))
|
||||
|
||||
@@ -1,76 +1,53 @@
|
||||
# RandomFileSenderBot
|
||||
|
||||
This Kotlin Multiplatform example sends randomly selected local files in response to a Telegram command. It uses long
|
||||
polling and can run on the JVM or as a Kotlin/Native executable.
|
||||
A multiplatform bot (JVM + Native) that picks random files from a directory and sends them to the
|
||||
requester.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
The bot registers one command:
|
||||
|
||||
- `/send_file` requests one file;
|
||||
- `/send_file N` requests `N` files when `N` is a positive integer; and
|
||||
- a missing or non-numeric count defaults to one. Zero and negative counts select nothing and receive
|
||||
`Nothing selected :(`.
|
||||
|
||||
For each requested file, the picker starts at the configured root. A file root is selected directly; a directory root
|
||||
is searched by choosing one random child at each level until a file is reached. This is not a uniform choice among all
|
||||
files in an uneven directory tree, and the same file may be selected more than once. Zero-byte files and unsuccessful
|
||||
selections are retried. Consequently, a positive request can keep retrying indefinitely when no non-empty file is
|
||||
reachable.
|
||||
|
||||
One file is sent as a document. Multiple files are sent as document media groups, split at Telegram's maximum media
|
||||
group size. All sends enable Telegram's protected-content flag. The bot also prints its own account information at
|
||||
startup and prints polling exceptions.
|
||||
|
||||
## Setup and security
|
||||
|
||||
Create a bot with BotFather and obtain its token. Give the process read access to a dedicated directory containing only
|
||||
files that every bot user may receive, and pass that directory explicitly. The bot has no user or chat allowlist and no
|
||||
file-name or file-type filter; anyone able to send it the command can request files reachable through the configured
|
||||
tree. Protected content is not an access-control mechanism.
|
||||
|
||||
Keep the token private. These launchers accept it on the command line, where it may be retained in shell history or be
|
||||
visible to other local processes. Also avoid roots containing secrets or links to locations outside the intended tree.
|
||||
Picks one or more files at random from a specified directory and sends them to the user. Multiple
|
||||
files are batched into a media group. Files are sent as protected content. The bot is implemented
|
||||
as a shared library with separate JVM and Native launcher entry points.
|
||||
|
||||
## Arguments
|
||||
|
||||
Both launchers interpret arguments in the same order:
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
| 2 *(optional)* | `/path/to/dir` | Directory to pick files from (defaults to the current working directory) |
|
||||
|
||||
1. `BOT_TOKEN` (required). Omitting it fails immediately.
|
||||
2. `ROOT_PATH` (optional in code), either a file or directory. Relative paths are resolved from the process working
|
||||
directory; use an explicit absolute path for predictable behavior. The launchers pass an empty path when this
|
||||
argument is omitted, whose filesystem behavior differs by platform and is not a reliable working-directory default.
|
||||
Optional flags (any order):
|
||||
|
||||
Additional arguments are ignored.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Launch from the repository root
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/send_file` | Send 1 random file from the configured directory |
|
||||
| `/send_file N` | Send *N* random files from the configured directory |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Platform-specific random file selection (JVM uses `java.io.File`, Native uses POSIX directory API)
|
||||
- Groups multiple files into a single media group message when N > 1
|
||||
- Files are sent as protected content (forwarding disabled)
|
||||
- Multiplatform: shared logic in `commonMain`, launchers in `jvm_launcher` and `native_launcher`
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
### JVM
|
||||
|
||||
```bash
|
||||
./gradlew :RandomFileSenderBot:runJvm --args="<BOT_TOKEN> /absolute/path/to/files"
|
||||
./gradlew :RandomFileSenderBot:jvm_launcher:run --args="BOT_TOKEN /optional/path"
|
||||
```
|
||||
|
||||
The JVM picker uses `java.io.File`. A missing, empty, or unreadable directory produces no selection and therefore causes
|
||||
a positive request to keep retrying.
|
||||
|
||||
### Kotlin/Native
|
||||
|
||||
The shared native configuration selects Linux x64, Linux Arm64, or Windows x64 for the current host. macOS is not
|
||||
configured. Link the debug executable with Gradle, then pass the arguments directly to the generated program:
|
||||
### Native (after build)
|
||||
|
||||
```bash
|
||||
./gradlew :RandomFileSenderBot:linkDebugExecutableNative
|
||||
./RandomFileSenderBot/build/bin/native/debugExecutable/RandomFileSenderBot.kexe "<BOT_TOKEN>" "/absolute/path/to/files"
|
||||
./RandomFileSenderBot/native_launcher/build/bin/native/releaseExecutable/native_launcher.kexe BOT_TOKEN /optional/path
|
||||
```
|
||||
|
||||
On Windows, run
|
||||
`RandomFileSenderBot\build\bin\native\debugExecutable\RandomFileSenderBot.exe "<BOT_TOKEN>" "C:\path\to\files"`
|
||||
after the same Gradle link task. The native picker uses Okio; unlike the JVM picker, inaccessible or invalid paths may
|
||||
raise a filesystem exception that is printed by the polling exception handler.
|
||||
|
||||
## Source sets
|
||||
|
||||
- `commonMain` contains the picker contract and the long-polling bot behavior.
|
||||
- `jvmMain` implements recursive selection with `java.io.File` and provides the suspending JVM entry point.
|
||||
- `nativeMain` implements recursive selection with Okio and provides a `runBlocking` native entry point.
|
||||
|
||||
@@ -18,18 +18,14 @@ import dev.inmo.tgbotapi.types.mediaCountInMediaGroup
|
||||
|
||||
private const val command = "send_file"
|
||||
|
||||
/**
|
||||
* Selects a file by recursively choosing random children below [currentRoot].
|
||||
*
|
||||
* @return the selected file, or `null` when the picker cannot continue from the current root
|
||||
*/
|
||||
expect fun pickFile(currentRoot: MPPFile): MPPFile?
|
||||
|
||||
/**
|
||||
* Runs the long-polling random-file bot using [token] and serving selections rooted at [folder].
|
||||
*
|
||||
* `/send_file` selects one non-empty file, while `/send_file N` selects `N` files and splits them into valid Telegram
|
||||
* media-group sizes.
|
||||
* This bot will send files inside of working directory OR from directory in the second argument.
|
||||
* You may send /send_file command to this bot to get random file from the directory OR
|
||||
* `/send_file $number` when you want to receive required number of files. For example,
|
||||
* /send_file and `/send_file 1` will have the same effect - bot will send one random file.
|
||||
* But if you will send `/send_file 5` it will choose 5 random files and send them as group
|
||||
*/
|
||||
suspend fun doRandomFileSenderBot(token: String, folder: MPPFile) {
|
||||
val bot = telegramBot(token)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import java.io.File
|
||||
|
||||
/** JVM picker backed by [File], returning a file root directly or descending through random directory children. */
|
||||
actual fun pickFile(currentRoot: MPPFile): File? {
|
||||
if (currentRoot.isFile) {
|
||||
return currentRoot
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
|
||||
/** JVM entry point; [args] contains the bot token followed by an optional picker root. */
|
||||
suspend fun main(args: Array<String>) {
|
||||
doRandomFileSenderBot(args.first(), MPPFile(args.getOrNull(1) ?: ""))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import okio.FileSystem
|
||||
|
||||
/** Native picker backed by Okio, returning a file root directly or descending through random directory children. */
|
||||
actual fun pickFile(currentRoot: MPPFile): MPPFile? {
|
||||
if (FileSystem.SYSTEM.exists(currentRoot) && FileSystem.SYSTEM.listOrNull(currentRoot) == null) {
|
||||
return currentRoot
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.Path.Companion.toPath
|
||||
|
||||
/** Kotlin/Native entry point; [args] contains the bot token followed by an optional picker root. */
|
||||
fun main(args: Array<String>) {
|
||||
runBlocking {
|
||||
doRandomFileSenderBot(args.first(), args.getOrNull(1) ?.toPath() ?: "".toPath())
|
||||
|
||||
@@ -1,44 +1,41 @@
|
||||
# ReactionsInfoBot
|
||||
|
||||
A long-polling example that handles Telegram's per-user reaction updates and anonymous reaction-count updates. It has no commands and does not respond to ordinary messages.
|
||||
A bot that tracks message reactions and reports them back to the user.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
When an identifiable user changes their reactions on a message, the bot:
|
||||
|
||||
1. temporarily adds a `✍` reaction to the original message;
|
||||
2. sends that user a private message which externally replies to the reacted message;
|
||||
3. lists the user's new reaction set, including ordinary emoji, rendered custom emoji with its custom-emoji ID, a generic label for paid reactions, and fallback information for unknown reaction types;
|
||||
4. removes its temporary reaction after the private reply succeeds.
|
||||
|
||||
If the user removed all reactions, the private message contains only its heading. Updates where a chat is the reaction actor are not handled by this per-user trigger. Telegram also does not deliver reaction-change updates caused by bots, so the temporary `✍` reaction does not trigger this handler recursively.
|
||||
|
||||
For anonymous reaction-count updates, the bot fetches and prints the extended chat plus the raw count update to standard output. It does not send a Telegram message for those updates, which Telegram may deliver after a delay.
|
||||
|
||||
If adding `✍` is not allowed, the private-message step is not reached. If the private message fails, the final cleanup is not reached and the bot's temporary reaction can remain on the original message.
|
||||
|
||||
## Telegram setup, permissions, and privacy
|
||||
|
||||
- Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
- Add the bot to each group, supergroup, or channel to watch and promote it to administrator. Telegram requires administrator status for both `message_reaction` and `message_reaction_count` updates; the long-polling setup explicitly requests these update types.
|
||||
- Enable reactions in the watched chat and allow the `✍` emoji so the bot can apply its temporary marker.
|
||||
- Each user who should receive reports must first open the bot's private chat and press **Start**. The bot has no `/start` response, but Telegram otherwise prevents it from initiating a private conversation. Reports also fail if the user blocks the bot.
|
||||
- BotFather privacy mode may remain enabled. It controls group-message delivery, not reaction updates.
|
||||
|
||||
The example uses long polling and automatically removes an existing webhook at startup. Run only one update consumer for the token at a time.
|
||||
Monitors reaction updates in the bot's private chat. When a user adds or removes a reaction on a
|
||||
message, the bot replies to that message with a formatted summary of the current reactions.
|
||||
The bot also sets a reaction emoji on incoming messages to acknowledge them.
|
||||
|
||||
## Arguments
|
||||
|
||||
The first argument is the required bot token. Debug logging is enabled only when the second argument is exactly `debug`; later occurrences are ignored.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
None.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Handles `MessageReactionUpdated` events (per-user reaction changes)
|
||||
- Handles `MessageReactionCountUpdated` events (aggregate reaction counts)
|
||||
- Identifies reaction types: standard emoji, custom emoji, paid reactions
|
||||
- Replies to the reacted-to message with a formatted list of current reactions
|
||||
- Sets a reaction on received messages using `setMessageReaction`
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :ReactionsInfoBot:run --args="<BOT_TOKEN>"
|
||||
```
|
||||
|
||||
```bash
|
||||
./gradlew :ReactionsInfoBot:run --args="<BOT_TOKEN> debug"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
@@ -15,12 +15,7 @@ import dev.inmo.tgbotapi.utils.customEmoji
|
||||
import dev.inmo.tgbotapi.utils.regular
|
||||
|
||||
/**
|
||||
* Runs the ReactionsInfoBot example using long polling.
|
||||
*
|
||||
* User-attributed reaction changes are reported to the reacting user in a private cross-chat reply, with a temporary
|
||||
* `✍` reaction marking the source message. Anonymous reaction-count updates are printed to standard output.
|
||||
*
|
||||
* @param args the bot token first and, optionally, `debug` as the second element to enable formatted KSLog output
|
||||
* This bot will send info about user reactions in his PM with reply to message user reacted to
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -1,75 +1,48 @@
|
||||
# ResenderBot
|
||||
|
||||
A multiplatform long-polling example that recreates received content in the same
|
||||
chat. Shared behavior lives in `ResenderBotLib`; JVM, browser/JS, and native entry
|
||||
points start it in platform-specific ways.
|
||||
A multiplatform bot (JVM + Native + JS) that echoes every content message back to the sender.
|
||||
|
||||
## Resend behavior
|
||||
## Functionality
|
||||
|
||||
The bot defines no commands. For each content message, it shows a typing action and
|
||||
uses `createResend` to send equivalent content back to the originating chat. When
|
||||
present, it carries over the replied-to message metadata, text quote entities and
|
||||
position, and the message effect ID.
|
||||
For every content message the bot receives, it immediately re-sends the same content back to the
|
||||
originating chat. Reply quotes and message effects are preserved. The bot is implemented as a
|
||||
shared library (`ResenderBotLib`) with separate launcher modules for JVM and Native targets.
|
||||
|
||||
Business content sent by the business-connection owner is ignored; other delivered
|
||||
content messages are eligible. Processing is separated by chat. The bot prints each
|
||||
received update and each resend result, and reports its `getMe` result through the
|
||||
launcher's output callback.
|
||||
## Arguments
|
||||
|
||||
## Source sets and launchers
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
- `ResenderBotLib/commonMain` provides the public `activateResenderBot` function and
|
||||
all Telegram handlers for JVM, JS, and native consumers.
|
||||
- `ResenderBotLib/jsMain` provides a browser form. Each submission starts another
|
||||
bot and displays callback output in its own page element; console output remains
|
||||
in the browser developer tools.
|
||||
- `jvm_launcher` provides a suspending CLI entry point and optional debug logging.
|
||||
- `native_launcher` wraps the shared suspending function in `runBlocking`. Its
|
||||
shared native template selects Linux x64/Arm64 or Windows x64 for the host.
|
||||
Optional flags (any order):
|
||||
|
||||
## Setup and permissions
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Create a bot, obtain its token, and keep it private. Start a private chat with the
|
||||
bot or add it to a chat where Telegram will deliver the desired content. The bot
|
||||
must be allowed to send each content type it should reproduce; no administrator-only
|
||||
methods are used. Long polling is used, so run only one launcher per token.
|
||||
## Bot Commands
|
||||
|
||||
The browser form handles the token in client-side code and uses a plain text input.
|
||||
Use it only on a trusted local page, submit once, and close the page when finished.
|
||||
Linux native builds also require the repository's documented libcurl dependency.
|
||||
None.
|
||||
|
||||
## Run from the repository root
|
||||
## Capabilities
|
||||
|
||||
- Re-sends any content message (text, photo, video, audio, document, sticker, etc.)
|
||||
- Preserves `reply_to_message` quote when the original message was a reply
|
||||
- Preserves `effect_id` (message effects / animations)
|
||||
- Shared `commonMain` implementation across JVM, Native, and JS targets
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
### JVM
|
||||
|
||||
The first argument is the required token. `debug` is recognized only as the second
|
||||
argument; later arguments are ignored.
|
||||
|
||||
```bash
|
||||
./gradlew :ResenderBot:jvm_launcher:run --args="BOT_TOKEN"
|
||||
./gradlew :ResenderBot:jvm_launcher:run --args="BOT_TOKEN debug"
|
||||
```
|
||||
|
||||
### Browser/JS
|
||||
### Native (after build)
|
||||
|
||||
```bash
|
||||
./gradlew :ResenderBot:ResenderBotLib:jsBrowserDevelopmentRun
|
||||
./ResenderBot/native_launcher/build/bin/native/releaseExecutable/native_launcher.kexe BOT_TOKEN
|
||||
```
|
||||
|
||||
Enter the token in the page form and select **Start bot**. There are no browser
|
||||
command-line arguments.
|
||||
|
||||
### Kotlin/Native
|
||||
|
||||
Build the host-specific debug executable, then pass the required token directly to
|
||||
it. Additional native arguments are ignored.
|
||||
|
||||
```bash
|
||||
./gradlew :ResenderBot:native_launcher:linkDebugExecutableNative
|
||||
./ResenderBot/native_launcher/build/bin/native/debugExecutable/native_launcher.kexe "BOT_TOKEN"
|
||||
```
|
||||
|
||||
On Windows, run
|
||||
`ResenderBot\\native_launcher\\build\\bin\\native\\debugExecutable\\native_launcher.exe "BOT_TOKEN"`
|
||||
after the same Gradle link task. macOS hosts are not configured by the native
|
||||
launcher template.
|
||||
|
||||
@@ -20,16 +20,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
|
||||
/**
|
||||
* Starts the shared long-polling resender and suspends until polling stops.
|
||||
*
|
||||
* Eligible content is recreated in its source chat with reply/quote metadata and
|
||||
* message effects preserved. Business messages sent by the business-connection
|
||||
* owner are skipped.
|
||||
*
|
||||
* @param token bot token used for polling and API requests.
|
||||
* @param print output callback for bot information and resend diagnostics.
|
||||
*/
|
||||
suspend fun activateResenderBot(
|
||||
token: String,
|
||||
print: (Any) -> Unit
|
||||
|
||||
@@ -4,10 +4,6 @@ import org.w3c.dom.*
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
/**
|
||||
* Installs the browser token form and starts one resender for each submission.
|
||||
* Callback output from each instance is displayed in its associated page element.
|
||||
*/
|
||||
fun main() {
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
|
||||
@@ -3,12 +3,6 @@ import dev.inmo.kslog.common.LogLevel
|
||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||
import dev.inmo.kslog.common.setDefaultKSLog
|
||||
|
||||
/**
|
||||
* Starts the JVM resender launcher.
|
||||
*
|
||||
* [args] must contain the bot token first. An optional exact `debug` value in the
|
||||
* second position enables diagnostic logging; later elements are ignored.
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val isDebug = args.getOrNull(1) == "debug"
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Starts the native resender launcher with the first element of [args] as its bot
|
||||
* token. Later elements are ignored.
|
||||
*/
|
||||
fun main(vararg args: String) {
|
||||
runBlocking {
|
||||
activateResenderBot(args.first()) {
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# RichMessagesBot
|
||||
|
||||
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.
|
||||
|
||||
## Commands
|
||||
|
||||
The bot installs seven commands in Telegram's default command menu. Three
|
||||
additional handlers can be invoked by typing their commands manually.
|
||||
|
||||
| Command | In menu | Demonstration |
|
||||
| --- | --- | --- |
|
||||
| `/rich_html` | Yes | Sends the full HTML fixture: inline styles and links, references, emoji and time links, math, headings, lists and checkboxes, quotations, remote media and maps, collages, slideshows, tables, details, and captions. |
|
||||
| `/rich_markdown` | Yes | Sends and logs the corresponding Markdown fixture, including remote photo, video, audio, voice-note, animation, collage, and slideshow markup. |
|
||||
| `/rich_markdown_medialess` | No | Sends and logs the Markdown fixture without media, collages, or slideshows. |
|
||||
| `/rich_markdown_blocks` | No | Sends and logs the full fixture as a typed block tree, including first-class media blocks, captions, collages, and slideshows. |
|
||||
| `/rich_markdown_medialess_blocks` | No | Sends and logs the same typed block tree without its media section. |
|
||||
| `/rich_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_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.
|
||||
|
||||
## 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 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. |
|
||||
|
||||
Every received update is also printed to standard output. A rich message received
|
||||
while `/wait_rich` is active can therefore be observed by the waiter, the general
|
||||
rich-message trigger, and the filtered update flow.
|
||||
|
||||
## Media notes
|
||||
|
||||
The built-in HTML and Markdown fixtures refer to public files under
|
||||
`https://telegram.org/example/`. The typed full fixture constructs Telegram media
|
||||
from the same URLs. The photo trigger instead demonstrates reusing an existing
|
||||
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.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
1. Create a bot with BotFather and obtain its token. Keep the token out of source
|
||||
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
|
||||
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.
|
||||
5. Enable guest queries for the bot to exercise the `/rich_guest` guest-request
|
||||
path. The bot does not need to be a member of the target chat for that path.
|
||||
|
||||
No handler requires an administrator-only Bot API method. The program does not
|
||||
configure a webhook or validate chat permissions before making requests.
|
||||
|
||||
## Arguments
|
||||
|
||||
The first program argument is required and is always treated as the bot token.
|
||||
Optional flags are exact and case-sensitive, may follow the token in either
|
||||
order, and unknown later arguments are ignored.
|
||||
|
||||
| Argument | Effect |
|
||||
| --- | --- |
|
||||
| `<BOT_TOKEN>` | Token used to create the bot. Omitting it fails before polling starts. |
|
||||
| `debug` | Enables formatted KSLog diagnostics on standard output. |
|
||||
| `testServer` | Connects to Telegram's Bot API test environment. |
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :RichMessagesBot:run --args="<BOT_TOKEN>"
|
||||
```
|
||||
|
||||
For example, with both optional flags:
|
||||
|
||||
```bash
|
||||
./gradlew :RichMessagesBot:run --args="<BOT_TOKEN> debug testServer"
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName="RichMessagesBotKt"
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
|
||||
}
|
||||
@@ -1,926 +0,0 @@
|
||||
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.subscribeLoggingDropExceptions
|
||||
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
|
||||
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.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.onGuestRequestMessage
|
||||
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
|
||||
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
|
||||
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.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.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.TelegramMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
|
||||
import dev.inmo.tgbotapi.types.rich.InputRichMessage
|
||||
import dev.inmo.tgbotapi.types.rich.InputRichMessageBlocks
|
||||
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.RichBlockTableCellAlign
|
||||
import dev.inmo.tgbotapi.types.rich.RichBlockTableCellVAlign
|
||||
import dev.inmo.tgbotapi.types.rich.RichTextPlain
|
||||
import dev.inmo.tgbotapi.types.rich.buildRichText
|
||||
import dev.inmo.tgbotapi.types.toChatId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* Runs a long-polling showcase of the rich-message APIs introduced in Telegram Bot API 10.1 and 10.2.
|
||||
*
|
||||
* Outgoing [dev.inmo.tgbotapi.types.rich.InputRichMessage] values use one of three representations:
|
||||
* [InputRichMessageHTML], [InputRichMessageMarkdown], or a typed [InputRichMessageBlocks] tree of
|
||||
* [dev.inmo.tgbotapi.types.rich.InputRichBlock] values. The handlers demonstrate [sendRichMessage],
|
||||
* [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.
|
||||
*
|
||||
* Incoming [dev.inmo.tgbotapi.types.message.content.RichMessageContent] and user-selected content covers
|
||||
* [onRichMessage], [waitRichMessage], [onlyRichMessageContentMessages], photo reuse, and
|
||||
* [InputRichMessageContent] in inline and guest-query results. Parsed content is exposed as a
|
||||
* [dev.inmo.tgbotapi.types.rich.RichMessage] containing [dev.inmo.tgbotapi.types.rich.RichBlock]s.
|
||||
*
|
||||
* @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags. The token
|
||||
* must be present; unrecognized later arguments are ignored.
|
||||
*/
|
||||
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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val testMarkdownText = """
|
||||
**bold text**
|
||||
__bold text__
|
||||
*italic text*
|
||||
_italic text_
|
||||
~~strikethrough text~~
|
||||
`inline fixed-width code`
|
||||
==marked text==
|
||||
||spoiler||
|
||||
|
||||
[inline URL](https://t.me/)
|
||||
[inline e-mail](mailto:user@example.com)
|
||||
[inline phone number](tel:+123456789)
|
||||
[inline mention of a user](tg://user?id=123456789)
|
||||

|
||||

|
||||
${'$'}x^2 + y^2$
|
||||
\#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username
|
||||
all the text above was on the same line
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
Paragraph text
|
||||
|
||||
```python
|
||||
print('pre-formatted fixed-width code block written in the Python programming language')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- unordered list item
|
||||
* unordered list item
|
||||
+ unordered list item
|
||||
|
||||
1. ordered list item
|
||||
2. ordered list item
|
||||
|
||||
- [ ] task list item
|
||||
- [x] completed task list item
|
||||
|
||||
>Block quotation started
|
||||
>
|
||||
>Block quotation continued on the next line
|
||||
>Block quotation continued on the same line
|
||||
>
|
||||
>The last line of the block quotation
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
| Header 1 | Header 2 |
|
||||
|:---------|:--------:|
|
||||
| left | center |
|
||||
|
||||
Text with a reference[^id1] and another one[^id2].
|
||||
|
||||
[^id1]: Definition of the first footnote.
|
||||
[^id2]: Definition of the second footnote.
|
||||
|
||||
$${'$'}E = mc^2$$
|
||||
|
||||
```math
|
||||
E = mc^2
|
||||
```
|
||||
|
||||
## Example Nested Syntax Report for _Q1_
|
||||
Intro with <u>underlined text</u>, ==marked text==, and ${'$'}x^2 + y^2$.
|
||||
**Bold _italic <u>underlined italic bold</u> italic_ bold**
|
||||
<u>In inline tags, nested **markdown** is parsed</u>
|
||||
>Quote with **bold text, ~~strikethrough, and <tg-spoiler>spoiler</tg-spoiler>~~**, plus [a link](https://t.me/).
|
||||
|
||||
- List item with `code`, <sup>superscript</sup>, <sub>subscript</sub>, and a footnote[^note]
|
||||
- Another item with **bold <tg-spoiler><code>spoiler code</code></tg-spoiler>**
|
||||
- Another item with ~~strikethrough and <ins>inserted text</ins>~~
|
||||
|
||||
| Metric | Value |
|
||||
|:-------|------:|
|
||||
| Speed | **42** <sup>ms</sup> |
|
||||
| Status | <tg-spoiler>ready</tg-spoiler> |
|
||||
|
||||
[^note]: Footnote with _italic text_ and <u>HTML underline</u>.
|
||||
|
||||
---
|
||||
|
||||
# Details blocks can contain Markdown content:
|
||||
|
||||
<details open><summary>Summary with **bold text**</summary>
|
||||
|
||||
### Details heading
|
||||
- List item with _italic text_
|
||||
- List item with <tg-spoiler>spoiler</tg-spoiler>
|
||||
|
||||
</details>
|
||||
|
||||
# Collages and slideshows can contain Markdown media blocks:
|
||||
|
||||
<tg-collage>
|
||||
|
||||

|
||||

|
||||
|
||||
</tg-collage>
|
||||
|
||||
<tg-slideshow>
|
||||
|
||||

|
||||

|
||||
|
||||
</tg-slideshow>
|
||||
""".trimIndent()
|
||||
val testMarkdownMediaLessText = """
|
||||
**bold text**
|
||||
__bold text__
|
||||
*italic text*
|
||||
_italic text_
|
||||
~~strikethrough text~~
|
||||
`inline fixed-width code`
|
||||
==marked text==
|
||||
||spoiler||
|
||||
|
||||
[inline URL](https://t.me/)
|
||||
[inline e-mail](mailto:user@example.com)
|
||||
[inline phone number](tel:+123456789)
|
||||
[inline mention of a user](tg://user?id=123456789)
|
||||

|
||||

|
||||
${'$'}x^2 + y^2$
|
||||
\#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username
|
||||
all the text above was on the same line
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
Paragraph text
|
||||
|
||||
```python
|
||||
print('pre-formatted fixed-width code block written in the Python programming language')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- unordered list item
|
||||
* unordered list item
|
||||
+ unordered list item
|
||||
|
||||
1. ordered list item
|
||||
2. ordered list item
|
||||
|
||||
- [ ] task list item
|
||||
- [x] completed task list item
|
||||
|
||||
>Block quotation started
|
||||
>
|
||||
>Block quotation continued on the next line
|
||||
>Block quotation continued on the same line
|
||||
>
|
||||
>The last line of the block quotation
|
||||
|
||||
| Header 1 | Header 2 |
|
||||
|:---------|:--------:|
|
||||
| left | center |
|
||||
|
||||
Text with a reference[^id1] and another one[^id2].
|
||||
|
||||
[^id1]: Definition of the first footnote.
|
||||
[^id2]: Definition of the second footnote.
|
||||
|
||||
$${'$'}E = mc^2$$
|
||||
|
||||
```math
|
||||
E = mc^2
|
||||
```
|
||||
|
||||
## Example Nested Syntax Report for _Q1_
|
||||
Intro with <u>underlined text</u>, ==marked text==, and ${'$'}x^2 + y^2$.
|
||||
**Bold _italic <u>underlined italic bold</u> italic_ bold**
|
||||
<u>In inline tags, nested **markdown** is parsed</u>
|
||||
>Quote with **bold text, ~~strikethrough, and <tg-spoiler>spoiler</tg-spoiler>~~**, plus [a link](https://t.me/).
|
||||
|
||||
- List item with `code`, <sup>superscript</sup>, <sub>subscript</sub>, and a footnote[^note]
|
||||
- Another item with **bold <tg-spoiler><code>spoiler code</code></tg-spoiler>**
|
||||
- Another item with ~~strikethrough and <ins>inserted text</ins>~~
|
||||
|
||||
| Metric | Value |
|
||||
|:-------|------:|
|
||||
| Speed | **42** <sup>ms</sup> |
|
||||
| Status | <tg-spoiler>ready</tg-spoiler> |
|
||||
|
||||
[^note]: Footnote with _italic text_ and <u>HTML underline</u>.
|
||||
|
||||
---
|
||||
|
||||
# Details blocks can contain Markdown content:
|
||||
|
||||
<details open><summary>Summary with **bold text**</summary>
|
||||
|
||||
### Details heading
|
||||
- List item with _italic text_
|
||||
- List item with <tg-spoiler>spoiler</tg-spoiler>
|
||||
|
||||
</details>
|
||||
""".trimIndent()
|
||||
val testMarkdownMediaLessInputRichMessageBlocks = InputRichMessageBlocks {
|
||||
paragraph {
|
||||
bold("bold text")
|
||||
plain("\n")
|
||||
bold("bold text")
|
||||
plain("\n")
|
||||
italic("italic text")
|
||||
plain("\n")
|
||||
italic("italic text")
|
||||
plain("\n")
|
||||
strikethrough("strikethrough text")
|
||||
plain("\n")
|
||||
code("inline fixed-width code")
|
||||
plain("\n")
|
||||
marked("marked text")
|
||||
plain("\n")
|
||||
spoiler("spoiler")
|
||||
}
|
||||
paragraph {
|
||||
url("inline URL", "https://t.me/")
|
||||
plain("\n")
|
||||
email("inline e-mail", "user@example.com")
|
||||
plain("\n")
|
||||
phone("inline phone number", "+123456789")
|
||||
plain("\n")
|
||||
url("inline mention of a user", "tg://user?id=123456789")
|
||||
plain("\n")
|
||||
customEmoji(CustomEmojiId("5368324170671202286"), "👍")
|
||||
plain("\n")
|
||||
dateTime("22:45 tomorrow", TelegramDate(1647531900L), "wDT")
|
||||
plain("\n")
|
||||
mathematicalExpression("x^2 + y^2")
|
||||
plain("\n#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username\n")
|
||||
plain("all the text above was on the same line")
|
||||
}
|
||||
|
||||
h1("Heading 1")
|
||||
h2("Heading 2")
|
||||
h3("Heading 3")
|
||||
h4("Heading 4")
|
||||
h5("Heading 5")
|
||||
h6("Heading 6")
|
||||
paragraph("Paragraph text")
|
||||
preformatted(
|
||||
" print('pre-formatted fixed-width code block written in the Python programming language')",
|
||||
language = "python"
|
||||
)
|
||||
divider()
|
||||
|
||||
unorderedList {
|
||||
item("unordered list item")
|
||||
item("unordered list item")
|
||||
item("unordered list item")
|
||||
}
|
||||
orderedList {
|
||||
item(1) { paragraph("ordered list item") }
|
||||
item(2) { paragraph("ordered list item") }
|
||||
}
|
||||
unorderedList {
|
||||
item(hasCheckbox = true, isChecked = false) { paragraph("task list item") }
|
||||
item(hasCheckbox = true, isChecked = true) { paragraph("completed task list item") }
|
||||
}
|
||||
blockQuotation {
|
||||
paragraph("Block quotation started\nBlock quotation continued on the next line\nBlock quotation continued on the same line\nThe last line of the block quotation")
|
||||
}
|
||||
|
||||
table {
|
||||
row {
|
||||
headerCell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Top) { plain("Header 1") }
|
||||
headerCell(align = RichBlockTableCellAlign.Center, valign = RichBlockTableCellVAlign.Middle) { plain("Header 2") }
|
||||
headerCell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Bottom) { plain("Header 2") }
|
||||
}
|
||||
row {
|
||||
cell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Top) { plain("left") }
|
||||
cell(align = RichBlockTableCellAlign.Center, valign = RichBlockTableCellVAlign.Middle) { plain("center") }
|
||||
}
|
||||
}
|
||||
paragraph {
|
||||
plain("Text with a reference")
|
||||
referenceLink("id1", "id1")
|
||||
plain(" and another one")
|
||||
referenceLink("id2", "id2")
|
||||
plain(".")
|
||||
}
|
||||
paragraph { reference("Definition of the first footnote.", "id1") }
|
||||
paragraph { reference("Definition of the second footnote.", "id2") }
|
||||
mathematicalExpression("E = mc^2")
|
||||
preformatted("E = mc^2", language = "math")
|
||||
|
||||
h2 {
|
||||
plain("Example Nested Syntax Report for ")
|
||||
italic("Q1")
|
||||
}
|
||||
paragraph {
|
||||
plain("Intro with ")
|
||||
underline("underlined text")
|
||||
plain(", ")
|
||||
marked("marked text")
|
||||
plain(", and ")
|
||||
mathematicalExpression("x^2 + y^2")
|
||||
plain(".")
|
||||
}
|
||||
paragraph {
|
||||
bold {
|
||||
plain("Bold ")
|
||||
italic {
|
||||
plain("italic ")
|
||||
underline("underlined italic bold")
|
||||
plain(" italic")
|
||||
}
|
||||
plain(" bold")
|
||||
}
|
||||
}
|
||||
paragraph {
|
||||
underline {
|
||||
plain("In inline tags, nested ")
|
||||
bold("markdown")
|
||||
plain(" is parsed")
|
||||
}
|
||||
}
|
||||
blockQuotation {
|
||||
paragraph {
|
||||
plain("Quote with ")
|
||||
bold {
|
||||
plain("bold text, ")
|
||||
strikethrough {
|
||||
plain("strikethrough, and ")
|
||||
spoiler("spoiler")
|
||||
}
|
||||
}
|
||||
plain(", plus ")
|
||||
url("a link", "https://t.me/")
|
||||
plain(".")
|
||||
}
|
||||
}
|
||||
unorderedList {
|
||||
item {
|
||||
paragraph {
|
||||
plain("List item with ")
|
||||
code("code")
|
||||
plain(", ")
|
||||
superscript("superscript")
|
||||
plain(", ")
|
||||
subscript("subscript")
|
||||
plain(", and a footnote")
|
||||
referenceLink("note", "note")
|
||||
}
|
||||
}
|
||||
item {
|
||||
paragraph {
|
||||
plain("Another item with ")
|
||||
bold { spoiler { code("spoiler code") } }
|
||||
}
|
||||
}
|
||||
item {
|
||||
paragraph {
|
||||
plain("Another item with ")
|
||||
strikethrough {
|
||||
plain("strikethrough and ")
|
||||
underline("inserted text")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
row {
|
||||
headerCell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Middle) { plain("Metric") }
|
||||
headerCell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Middle) { plain("Value") }
|
||||
}
|
||||
row {
|
||||
cell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Middle) { plain("Speed") }
|
||||
cell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Middle) {
|
||||
bold("42")
|
||||
plain(" ")
|
||||
superscript("ms")
|
||||
}
|
||||
}
|
||||
row {
|
||||
cell(align = RichBlockTableCellAlign.Left, valign = RichBlockTableCellVAlign.Middle) { plain("Status") }
|
||||
cell(align = RichBlockTableCellAlign.Right, valign = RichBlockTableCellVAlign.Middle) { spoiler("ready") }
|
||||
}
|
||||
}
|
||||
paragraph {
|
||||
reference("note") {
|
||||
plain("Footnote with ")
|
||||
italic("italic text")
|
||||
plain(" and ")
|
||||
underline("HTML underline")
|
||||
plain(".")
|
||||
}
|
||||
}
|
||||
divider()
|
||||
h1("Details blocks can contain Markdown content:")
|
||||
details(
|
||||
summary = buildRichText {
|
||||
plain("Summary with ")
|
||||
bold("bold text")
|
||||
},
|
||||
isOpen = true
|
||||
) {
|
||||
h3("Details heading")
|
||||
unorderedList {
|
||||
item { paragraph { plain("List item with "); italic("italic text") } }
|
||||
item { paragraph { plain("List item with "); spoiler("spoiler") } }
|
||||
}
|
||||
}
|
||||
}
|
||||
val testMarkdownInputRichMessageBlocks = InputRichMessageBlocks {
|
||||
testMarkdownMediaLessInputRichMessageBlocks.blocks.orEmpty().forEach(::add)
|
||||
|
||||
val photo = TelegramMediaPhoto(InputFile.fromUrl("https://telegram.org/example/photo.jpg"))
|
||||
val video = TelegramMediaVideo(InputFile.fromUrl("https://telegram.org/example/video.mp4"))
|
||||
val audio = TelegramMediaAudio(InputFile.fromUrl("https://telegram.org/example/audio.mp3"))
|
||||
val voiceNote = TelegramMediaVoiceNote(InputFile.fromUrl("https://telegram.org/example/audio.ogg"))
|
||||
val animation = TelegramMediaAnimation(InputFile.fromUrl("https://telegram.org/example/animation.gif"))
|
||||
|
||||
photo(photo)
|
||||
video(video)
|
||||
audio(audio)
|
||||
voiceNote(voiceNote)
|
||||
animation(animation)
|
||||
|
||||
photo(photo, RichBlockCaption(RichTextPlain("Photo caption")))
|
||||
video(video, RichBlockCaption(RichTextPlain("Video caption")))
|
||||
audio(audio, RichBlockCaption(RichTextPlain("Audio caption")))
|
||||
voiceNote(voiceNote, RichBlockCaption(RichTextPlain("Voice note caption")))
|
||||
animation(animation, RichBlockCaption(RichTextPlain("Animation caption")))
|
||||
|
||||
collage {
|
||||
photo(photo)
|
||||
video(video)
|
||||
}
|
||||
collage(RichBlockCaption(RichTextPlain("Collage caption"))) {
|
||||
video(video)
|
||||
photo(photo)
|
||||
}
|
||||
slideshow {
|
||||
photo(photo)
|
||||
video(video)
|
||||
}
|
||||
slideshow(RichBlockCaption(RichTextPlain("Slideshow caption"))) {
|
||||
video(video)
|
||||
photo(photo)
|
||||
}
|
||||
}
|
||||
|
||||
telegramBotWithBehaviourAndLongPolling(
|
||||
botToken,
|
||||
CoroutineScope(Dispatchers.IO),
|
||||
testServer = isTestServer
|
||||
) {
|
||||
// sendRichMessage with HTML-formatted content
|
||||
onCommand("rich_html") {
|
||||
sendRichMessage(
|
||||
it.chat.id,
|
||||
// InputRichMessageHTML factory — content described using HTML formatting
|
||||
InputRichMessageHTML(
|
||||
"""
|
||||
<a name="chapter-0"></a>
|
||||
<b>bold text</b>, <strong>bold text</strong>
|
||||
<i>italic text</i>, <em>italic text</em>
|
||||
<u>underlined text</u>, <ins>underlined text</ins>
|
||||
<s>strikethrough text</s>, <strike>strikethrough text</strike>, <del>strikethrough text</del>
|
||||
<code>inline fixed-width code</code>
|
||||
<mark>marked text</mark>
|
||||
<sub>subscript text</sub>
|
||||
<sup>superscript text</sup>
|
||||
<tg-spoiler>spoiler</tg-spoiler>
|
||||
|
||||
<a href="#note-1">Reference</a>
|
||||
<a href="https://t.me/">inline URL</a>
|
||||
<a href="mailto:user@example.com">inline e-mail</a>
|
||||
<a href="tel:+123456789">inline phone number</a>
|
||||
<a href="tg://user?id=123456789">inline mention of a user</a>
|
||||
<a href="#chapter-1">in-document link</a>
|
||||
<a name="chapter-1"></a>
|
||||
|
||||
<tg-reference name="note-1">Referenced text</tg-reference>
|
||||
<tg-emoji emoji-id="5368324170671202286">👍</tg-emoji>
|
||||
<img src="tg://emoji?id=5368324170671202286" alt="👍"/>
|
||||
<tg-time unix="1647531900" format="wDT">22:45 tomorrow</tg-time>
|
||||
<tg-math>x^2 + y^2</tg-math>
|
||||
|
||||
#hashtag ${'$'}USD +12345678901, card: 4242 4242 4242 4242, https://t.me t.me a@t.me /command @username
|
||||
|
||||
all the text above was on the same line
|
||||
|
||||
<h1>Heading 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h4>Heading 4</h4>
|
||||
<h5>Heading 5</h5>
|
||||
<h6>Heading 6</h6>
|
||||
|
||||
<a name="chapter-2"></a>
|
||||
|
||||
<p>Paragraph text</p>
|
||||
<pre>pre-formatted fixed-width code block</pre>
|
||||
<pre><code class="language-python"> print('pre-formatted fixed-width code block written in the Python programming language')</code></pre>
|
||||
<footer>Footer text</footer>
|
||||
<hr/>
|
||||
<ul><li>unordered list item</li></ul>
|
||||
<ol><li>ordered list item</li></ol>
|
||||
<ol start="3" type="a" reversed><li>ordered list item</li></ol>
|
||||
<ol><li value="7" type="i">ordered list item with explicit number</li></ol>
|
||||
<ul>
|
||||
<li><input type="checkbox" checked>Checked checkbox</li>
|
||||
<li><input type="checkbox">Unchecked checkbox</li>
|
||||
</ul>
|
||||
|
||||
<blockquote>Block quotation started<br>Block quotation continued<br>The last line of the block quotation<cite>The Author</cite></blockquote>
|
||||
<aside>Pull quote<cite>The Author</cite></aside>
|
||||
|
||||
<img src="https://telegram.org/example/photo.jpg"/>
|
||||
<video src="https://telegram.org/example/video.mp4"></video>
|
||||
<audio src="https://telegram.org/example/audio.mp3"></audio>
|
||||
<audio src="https://telegram.org/example/audio.ogg"></audio>
|
||||
<video src="https://telegram.org/example/animation.gif"></video>
|
||||
|
||||
<figure><img src="https://telegram.org/example/photo.jpg" tg-spoiler/><figcaption>Photo caption<cite>Photo credit</cite></figcaption></figure>
|
||||
<figure><video src="https://telegram.org/example/video.mp4" tg-spoiler></video><figcaption>Video caption</figcaption></figure>
|
||||
<figure><audio src="https://telegram.org/example/audio.mp3"></audio><figcaption>Audio caption</figcaption></figure>
|
||||
<figure><audio src="https://telegram.org/example/audio.ogg"></audio><figcaption>Voice note caption</figcaption></figure>
|
||||
<figure><video src="https://telegram.org/example/animation.gif" tg-spoiler></video><figcaption>Animation caption</figcaption></figure>
|
||||
|
||||
<tg-map lat="41.9" long="12.5" zoom="14"/>
|
||||
<figure><tg-map lat="41.9" long="12.5" zoom="14"/><figcaption>Map caption</figcaption></figure>
|
||||
|
||||
<tg-collage><img src="https://telegram.org/example/photo.jpg"/><video src="https://telegram.org/example/video.mp4"/></tg-collage>
|
||||
<tg-collage><video src="https://telegram.org/example/video.mp4"/><img src="https://telegram.org/example/photo.jpg"/><figcaption>Collage caption</figcaption></tg-collage>
|
||||
<tg-slideshow><img src="https://telegram.org/example/photo.jpg"/><video src="https://telegram.org/example/video.mp4"/></tg-slideshow>
|
||||
<tg-slideshow><video src="https://telegram.org/example/video.mp4"/><img src="https://telegram.org/example/photo.jpg"/><figcaption>Slideshow caption</figcaption></tg-slideshow>
|
||||
|
||||
<table><tr><th>Header 1</th><th>Header 2</th></tr><tr><td>Value 1</td><td>Value 2</td></tr></table>
|
||||
<table bordered striped><caption>Table caption</caption>
|
||||
<tr><td colspan="2" rowspan="2" align="left">Value</td><td align="center">Value2</td><td align="right">Value3</td></tr>
|
||||
<tr><td valign="top">Value4</td><td valign="middle">Value5</td><td valign="bottom">Value6</td></tr>
|
||||
<tr><td>Value7</td></tr></table>
|
||||
|
||||
<details><summary>Title</summary>Content</details>
|
||||
<details open><summary>Title</summary>Content</details>
|
||||
<tg-math-block>E = mc^2</tg-math-block>
|
||||
""".trimIndent()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// sendRichMessage with Markdown-formatted content
|
||||
onCommand("rich_markdown") {
|
||||
val sent = sendRichMessage(
|
||||
it.chat.id,
|
||||
InputRichMessageMarkdown(
|
||||
testMarkdownText
|
||||
)
|
||||
)
|
||||
println(sent)
|
||||
}
|
||||
|
||||
// sendRichMessage with Markdown-formatted content
|
||||
onCommand("rich_markdown_medialess") {
|
||||
val sent = sendRichMessage(
|
||||
it.chat.id,
|
||||
InputRichMessageMarkdown(
|
||||
testMarkdownMediaLessText
|
||||
)
|
||||
)
|
||||
println(sent)
|
||||
}
|
||||
|
||||
// sendRichMessage with Markdown-formatted content
|
||||
onCommand("rich_markdown_blocks") {
|
||||
val sent = sendRichMessage(
|
||||
it.chat.id,
|
||||
testMarkdownInputRichMessageBlocks
|
||||
)
|
||||
println(sent)
|
||||
}
|
||||
|
||||
// sendRichMessage with Markdown-formatted content
|
||||
onCommand("rich_markdown_medialess_blocks") {
|
||||
val sent = sendRichMessage(
|
||||
it.chat.id,
|
||||
testMarkdownMediaLessInputRichMessageBlocks
|
||||
)
|
||||
println(sent)
|
||||
}
|
||||
|
||||
// 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
|
||||
val parts = listOf(
|
||||
"Thinking",
|
||||
"Thinking about *rich* messages",
|
||||
"Thinking about *rich* messages and how to _stream_ them"
|
||||
)
|
||||
parts.forEach { part ->
|
||||
sendRichMessageDraft(chatId, draftId, InputRichMessageMarkdown(part))
|
||||
delay(1000)
|
||||
}
|
||||
// finalize the streamed draft with the real message
|
||||
sendRichMessage(chatId, InputRichMessageMarkdown("Done! Here is the *final* rich message."))
|
||||
}
|
||||
|
||||
// EditChatMessageRichText: send a rich message, then edit it with new rich content
|
||||
onCommand("rich_edit") {
|
||||
val sent = sendRichMessage(it.chat.id, InputRichMessageMarkdown("*Original* rich message"))
|
||||
delay(2000)
|
||||
execute(
|
||||
EditChatMessageRichText(
|
||||
chatId = sent.chat.id,
|
||||
messageId = sent.messageId,
|
||||
// the new rich_message parameter of editMessageText
|
||||
richMessage = InputRichMessageMarkdown("*Edited* rich message — now _updated_")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// === Bots API 10.2 additions: InputRichBlocks DSL + rich message media ===
|
||||
|
||||
// InputRichMessageBlocks { } — build a rich message from a typed InputRichBlock tree instead
|
||||
// of an HTML/Markdown string (exactly one of html/markdown/blocks may be used). The lambda is an
|
||||
// InputRichBlocksBuilder; buildInputRichBlocks { } returns the raw List<InputRichBlock> the same way.
|
||||
onCommand("rich_blocks") {
|
||||
sendRichMessage(
|
||||
it.chat.id,
|
||||
InputRichMessageBlocks {
|
||||
heading("Rich blocks (Bots API 10.2)", level = 1)
|
||||
paragraph {
|
||||
plain("This message is built from ")
|
||||
bold("structured InputRichBlocks")
|
||||
plain(" — no HTML or Markdown string is involved.")
|
||||
}
|
||||
|
||||
h2("Lists")
|
||||
h3("Ordered")
|
||||
orderedList {
|
||||
item(0) { paragraph("A plain list item") }
|
||||
item(1) { paragraph { url("google", "google.com") } }
|
||||
item(2, hasCheckbox = true, isChecked = true) { paragraph("A plain list item") }
|
||||
item(3, hasCheckbox = true, isChecked = false) { paragraph("A plain list item") }
|
||||
}
|
||||
divider()
|
||||
h3("Unordered")
|
||||
unorderedList {
|
||||
item { paragraph("A plain list item") }
|
||||
item { paragraph { url("google", "google.com") } }
|
||||
item(hasCheckbox = true, isChecked = true) { paragraph("A plain list item") }
|
||||
item(hasCheckbox = true, isChecked = false) { paragraph("A plain list item") }
|
||||
}
|
||||
|
||||
divider()
|
||||
|
||||
heading("Code", level = 2)
|
||||
preformatted("val answer = 42", language = "kotlin")
|
||||
|
||||
heading("Quotation", level = 2)
|
||||
blockQuotation {
|
||||
paragraph {
|
||||
plain("Quotations are themselves made of nested blocks — ")
|
||||
italic("including inline formatting")
|
||||
plain(".")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
listOf("Analyzing your request", "Composing a structured answer").forEach { step ->
|
||||
sendRichMessageDraft(
|
||||
chatId,
|
||||
draftId,
|
||||
InputRichMessageBlocks { thinking(step) }
|
||||
)
|
||||
delay(1000)
|
||||
}
|
||||
// finalize the streamed draft with the real (non-thinking) blocks
|
||||
sendRichMessage(
|
||||
chatId,
|
||||
InputRichMessageBlocks {
|
||||
heading("Answer", level = 2)
|
||||
paragraph("Here is the finalized, structured reply.")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Rich message media: send me a photo and it gets embedded into a rich message two ways.
|
||||
onPhoto { message ->
|
||||
// reuse the received file by its fileId (no upload). To upload a brand-new file instead,
|
||||
// build the TelegramMedia from file.asMultipartFile() — SendRichMessage collects any
|
||||
// MultipartFile inside the rich message and uploads it as attach://<id> automatically.
|
||||
val photoMedia = TelegramMediaPhoto(message.content.media.fileId)
|
||||
|
||||
// (1) referenced from HTML via tg://photo?id=<id>, resolved through InputRichMessage.media
|
||||
sendRichMessage(
|
||||
message.chat.id,
|
||||
InputRichMessageHTML(
|
||||
"""
|
||||
<h2>Your photo, referenced from HTML</h2>
|
||||
<p>Below is your photo, referenced via <code>tg://photo?id=userphoto</code>:</p>
|
||||
<img src="tg://photo?id=userphoto"/>
|
||||
""".trimIndent(),
|
||||
media = listOf(
|
||||
InputRichMessageMedia(id = "userphoto", media = photoMedia)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// (2) as a first-class media block inside an InputRichBlocks tree
|
||||
sendRichMessage(
|
||||
message.chat.id,
|
||||
InputRichMessageBlocks {
|
||||
heading("Your photo, as a media block", level = 2)
|
||||
paragraph("The same photo, this time a photo() block inside the blocks tree:")
|
||||
photo(photoMedia)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// waitRichMessage expectation: wait for the user to send a rich message
|
||||
onCommand("wait_rich") {
|
||||
reply(it, "Send me a rich message now")
|
||||
val richMessageContent = waitRichMessage().first()
|
||||
reply(
|
||||
it,
|
||||
"Got rich message with ${richMessageContent.richMessage.blocks.size} block(s)"
|
||||
)
|
||||
}
|
||||
|
||||
onGuestRequestMessage {
|
||||
val withTextContent = it.withContentOrNull<TextContent>() ?: return@onGuestRequestMessage
|
||||
val haveCommand = withTextContent.content.text.contains("/rich_guest")
|
||||
if (haveCommand) {
|
||||
answer(
|
||||
it.guestQueryId,
|
||||
InlineQueryResultArticle(
|
||||
InlineQueryId("rich_content"),
|
||||
"Send rich message",
|
||||
InputRichMessageContent(
|
||||
InputRichMessageMarkdown(
|
||||
testMarkdownText
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// onRichMessage trigger: incoming messages carrying the new rich_message field
|
||||
onRichMessage { message ->
|
||||
val richMessage = message.content.richMessage
|
||||
println("=== Rich message received ===")
|
||||
println(" isRtl: ${richMessage.isRtl}")
|
||||
println(" blocks: ${richMessage.blocks.size}")
|
||||
richMessage.blocks.forEachIndexed { index, block ->
|
||||
println(" [$index] $block")
|
||||
}
|
||||
reply(message, "Received a rich message with ${richMessage.blocks.size} block(s)")
|
||||
execute(
|
||||
message.content.createResend(
|
||||
message.chat.id,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// InputRichMessageContent as InputMessageContent of inline query results
|
||||
onBaseInlineQuery { query ->
|
||||
answer(
|
||||
query,
|
||||
results = listOf(
|
||||
InlineQueryResultArticle(
|
||||
InlineQueryId("rich_html"),
|
||||
"Rich message (HTML)",
|
||||
// InputRichMessageContent wraps an InputRichMessage and is a valid InputMessageContent
|
||||
InputRichMessageContent(
|
||||
InputRichMessageHTML("<b>Bold</b> rich message sent via inline query")
|
||||
),
|
||||
description = "InputRichMessageContent built from HTML"
|
||||
),
|
||||
InlineQueryResultArticle(
|
||||
InlineQueryId("rich_markdown"),
|
||||
"Rich message (Markdown)",
|
||||
InputRichMessageContent(
|
||||
InputRichMessageMarkdown("*Bold* rich message sent via inline query")
|
||||
),
|
||||
description = "InputRichMessageContent built from Markdown"
|
||||
)
|
||||
),
|
||||
cachedTime = 0
|
||||
)
|
||||
}
|
||||
|
||||
// onlyRichMessageContentMessages: filter a Flow<ContentMessage<*>> down to rich message content
|
||||
allUpdatesFlow
|
||||
.mapNotNull { it.baseSentMessageUpdateOrNull() ?.data ?.contentMessageOrNull() }
|
||||
.onlyRichMessageContentMessages()
|
||||
.subscribeLoggingDropExceptions(scope = this) { richMessageContentMessage ->
|
||||
println("[onlyRichMessageContentMessages] ${richMessageContentMessage.content.richMessage.blocks.size} blocks")
|
||||
}
|
||||
|
||||
setMyCommands(
|
||||
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_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"),
|
||||
BotCommand("wait_rich", "Wait for you to send a rich message"),
|
||||
)
|
||||
|
||||
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||
println(it)
|
||||
}
|
||||
}.second.join()
|
||||
}
|
||||
@@ -1,54 +1,50 @@
|
||||
# RightsChangerBot
|
||||
|
||||
A long-polling/FSM example for changing a member's chat permissions and a channel administrator's rights with inline keyboards. The bot registers its commands in the all-group-chats scope and prints every received update to standard output.
|
||||
A bot for managing user permissions and administrator rights in Telegram groups and channels.
|
||||
|
||||
## Commands and callbacks
|
||||
## Functionality
|
||||
|
||||
### Member permissions
|
||||
|
||||
Run these commands in a group or supergroup as a reply to a non-administrator's message:
|
||||
|
||||
- `/simple` shows toggles for polls, other messages, and web-page previews. Changes use Telegram's dependent/common permission model (`useIndependentChatPermissions = false`), so related permissions may change together.
|
||||
- `/granular` shows independent toggles for text messages, other messages, audio, voice notes, video, video notes, photos, web-page previews, polls, and documents (`useIndependentChatPermissions = true`).
|
||||
|
||||
The keyboard shows `✅` for allowed, `❌` for denied, and no suffix when Telegram reports no explicit value. Clicking a button restricts the replied-to member and edits the keyboard with the refreshed state. If the command is not a valid reply, or the target is an administrator/owner rather than a normal or restricted member, the bot usually returns without a response.
|
||||
|
||||
### Channel administrator rights
|
||||
|
||||
Send `/rights_in_channel` in a private chat with the bot. Telegram's chat/user request buttons used by this flow are available only in private chats, even though the command is registered only in the group command-menu scope.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Callbacks are processed only when clicked by the configured allowed user. They are not explicitly answered with `answerCallbackQuery`, so Telegram clients may retain their loading indicator until the edit completes or the callback times out.
|
||||
|
||||
## Setup, permissions, and security
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Current authorization caveats: `/rights_in_channel` has no sender filter, although its resulting mutation callbacks still require `ALLOWED_USER_ID`. `/granular` checks `ALLOWED_USER_ID` in normal public chats but accepts channel-post commands without that sender check; its callbacks remain protected. Keep the bot limited to trusted chats.
|
||||
|
||||
Long polling automatically removes an existing webhook at startup. Run only one update consumer for the token at a time.
|
||||
Provides two modes of permission editing (simple / granular) for regular member restrictions, and a
|
||||
full FSM-based flow for editing channel administrator rights. Changes are presented as inline
|
||||
keyboards with visual ✅/❌ toggles that persist until the user is done.
|
||||
|
||||
## Arguments
|
||||
|
||||
The bot requires the token first and the allowed numeric user ID second. Debug logging is enabled only when the third argument is exactly `debug`.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
| 2 | `ADMIN_USER_ID` | Numeric Telegram user ID allowed to use the bot |
|
||||
|
||||
Optional flags (any order after the required arguments):
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/simple` | Show a common-permissions keyboard (send messages, polls, web previews, etc.) for the replied-to user |
|
||||
| `/granular` | Show a granular-permissions keyboard (individual media types) for the replied-to user |
|
||||
| `/rights_in_channel` | Start the FSM flow to pick a channel and a user, then edit that user's administrator rights in the channel |
|
||||
|
||||
All commands must be sent as a **reply** to a target user's message.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- **Simple mode** — toggles grouped permissions: send messages, send media, send polls, send other content, add web page previews, change info, invite users, pin messages
|
||||
- **Granular mode** — toggles individual media-type permissions: audios, documents, photos, videos, video notes, voice notes, stickers, animations, games, gift premiums, forward channels, forward non-channels
|
||||
- **Channel admin rights** — FSM with three states:
|
||||
1. `RetrievingChannelChatState` — user picks the channel
|
||||
2. `RetrievingUserIdChatState` — user picks the member
|
||||
3. `RetrievingChatInfoDoneState` — inline keyboard for toggling admin rights (post messages, edit messages, delete messages, ban users, invite users, pin messages, manage topics, manage video chats, post stories, edit stories, delete stories, remain anonymous)
|
||||
- Inline keyboard callbacks update permission state in real time
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
./gradlew :RightsChangerBot:run --args="<BOT_TOKEN> <ALLOWED_USER_ID>"
|
||||
```
|
||||
|
||||
```bash
|
||||
./gradlew :RightsChangerBot:run --args="<BOT_TOKEN> <ALLOWED_USER_ID> debug"
|
||||
../gradlew run --args="BOT_TOKEN ADMIN_USER_ID"
|
||||
```
|
||||
|
||||
@@ -33,27 +33,20 @@ import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
|
||||
import dev.inmo.tgbotapi.types.chat.member.ChatCommonAdministratorRights
|
||||
import dev.inmo.tgbotapi.types.commands.BotCommandScope
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.AccessibleMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatMessage
|
||||
import dev.inmo.tgbotapi.types.request.RequestId
|
||||
import dev.inmo.tgbotapi.utils.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/** States used by the private-chat flow that selects a channel and one of its users. */
|
||||
sealed interface UserRetrievingStep : State {
|
||||
/** Waits for the user to share a channel in the private chat identified by [context]. */
|
||||
data class RetrievingChannelChatState(
|
||||
override val context: ChatId
|
||||
) : UserRetrievingStep
|
||||
|
||||
/** Waits for a user selection after [channelId] has been shared. */
|
||||
data class RetrievingUserIdChatState(
|
||||
override val context: ChatId,
|
||||
val channelId: ChatId
|
||||
) : UserRetrievingStep
|
||||
|
||||
/** Carries the selected [channelId] and [userId] to the administrator-rights keyboard step. */
|
||||
data class RetrievingChatInfoDoneState(
|
||||
override val context: ChatId,
|
||||
val channelId: ChatId,
|
||||
@@ -61,12 +54,6 @@ sealed interface UserRetrievingStep : State {
|
||||
) : UserRetrievingStep
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the RightsChangerBot FSM and callback handlers using long polling.
|
||||
*
|
||||
* @param args the bot token, the only user ID authorized to mutate rights, and optionally `debug` as the third
|
||||
* element to enable formatted KSLog output
|
||||
*/
|
||||
@OptIn(PreviewFeature::class)
|
||||
suspend fun main(args: Array<String>) {
|
||||
val botToken = args.first()
|
||||
@@ -221,7 +208,7 @@ suspend fun main(args: Array<String>) {
|
||||
) {
|
||||
val replyMessage = it.replyTo
|
||||
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
||||
if (replyMessage is ChatMessage) {
|
||||
if (replyMessage is AccessibleMessage) {
|
||||
reply(
|
||||
replyMessage,
|
||||
"Manage keyboard:",
|
||||
@@ -242,7 +229,7 @@ suspend fun main(args: Array<String>) {
|
||||
val replyMessage = it.replyTo
|
||||
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
||||
|
||||
if (replyMessage is ChatMessage) {
|
||||
if (replyMessage is AccessibleMessage) {
|
||||
reply(
|
||||
replyMessage,
|
||||
"Manage keyboard:",
|
||||
@@ -260,7 +247,7 @@ suspend fun main(args: Array<String>) {
|
||||
initialFilter = { it.user.id == allowedAdmin }
|
||||
) {
|
||||
val messageReply =
|
||||
it.message.chatContentMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||
val userId = messageReply.user.id
|
||||
val permissions =
|
||||
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
||||
@@ -347,7 +334,7 @@ suspend fun main(args: Array<String>) {
|
||||
initialFilter = { it.user.id == allowedAdmin }
|
||||
) {
|
||||
val messageReply =
|
||||
it.message.chatContentMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||
val userId = messageReply.user.id
|
||||
val permissions =
|
||||
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
# SlotMachineDetectorBot
|
||||
|
||||
A long-polling example that distinguishes slot-machine dice from other Telegram
|
||||
dice animations and decodes the slot reels.
|
||||
A bot that detects slot-machine dice rolls and reports the result.
|
||||
|
||||
## Trigger and output
|
||||
## Functionality
|
||||
|
||||
The bot handles every dice message and defines no commands.
|
||||
Listens for dice messages of the *SlotMachine* type. When one is received, it calculates the
|
||||
combination shown on the three reels and replies with the formatted result.
|
||||
|
||||
- For a slot-machine dice, it calls `calculateSlotMachineResult` and replies in the
|
||||
exact format `<left-reel>|<center-reel>|<right-reel>`.
|
||||
- If a slot-machine value cannot be decoded, the handler sends no reply.
|
||||
- For every other dice animation, it replies
|
||||
`There is no slot machine dice in message`.
|
||||
## Arguments
|
||||
|
||||
The example reports the three decoded reels only. It does not calculate a numeric
|
||||
score or decide whether the combination wins.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Setup, permissions, and privacy
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
1. Create a bot, obtain its token, and keep the token private.
|
||||
2. Start a private chat with it, or add it to a group where dice should be observed.
|
||||
3. Allow the bot to send replies in that chat.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Dice are ordinary non-command messages. In groups, configure Telegram's bot privacy
|
||||
setting so the desired dice updates are delivered; disable privacy mode if the bot
|
||||
should inspect all group dice. No administrator-only API methods are used. The bot
|
||||
stores no message or dice data.
|
||||
## Bot Commands
|
||||
|
||||
## Run
|
||||
None.
|
||||
|
||||
From the repository root:
|
||||
## Capabilities
|
||||
|
||||
- Filters incoming dice messages specifically for the slot-machine emoji type
|
||||
- Decodes the numeric dice value into the three reel symbols
|
||||
- Replies with a human-readable description of the result
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :SlotMachineDetectorBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required bot token. Omitting it causes startup to fail;
|
||||
additional arguments are ignored. This launcher has no debug or test-server flag.
|
||||
|
||||
@@ -6,13 +6,6 @@ import dev.inmo.tgbotapi.extensions.utils.*
|
||||
import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Starts the long-polling slot-machine detector.
|
||||
*
|
||||
* [args] must contain the bot token first; later elements are ignored. Slot-machine
|
||||
* dice receive a pipe-separated three-reel reply, while other dice receive an
|
||||
* explanatory reply. An undecodable slot-machine result is silently ignored.
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
val bot = telegramBot(args.first())
|
||||
|
||||
|
||||
@@ -1,65 +1,46 @@
|
||||
# StarTransactionsBot
|
||||
|
||||
A long-polling Telegram Stars playground for invoices, transaction history, paid
|
||||
media, pre-checkout approval, and refund updates.
|
||||
A bot that demonstrates Telegram Stars payments: sending invoices, handling transactions, and
|
||||
delivering paid media.
|
||||
|
||||
## Commands
|
||||
## Functionality
|
||||
|
||||
- `/start` replies with a sample invoice charging `1` Star, payload
|
||||
`sample payload`, and a **Pay** button. Matching pre-checkout queries are approved
|
||||
automatically.
|
||||
- `/transactions` works only in the chat whose numeric ID was supplied as the
|
||||
second program argument. It replies with the bot's first transaction page.
|
||||
Sends a 1-star invoice on `/start`. After successful payment the bot sends paid media (a photo
|
||||
and a video). The admin can browse the full transaction history with pagination. Refunds received
|
||||
from Telegram are logged. Checkout queries are validated before approval.
|
||||
|
||||
The commands are not registered in Telegram's command menu.
|
||||
## Arguments
|
||||
|
||||
## Transaction pages
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
| 2 | `ADMIN_USER_ID` | Numeric Telegram user ID that is allowed to view transaction history |
|
||||
|
||||
Pages use an offset and a default limit of `10`. Each returned transaction includes
|
||||
its ID, date, amount, direction (`incoming`, `outgoing`, or `unknown`), and partner
|
||||
type. The inline keyboard contains:
|
||||
Optional flags (any order after the required arguments):
|
||||
|
||||
- `<` when a previous nonnegative offset exists;
|
||||
- `>` on every page, using `offset + limit`, even when no later records exist.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Selecting either button fetches that page and edits the existing text message. The
|
||||
initial `/transactions` command is admin-chat filtered; pagination callbacks are not
|
||||
independently filtered, so keep the resulting message in the private admin chat.
|
||||
Pagination state is encoded in callback data and is not persisted by the bot.
|
||||
## Bot Commands
|
||||
|
||||
## Other triggers
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Send a 1-star invoice to the user |
|
||||
| `/transactions` | Browse paginated star transaction history *(admin only)* |
|
||||
|
||||
- A photo or video is sent back as paid media costing `1` Star.
|
||||
- A visual gallery is downloaded to temporary files and re-uploaded as one-Star
|
||||
paid media, retaining its caption above the media; only photos and videos are used.
|
||||
- Paid-media-info messages are printed to standard output.
|
||||
- Refunded-payment events receive a reply containing the payment information.
|
||||
- Every received update is printed to standard output.
|
||||
## Capabilities
|
||||
|
||||
## Setup, permissions, and payment safety
|
||||
- Creates and sends a Stars invoice via `sendInvoice`
|
||||
- Handles `PreCheckoutQuery` events to approve or reject checkout
|
||||
- Delivers paid media (photo + video) after a successful payment
|
||||
- Paginates transaction history using inline keyboard next/previous buttons
|
||||
- Tracks and logs refund notifications
|
||||
- Runs via long polling
|
||||
|
||||
Create a bot, keep its token private, and choose the numeric ID of the user who may
|
||||
open transaction history. Use that user's private chat, and allow the bot to send
|
||||
messages, invoices, photos, and videos. The Telegram environment and client must
|
||||
support Stars and paid media; purchasers need Stars to complete payments.
|
||||
|
||||
This is a charging example: `/start` and delivered photo/video content can create
|
||||
one-Star purchase flows. Use test accounts or Telegram's test environment when
|
||||
appropriate. The code approves its sample pre-checkout query but performs no
|
||||
fulfilment after payment. No chat-administrator methods or webhook are configured.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root, pass the token and decimal admin ID first:
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :StarTransactionsBot:run --args="BOT_TOKEN ADMIN_USER_ID"
|
||||
```
|
||||
|
||||
Both arguments are required; an absent or nonnumeric admin ID stops startup. The
|
||||
optional, case-sensitive flags `debug` and `testServer` may follow them in either
|
||||
order to enable formatted logging or Telegram's test API.
|
||||
|
||||
```bash
|
||||
./gradlew :StarTransactionsBot:run --args="BOT_TOKEN ADMIN_USER_ID debug testServer"
|
||||
../gradlew run --args="BOT_TOKEN ADMIN_USER_ID"
|
||||
```
|
||||
|
||||
@@ -37,13 +37,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts the long-polling Telegram Stars and paid-media playground.
|
||||
*
|
||||
* The first element of [args] must be the bot token and the second a numeric admin
|
||||
* user/chat ID authorized for `/transactions`. Optional exact values `debug` and
|
||||
* `testServer` enable diagnostic logging and Telegram's test environment. Other
|
||||
* handlers create a one-Star invoice or paid media and report payment-related
|
||||
* updates.
|
||||
* An example bot that interacts with Telegram Stars API (used for payments)
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
# StickerInfoBot
|
||||
|
||||
A multiplatform long-polling example that looks up Telegram sticker-set metadata. `StickerInfoBotLib` contains the shared JVM/JS behavior, with a browser entry point in `jsMain`; `jvm_launcher` provides the command-line entry point. The bot has no commands.
|
||||
A multiplatform bot (JVM + JS) that displays detailed information about stickers and custom emoji.
|
||||
|
||||
## Behavior and output
|
||||
## Functionality
|
||||
|
||||
At startup, the bot calls `getMe` and reports the returned bot information through the active launcher. It then handles:
|
||||
When the user sends a sticker, the bot replies with the sticker set name, title, and sticker type.
|
||||
When the user sends a text message containing custom emoji entities, the bot fetches the
|
||||
corresponding sticker objects and sends back their information.
|
||||
|
||||
- **Sticker messages:** looks up the sticker's set and replies to the message with the set name, title, and type (`Regular`, `Mask`, `Custom emoji`, or the raw unknown type). If no set can be resolved, it replies with **Looks like this stickerset has been removed**.
|
||||
- **Text messages:** shows a typing action, scans the message entities for custom emoji, resolves their stickers and sticker sets, removes duplicate sets, and replies with the same metadata for each set. Sets without a resolvable name are skipped. Text without resolvable custom emoji produces no reply.
|
||||
## Arguments
|
||||
|
||||
Long output from text containing several custom-emoji sets is split into Telegram-sized messages. Every received update is printed to the JVM terminal or browser developer console, and uncaught processing errors print stack traces.
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
## Telegram setup and permissions
|
||||
Optional flags (any order):
|
||||
|
||||
- Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
- No administrator rights are required in a private chat. The bot only needs to receive the source message and be allowed to send replies.
|
||||
- To inspect arbitrary sticker and custom-emoji messages in a group, either disable privacy mode with BotFather's `/setprivacy` or promote the bot to administrator. With privacy mode enabled as a regular member, Telegram does not deliver most ordinary group messages to it.
|
||||
- The example uses long polling and automatically removes an existing webhook at startup. Run only one launcher for a token at a time.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
## Launchers
|
||||
## Bot Commands
|
||||
|
||||
Run these commands from the repository root.
|
||||
None.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Detects incoming sticker messages and calls `getStickerSet` to retrieve set metadata
|
||||
- Reports sticker set name, title, and type (regular / mask / custom emoji)
|
||||
- Scans text message entities for `CustomEmoji` types
|
||||
- Fetches the corresponding sticker objects via `getCustomEmojiStickers`
|
||||
- Sends sticker information back as a formatted reply
|
||||
- Shared `commonMain` library with JVM and JS launchers
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
### JVM
|
||||
|
||||
The required first argument is the bot token. Additional arguments are ignored.
|
||||
|
||||
```bash
|
||||
./gradlew :StickerInfoBot:jvm_launcher:run --args="<BOT_TOKEN>"
|
||||
./gradlew :StickerInfoBot:jvm_launcher:run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
> **Known issue:** `jvm_launcher/build.gradle` declares `StickerInfoBotJvmKt`, while the current launcher filename produces `StickerInfoBotBotJvmKt`. The `run` task cannot start until that main-class setting is corrected; it is left unchanged by this documentation-only update.
|
||||
|
||||
The JVM launcher prints the startup `getMe` result, raw updates, and errors to standard output.
|
||||
|
||||
### Browser/JS
|
||||
|
||||
Start the Kotlin/JS browser development run:
|
||||
|
||||
```bash
|
||||
./gradlew :StickerInfoBot:StickerInfoBotLib:jsBrowserDevelopmentRun
|
||||
```
|
||||
|
||||
Enter the token in the bundled form and press **Start bot**. The page renders the startup `getMe` result; raw updates and errors appear in the browser developer console. The bot runs only while the page remains active, and every form submission starts another polling instance.
|
||||
|
||||
The browser handles the bot token in client-side code. Use this target only from a trusted local page and do not expose a token through a publicly hosted copy.
|
||||
|
||||
@@ -21,11 +21,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
|
||||
/**
|
||||
* Formats this sticker set's name, title, and type as Telegram text entities.
|
||||
*
|
||||
* A `null` receiver produces a bold warning that the sticker set appears to have been removed.
|
||||
*/
|
||||
fun StickerSet?.buildInfo() = buildEntities {
|
||||
if (this@buildInfo == null) {
|
||||
bold("Looks like this stickerset has been removed")
|
||||
@@ -43,15 +38,6 @@ fun StickerSet?.buildInfo() = buildEntities {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and runs the shared StickerInfoBot behavior using long polling.
|
||||
*
|
||||
* Sticker messages are answered with their set metadata. Text messages are scanned for custom-emoji entities and
|
||||
* answered with metadata for each distinct resolved sticker set. Every update is also logged.
|
||||
*
|
||||
* @param token the Telegram bot token
|
||||
* @param print receives the bot information returned by the startup `getMe` request
|
||||
*/
|
||||
suspend fun activateStickerInfoBot(
|
||||
token: String,
|
||||
print: (Any) -> Unit
|
||||
|
||||
@@ -4,11 +4,6 @@ import org.w3c.dom.*
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
/**
|
||||
* Installs the browser token form after `DOMContentLoaded` and starts a bot for every submission.
|
||||
*
|
||||
* Each bot receives its token from `bot_token`; its startup information is appended under `bots_container`.
|
||||
*/
|
||||
fun main() {
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Runs [activateStickerInfoBot] on the JVM and prints its startup bot information to standard output.
|
||||
*
|
||||
* @param args the Telegram bot token as the first element; additional elements are ignored
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
activateStickerInfoBot(args.first()) {
|
||||
println(it)
|
||||
|
||||
@@ -1,67 +1,46 @@
|
||||
# StickerSetHandler
|
||||
|
||||
StickerSetHandler is a Kotlin/JVM long-polling example that creates and manages one Telegram sticker set for each
|
||||
private chat. It copies stickers sent to the bot into a set owned by the user.
|
||||
A bot that builds and manages a personal sticker set for each user from stickers they send.
|
||||
|
||||
## Behavior
|
||||
## Functionality
|
||||
|
||||
| Trigger | Action |
|
||||
| --- | --- |
|
||||
| `/start` | Replies with a short hint for the `/delete` command. |
|
||||
| `/delete` | Deletes the chat's entire sticker set and replies `Deleted`; if deletion fails, it replies that it could not delete the set. |
|
||||
| A regular, mask, or custom-emoji sticker | Downloads and re-uploads the sticker. The first supported sticker creates the set; later compatible stickers are added to it. The bot replies with the created or newly added sticker. |
|
||||
| Any other update | Performs no chat action. Every update is still printed to standard output. |
|
||||
|
||||
The deterministic set name is `s<chat_id>_by_<bot_username>`, and a newly created set is titled
|
||||
`Sticker set by <bot_first_name>`. The original sticker format, emoji (or a smiling fallback), mask position, and the
|
||||
initial custom-emoji repainting setting are preserved where applicable. Sticker keywords are not copied. Sticker
|
||||
types cannot be mixed in one set: the first sticker determines whether the set is regular, mask, or custom emoji.
|
||||
|
||||
After `/delete`, sending another supported sticker recreates the set. Sticker objects that the library does not
|
||||
recognize are ignored.
|
||||
|
||||
## Telegram setup and permissions
|
||||
|
||||
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
|
||||
2. Open a private chat with the bot and start it.
|
||||
3. Send a sticker, or use `/start` to display the available command.
|
||||
|
||||
No Telegram administrator rights or special BotFather modes are required for this private-chat workflow. The
|
||||
process needs network access and a writable temporary directory because it downloads each sticker before uploading
|
||||
it to Telegram.
|
||||
|
||||
Use the example in private chats only. The code passes the incoming chat ID as the sticker-set owner's user ID;
|
||||
private-chat IDs identify the user, while group and channel IDs do not. Telegram also enforces its own account,
|
||||
sticker-type, format, and sticker-set limits, so an otherwise supported request can still be rejected.
|
||||
When a user sends a sticker, the bot extracts its emoji and adds it to a per-user sticker set
|
||||
named `<user_id>_by_<bot_username>`. If the set does not yet exist, it is created first. The bot
|
||||
supports regular, mask, and custom emoji sticker sets, determined by the type of the first sticker
|
||||
added. Sending `/delete` removes the user's entire sticker set.
|
||||
|
||||
## Arguments
|
||||
|
||||
| Position | Argument | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `BOT_TOKEN` | Yes | Bot API token issued by BotFather. |
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
The program reads only the first argument. It does not implement `debug`, `testServer`, or other optional flags, and
|
||||
it exits during startup if the token is omitted.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
## Run
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
From the repository root:
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Sends a welcome message explaining how to use the bot |
|
||||
| `/delete` | Deletes the user's personal sticker set created by this bot |
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Per-user sticker set with a deterministic name based on user ID and bot username
|
||||
- Automatic sticker set creation on first sticker received
|
||||
- Supports all sticker set types: regular, mask, custom emoji
|
||||
- Emoji extraction from incoming stickers
|
||||
- Sticker added to an existing set via `addStickerToSet`
|
||||
- Set deletion via `deleteStickerSet`
|
||||
- Runs via long polling
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :StickerSetHandler:run --args="<BOT_TOKEN>"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
Keep the real token out of source control and be aware that command-line arguments may be visible in shell history
|
||||
or process listings. The bot polls until the process is stopped.
|
||||
|
||||
## Safety notes
|
||||
|
||||
- `/delete` has no confirmation step and removes the complete set, not just its most recent sticker.
|
||||
- The set name contains the numeric private-chat/user ID. That identifier can be exposed when the sticker-set name
|
||||
or link is displayed or shared.
|
||||
- There is no allowlist, rate limit, or moderation. Every supported sticker is downloaded to temporary storage and
|
||||
uploaded again, so expose the bot only with suitable API, bandwidth, and disk limits.
|
||||
- Every received update is written to standard output, and several failure paths print stack traces. Those logs can
|
||||
contain user, chat, message, and file metadata and should be protected accordingly.
|
||||
- Any sticker-set lookup failure is treated as if the set were absent, so a temporary Telegram or network failure
|
||||
can lead to a failed creation attempt rather than a retry.
|
||||
|
||||
@@ -24,13 +24,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts the long-polling sticker-set example using the bot token in the first command-line argument.
|
||||
*
|
||||
* Supported sticker messages create or extend the deterministic set for the current private chat, while `/delete`
|
||||
* removes that set. Private chats are required because the chat ID is also passed to Telegram as the set owner's
|
||||
* user ID.
|
||||
*
|
||||
* @param args command-line arguments whose first element must be a Bot API token
|
||||
* Send sticker to this bot to form your own stickers set. Send /delete to delete this sticker set
|
||||
*/
|
||||
suspend fun main(args: Array<String>) {
|
||||
telegramBotWithBehaviourAndLongPolling(
|
||||
|
||||
@@ -1,59 +1,43 @@
|
||||
# SuggestedPosts
|
||||
|
||||
A long-polling playground for channel direct messages and suggested-post lifecycle
|
||||
updates.
|
||||
A bot that handles the channel Direct Messages (suggested post) approval flow.
|
||||
|
||||
## Commands and content triggers
|
||||
## Functionality
|
||||
|
||||
- `/start` fetches and prints full information for the command's chat. It sends no
|
||||
reply and is not registered in Telegram's command menu.
|
||||
- Every delivered channel-direct-message content message is printed with its chat
|
||||
information, then resent to the same direct-message chat with empty
|
||||
`SuggestedPostParameters` so it becomes a suggested post.
|
||||
- Channel paid-post content is printed to standard output.
|
||||
Monitors suggested post events in a channel connected via Direct Messages. When a post is
|
||||
suggested, the bot automatically schedules a decline after a short delay (demonstrating the
|
||||
decline flow). Paid post events and approval/decline confirmations are also tracked and logged.
|
||||
|
||||
## Suggested-post lifecycle
|
||||
## Arguments
|
||||
|
||||
For each detected suggested post, the bot races three branches:
|
||||
| Position | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| 1 | `BOT_TOKEN` | Telegram bot token |
|
||||
|
||||
1. wait for a suggested-post-approved event from the same chat;
|
||||
2. wait for a suggested-post-declined event from the same chat;
|
||||
3. after one-second delays, send `3`, `2`, and `1`, then decline the post.
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
A matching approval or decline ends the countdown branch. The waits correlate by
|
||||
chat ID rather than individual suggested-post message ID, which matters when several
|
||||
suggestions are active in the same chat. The bot does not approve posts itself and
|
||||
does not set a price or scheduled publication time.
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `debug` | Enable verbose debug logging |
|
||||
| `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
Lifecycle events are printed and receive these replies:
|
||||
## Bot Commands
|
||||
|
||||
- paid → `Paid`;
|
||||
- approved → `Approved`;
|
||||
- declined → `Declined`;
|
||||
- refunded → `Refunded`;
|
||||
- approval failed → `Approval failed`.
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/start` | Initialises the bot and confirms it is running |
|
||||
|
||||
Every update is also printed. State exists only in active coroutine waits; restarting
|
||||
the bot cancels pending countdowns and forgets active suggestions.
|
||||
## Capabilities
|
||||
|
||||
## Setup and permissions
|
||||
- Handles `SuggestedPostApproved` events
|
||||
- Handles `SuggestedPostDeclined` events
|
||||
- Handles `SuggestedPostPaid` and `SuggestedPostRefunded` events
|
||||
- Handles `SuggestedPostApprovalFailed` errors
|
||||
- Automatically declines new suggestions after a configurable delay to demonstrate the decline API
|
||||
- Runs via long polling
|
||||
|
||||
Use a channel with direct messages and suggested posts enabled. Add the bot with
|
||||
enough channel access to receive direct-message updates, resend their content, send
|
||||
countdown/reply messages, and decline suggested posts. The code does not validate
|
||||
these permissions at startup and configures no webhook.
|
||||
|
||||
Full updates, chat details, suggested content, and payment-related events are logged.
|
||||
Use a private test channel and protect process output.
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :SuggestedPosts:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The token is the required first argument. Optional exact flags may follow it in any
|
||||
order: `debug` enables formatted logging, and `testServer` selects Telegram's test
|
||||
environment. Other arguments are ignored.
|
||||
|
||||
@@ -31,7 +31,7 @@ import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
||||
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChannelPaidPost
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
import dev.inmo.tgbotapi.utils.firstOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -41,12 +41,7 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Starts the long-polling suggested-post lifecycle playground.
|
||||
*
|
||||
* The first element of [args] must be the bot token. Optional exact values `debug`
|
||||
* and `testServer` enable diagnostic logging and Telegram's test environment.
|
||||
* Channel direct-message content is resent as a suggestion, then automatically
|
||||
* declined after a three-message countdown unless an approval or decline arrives.
|
||||
* This place can be the playground for your code.
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
@@ -1,57 +1,46 @@
|
||||
# TagsBot
|
||||
|
||||
A long-polling example for setting chat-member tags, delegating tag-management
|
||||
rights, and reading sender tags from group messages.
|
||||
A bot that manages custom member tags in Telegram groups.
|
||||
|
||||
## Commands
|
||||
## Functionality
|
||||
|
||||
All commands target the identifiable user who sent the replied-to group content
|
||||
message. Without such a reply, they silently do nothing.
|
||||
Allows administrators to assign custom text tags to group members, remove tags, and grant or
|
||||
revoke the *manage tags* permission. All tag-related commands require the command to be sent as a
|
||||
reply to the target member's message.
|
||||
|
||||
- `/setChatMemberTag <tag>` calls `setChatMemberTag` for that user. The tag is the
|
||||
command's remaining text after removing at most one leading space; it is not
|
||||
otherwise trimmed.
|
||||
- `/removeChatMemberTag` clears that user's tag by sending `null`.
|
||||
- `/setCanManageTags true` invokes `promoteChatAdministrator` with
|
||||
`canManageTags = true`. Any remaining text other than exact, lowercase `true`
|
||||
sets that permission to `false`.
|
||||
## Arguments
|
||||
|
||||
The bot sends no success reply for these operations and does not register commands
|
||||
in Telegram's command menu.
|
||||
| Position | Value | Sample | Description |
|
||||
|----------|-------|--------|-------------|
|
||||
| 1 | `BOT_TOKEN` | `1234567890:AABBccDDeeFF` | Telegram bot token |
|
||||
|
||||
## Message trigger and storage
|
||||
Optional arguments (any order after the token):
|
||||
|
||||
For every delivered group content message that can be interpreted as potentially
|
||||
coming from a user, the bot sends two replies:
|
||||
| Value | Sample | Description |
|
||||
|-------|--------|-------------|
|
||||
| `debug` | `debug` | Enable verbose debug logging |
|
||||
| `testServer` | `testServer` | Connect to the Telegram test server instead of production |
|
||||
|
||||
- `Tag after casting: <tag>` using the typed `senderTag` property;
|
||||
- `Tag by getting via risk API: <tag>` using the raw `sender_tag` field.
|
||||
## Bot Commands
|
||||
|
||||
The displayed value may be `null`. Command messages also reach this content handler.
|
||||
Tags and tag-management rights are stored by Telegram as chat-member state; this
|
||||
example has no local map, database, or persistence layer. It prints its own bot
|
||||
information at startup and logs every received update.
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/setChatMemberTag <tag>` | Set a custom tag on the replied-to member |
|
||||
| `/removeChatMemberTag` | Remove the custom tag from the replied-to member |
|
||||
| `/setCanManageTags <true\|false>` | Grant (`true`) or revoke (`false`) the *manage tags* admin right for the replied-to member |
|
||||
|
||||
## Setup, permissions, and privacy
|
||||
All commands must be sent as a **reply** to the target user's message.
|
||||
|
||||
1. Create a bot, obtain its token, and keep it private.
|
||||
2. Add it to a test group and allow it to send messages.
|
||||
3. Grant the Telegram administrator rights needed to set member tags and promote
|
||||
members/change their `canManageTags` permission.
|
||||
## Capabilities
|
||||
|
||||
To inspect ordinary group messages, configure bot privacy so Telegram delivers them;
|
||||
an administrator bot normally has broader visibility, but verify delivery in the
|
||||
target group. The bot publicly echoes sender tags and logs full updates, so protect
|
||||
the group and process output. The code configures no webhook.
|
||||
- Sets custom tags on group members via `setChatMemberTag`
|
||||
- Removes tags via `removeChatMemberTag`
|
||||
- Promotes members with tag management permission via `promoteChatMember`
|
||||
- Reads existing tag information through the Risk API (`getChatMember`)
|
||||
- Runs via long polling
|
||||
|
||||
## Run
|
||||
|
||||
From the repository root:
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./gradlew :TagsBot:run --args="BOT_TOKEN"
|
||||
../gradlew run --args="BOT_TOKEN"
|
||||
```
|
||||
|
||||
The first argument is the required token. Optional exact flags may follow it in any
|
||||
order: `debug` enables formatted logging, and `testServer` selects Telegram's test
|
||||
environment. Other arguments are ignored.
|
||||
|
||||
@@ -3,6 +3,7 @@ import dev.inmo.kslog.common.LogLevel
|
||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||
import dev.inmo.kslog.common.setDefaultKSLog
|
||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.tgbotapi.abstracts.FromUser
|
||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
||||
@@ -39,14 +40,6 @@ import dev.inmo.tgbotapi.utils.buildEntities
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
/**
|
||||
* Starts the long-polling chat-member-tag example.
|
||||
*
|
||||
* The first element of [args] must be the bot token. Optional exact values `debug`
|
||||
* and `testServer` enable diagnostic logging and Telegram's test environment.
|
||||
* Reply-based commands set or clear a group member's tag and tag-management right;
|
||||
* delivered group content receives replies showing its typed and raw sender tag.
|
||||
*/
|
||||
suspend fun main(vararg args: String) {
|
||||
val botToken = args.first()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user