Compare commits

...

453 Commits

Author SHA1 Message Date
447de0c3ce update examples for ktgbotapi 37.0.0 2026-08-30 17:44:15 +06:00
ce48393895 update readmes 2026-08-21 15:51:06 +06:00
016400a821 improve communities bot 2026-08-21 12:08:33 +06:00
2dd20f51db improvements in rich messages bot 2026-08-17 23:48:59 +06:00
1d9c7a35eb tmp improvements 2026-08-17 00:17:31 +06:00
f09dc34002 fixes 2026-08-16 23:43:15 +06:00
22e2d38e25 fixes 2026-08-16 16:03:10 +06:00
40425f6451 Add Bot API 10.2 Bot Subscriptions support example
New BotSubscriptionsBot demonstrating the 36.0.0 subscription-updates API:
- onBotSubscriptionUpdated trigger reading user / invoicePayload / state
- exhaustive handling of the typed BotSubscriptionUpdated.State sealed type
  (Active / Canceled / Failed data objects + Unknown value-class fallback)
- botSubscriptionUpdatedUpdatesFlow raw update flow
- waitBotSubscriptionUpdated expectation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:23:43 +06:00
56cc2e70c1 Add Bot API 10.2 Communities support example
New CommunitiesBot demonstrating the 36.0.0 communities API:
- onCommunityChatAdded / onCommunityChatRemoved triggers for the
  community_chat_added / community_chat_removed service events
- reading the Community (id/name) from CommunityChatAdded
- ExtendedChat.community (ChatFullInfo.community) via getChat
- waitCommunityChatAdded expectation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:22:18 +06:00
868cf646bd Add Bot API 10.2 Ephemeral Messages support example
New EphemeralMessagesBot demonstrating the 36.0.0 ephemeral-message API:
- sendTextMessage with receiverUserId + callbackQueryId to send a message
  visible only to one user in a group, in response to a callback query
- PossiblyEphemeralMessage detection of ephemeral messages
- editEphemeralMessageText / deleteEphemeralMessage addressed by
  chatId + receiverUserId + ephemeralMessageId
- reply() smart-branch replying ephemerally to an ephemeral message, and the
  explicit replyToEphemeral form
- BotCommand.isEphemeral flag

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:21:03 +06:00
2f5fa711a7 Add Bot API 10.2 Rich Messages input blocks and media examples
Extend RichMessagesBot with the 36.0.0 rich-message input additions:
- rich_blocks: build a rich message from the typed InputRichBlocks DSL
  (InputRichMessageBlocks { }) instead of an HTML/Markdown string
- rich_blocks_draft: stream a draft-only thinking() blocks sequence via
  sendRichMessageDraft, then finalize with sendRichMessage
- onPhoto: embed a received photo into a rich message both via
  InputRichMessageMedia (tg://photo?id=) referenced from HTML and as a
  first-class photo() media block inside the blocks tree

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:17:18 +06:00
5a2ba634c6 Update telegram_bot_api_version to 36.0.0 2026-07-19 17:11:00 +06:00
b209be74aa Merge pull request #364 from InsanusMokrassar/35.0.0
35.0.0
2026-07-01 17:44:58 +06:00
9737371a7e add medialess variant of markdown text 2026-07-01 15:10:41 +06:00
b4dddeed74 improve examples 2026-06-30 15:57:34 +06:00
e70650d213 add two bots for testing of new functionality 2026-06-29 18:02:05 +06:00
52b6df8ac1 Merge pull request #362 from InsanusMokrassar/34.0.0
34.0.0
2026-06-14 11:41:12 +06:00
8a024a3676 Update telegram_bot_api_version to 34.0.0 2026-06-14 01:30:27 +06:00
913c992e9d managed bots improvements 2026-06-12 22:26:24 +06:00
42c087b906 checks of live photos 2026-06-11 00:51:54 +06:00
242c5fc4df small improvement in chat management bot 2026-06-10 23:39:57 +06:00
9318891d94 buildable new bots 2026-06-06 13:33:37 +06:00
95a619431c fix of guest bot sample 2026-05-30 15:34:18 +06:00
3b61976734 add guest bot 2026-05-23 00:00:36 +06:00
b7f50b514f update tgbotapi version 2026-05-20 18:06:50 +06:00
89e1eec53e Merge pull request #358 from InsanusMokrassar/33.0.0
33.0.0
2026-04-18 19:34:46 +06:00
00ab078891 build fix 2026-04-18 18:40:16 +06:00
bd71e642b2 upfill polls bot 2026-04-15 16:52:54 +06:00
514d9d68b8 add checks of save button and other fixes 2026-04-15 15:37:50 +06:00
9746a068b7 update ManagedBotsBot 2026-04-14 19:58:42 +06:00
9903e0e323 add more infos 2026-04-14 18:19:57 +06:00
8268cd9bf4 add showing of request managed bot 2026-04-14 18:12:25 +06:00
b4e2d52e7e add managedbotsbot sample 2026-04-14 16:31:26 +06:00
f829ce7281 Merge branch 'master' into 33.0.0 2026-04-13 16:03:22 +06:00
20b2ae8175 update dependencies 2026-04-13 16:01:06 +06:00
29ad52b506 Merge pull request #352 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v31.2.0
2026-03-05 00:04:38 +06:00
renovate[bot]
b848c6bfad Update telegram_bot_api_version to v31.2.0 2026-03-04 17:50:36 +00:00
6642b95af2 Merge pull request #357 from InsanusMokrassar/31.1.0
31.1.0
2026-03-03 13:31:56 +06:00
828ab43317 add opportunity to set/unset tags rights in tags bot 2026-03-02 19:21:48 +06:00
1a4533221c add showing of custom emoji in button 2026-03-02 18:55:05 +06:00
e304a5ecab add TagsBot 2026-03-02 18:02:35 +06:00
600ac8ebbf Merge pull request #356 from InsanusMokrassar/31.0.0
31.0.0
2026-03-02 16:58:51 +06:00
07403546f4 update telegram bot api version 2026-02-24 15:07:13 +06:00
9d4b7b5a50 add gifts bot 2026-02-24 14:00:26 +06:00
e1f5e40143 fix of build 2026-02-20 19:21:23 +06:00
186a0f7abf update telegram bot api version 2026-02-20 18:58:24 +06:00
e660f06edf add showing of user audios and update telegram bot api version onto temporal version 2026-02-18 22:24:32 +06:00
fb6ed8b7ae rename GetMe to My bot 2026-02-18 19:59:34 +06:00
9981e82a10 small improvement of keyboards bot 2026-02-17 12:45:22 +06:00
bef86042f9 add support of styling for buttons 2026-02-16 23:31:59 +06:00
d1791b3058 add drafts bot 2026-02-16 18:57:43 +06:00
0432611f85 improve test bots 2026-02-16 17:40:23 +06:00
6b27aa01fb fix of topics according to 9.3 2026-02-15 23:34:37 +06:00
523e428bcb update dependencies 2026-02-15 15:52:53 +06:00
0e8714cf2b Merge pull request #351 from InsanusMokrassar/29.0.0
29.0.0
2025-09-28 10:54:27 +06:00
ea74f884bf change vrsion of telegram bot api 2025-09-25 14:05:44 +06:00
69cbc257b5 Merge branch 'master' into 29.0.0 2025-09-25 14:04:47 +06:00
d3e6014e06 fix dependencies 2025-09-23 18:29:42 +06:00
1d260f82e9 Merge branch 'master' into 29.0.0 2025-09-23 18:29:13 +06:00
5d0b48c4b7 update dependencies 2025-09-23 18:16:40 +06:00
34be1a25b2 update SuggestedPostsBot to include printing of paid post publishing 2025-09-23 16:34:57 +06:00
990614e257 add checklists tasks 2025-09-23 15:43:58 +06:00
0d1bcf05fd improve SuggestedPostsBot 2025-09-23 13:38:54 +06:00
7d5cb58a3f small improvement with suggested post approvement failes 2025-09-23 12:52:31 +06:00
261df14412 improvements in suggested posts bot 2025-09-23 12:49:11 +06:00
81ba5831c3 add suggested posts sample 2025-09-22 12:44:52 +06:00
0b9c715e25 start migration onto 29.0.0 2025-09-18 01:05:03 +06:00
0216919145 Merge pull request #345 from InsanusMokrassar/28.0.0
28.0.0
2025-08-23 16:14:17 +06:00
e2d56a4d80 finalize changes 2025-08-11 15:56:20 +06:00
70aca52960 update dependency 2025-08-11 15:22:21 +06:00
6c0d961339 temporal improvements for fsm 2025-08-11 15:22:21 +06:00
renovate[bot]
a3cdf693f2 Update ktor monorepo to v3.2.3 2025-08-11 15:22:02 +06:00
renovate[bot]
e378c6630c Update telegram_bot_api_version to v27.1.2 2025-08-07 10:58:59 +00:00
707ad9a160 Merge pull request #331 from InsanusMokrassar/renovate/compose_version
Update plugin org.jetbrains.compose to v1.8.2
2025-07-29 17:31:19 +06:00
68e9830a8f Merge pull request #341 from InsanusMokrassar/27.1.0
27.1.0
2025-07-29 17:28:01 +06:00
55ebdeadbc migration onto 27.1.0 2025-07-27 14:32:39 +06:00
renovate[bot]
d4f3d4bc68 Update plugin org.jetbrains.compose to v1.8.2 2025-07-25 17:23:59 +00:00
b3d06c9773 Merge pull request #340 from InsanusMokrassar/27.0.0
27.0.0
2025-07-25 23:22:44 +06:00
e6e3eabf97 update dependencies 2025-07-22 19:52:24 +06:00
47efedf311 start migration onto 27.0.0 2025-07-15 13:35:35 +06:00
8423b1377b Merge pull request #336 from InsanusMokrassar/26.1.0
26.1.0
2025-07-10 17:21:41 +06:00
d0029603ce improvements according to latest changes 2025-07-08 18:41:18 +06:00
8d8fa74779 start migration onto 26.1.0 2025-07-08 11:58:35 +06:00
459a70c47b Merge pull request #333 from InsanusMokrassar/26.0.0
26.0.0
2025-06-16 12:15:06 +06:00
88102f3afa update telegram bot api version up to last release one 2025-06-15 10:33:03 +06:00
a621058fdd update dependencies 2025-06-14 23:48:32 +06:00
56e072aabe Merge pull request #332 from InsanusMokrassar/25.0.0
25.0.0
2025-06-14 23:35:44 +06:00
73f05bbcd7 fix in repositories 2025-06-14 23:21:00 +06:00
f053013360 fix of build with adding google repository in webapp sample? 2025-06-14 22:28:14 +06:00
bc39279c6c update telegram bot api version 2025-06-01 22:18:08 +06:00
ad8fa92e87 protess on tests 2025-06-01 22:15:57 +06:00
b0554adb7f add delete_story test 2025-06-01 21:19:37 +06:00
ad90180def fixes 2025-06-01 20:23:19 +06:00
69eda92bc7 add set business account profile photo tests 2025-06-01 19:24:01 +06:00
aee070c6c6 add check of set business account bio 2025-05-25 19:42:47 +06:00
36163d5619 add sample for set business account username 2025-05-25 19:30:51 +06:00
92d1c7a402 add sections and update version of library 2025-05-25 13:49:02 +06:00
7ce784d0a2 add device storage sample and beckground/text color config in webapps 2025-05-25 13:01:22 +06:00
d203d48391 add support of setBusinessAccountName 2025-05-24 22:32:10 +06:00
9352bb0090 add tests for reading and removing business messages 2025-05-18 22:04:03 +06:00
b1bb11d826 update nexus.inmo.dev maven repo 2025-05-18 21:13:43 +06:00
349517462e start add tests for new business account features 2025-05-11 21:28:32 +06:00
1708cad654 start migration onto 25.0.0 2025-05-11 21:13:58 +06:00
f87a9c5c66 Merge pull request #316 from InsanusMokrassar/renovate/kotlin-monorepo
Update kotlin monorepo to v2.1.10
2025-02-15 16:46:54 +06:00
a7b54e4b63 Merge pull request #320 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.24.6
2025-02-15 16:45:39 +06:00
renovate[bot]
436213492d Update kotlin monorepo to v2.1.10 2025-02-15 10:45:34 +00:00
renovate[bot]
0c2110a71d Update dependency dev.inmo:micro_utils.ktor.server to v0.24.6 2025-02-15 10:45:29 +00:00
949fa1a429 Merge pull request #319 from InsanusMokrassar/renovate/ktor-monorepo
Update ktor monorepo to v3.1.0
2025-02-15 16:45:04 +06:00
97cdd5a95f Merge pull request #321 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v23.2.0
2025-02-15 16:44:49 +06:00
renovate[bot]
0cb116acef Update telegram_bot_api_version to v23.2.0 2025-02-15 10:32:03 +00:00
renovate[bot]
a0332c4efd Update ktor monorepo to v3.1.0 2025-02-11 19:14:32 +00:00
f6bce640da update dependencies 2025-02-01 09:26:06 +06:00
d22a99da19 Merge pull request #315 from InsanusMokrassar/23.1.1
23.1.1
2025-01-29 09:09:14 +06:00
467a3a1710 Update gradle.properties 2025-01-27 10:57:07 +06:00
5810bc5930 migration onto 23.1.1 2025-01-27 09:20:08 +06:00
2cf2c4264e Merge pull request #310 from InsanusMokrassar/renovate/ktor-monorepo
Update ktor monorepo to v3.0.3
2025-01-03 14:17:33 +06:00
renovate[bot]
3d5c2ee4b8 Update ktor monorepo to v3.0.3 2025-01-03 08:17:24 +00:00
360c6b4364 Merge pull request #312 from InsanusMokrassar/renovate/compose_version
Update plugin org.jetbrains.compose to v1.7.3
2025-01-03 14:16:48 +06:00
renovate[bot]
bb6a0a125a Update plugin org.jetbrains.compose to v1.7.3 2024-12-20 01:19:28 +00:00
6a61da2eb7 Merge pull request #311 from InsanusMokrassar/22.0.0
22.0.0
2024-12-09 08:54:48 +06:00
8cd75673f5 add opportunity to set custom emoji status from webapp 2024-12-08 13:17:39 +06:00
d294d0ef59 update events listeners 2024-12-08 11:58:47 +06:00
2ab8ccbfdf small refactor in webapp 2024-12-08 10:20:37 +06:00
d12e9aa032 rework to use compose 2024-12-08 10:14:42 +06:00
76f151586e start migration to compose in webapp 2024-12-07 11:36:39 +06:00
1c437690e4 migrate webapp 2024-12-06 16:32:47 +06:00
222c7ec8ee 22.0.0 2024-12-06 13:18:18 +06:00
59778a3add Merge pull request #309 from InsanusMokrassar/21.0.0
21.0.0
2024-11-30 17:58:05 +06:00
3e20835bc6 Update gradle.properties 2024-11-30 17:11:07 +06:00
c3ad2d4319 upgrade custom bot to include context data and additional context data 2024-11-30 14:42:21 +06:00
59fca968d7 update tgbotapi and include sample of context data in custom bot 2024-11-29 12:50:33 +06:00
f03ba5f177 Merge pull request #302 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v20.0.1
2024-11-13 13:53:07 +06:00
renovate[bot]
855d2c1296 Update telegram_bot_api_version to v20.0.1 2024-11-11 10:46:16 +00:00
280f5abce0 Merge pull request #294 from InsanusMokrassar/renovate/major-ktor-monorepo
Update dependency io.ktor:ktor-client-logging-jvm to v3
2024-11-04 01:39:22 +06:00
renovate[bot]
ed81e76ef8 Update dependency io.ktor:ktor-client-logging-jvm to v3 2024-11-03 19:39:16 +00:00
541b76b292 Merge pull request #301 from InsanusMokrassar/20.0.0
20.0.0
2024-11-02 00:34:52 +06:00
5b580b5a15 migration onto 20.0.0 2024-11-01 23:48:29 +06:00
86790ee414 add copyText sample button 2024-11-01 23:21:33 +06:00
0bbe430374 update ktgbotapi 2024-11-01 15:27:17 +06:00
b7d53a7410 Merge pull request #298 from InsanusMokrassar/19.0.0
19.0.0
2024-11-01 14:55:49 +06:00
73064db226 small adaptation 2024-10-30 18:09:23 +06:00
a50eda366d update dependencies and add webhooks sample 2024-10-30 14:38:46 +06:00
e34f0ec9d8 Update gradle-wrapper.properties 2024-10-22 17:36:59 +06:00
c2237f7e87 Merge pull request #297 from InsanusMokrassar/18.2.2
18.2.2
2024-10-22 17:35:39 +06:00
0bbc6a9555 Update gradle.properties 2024-10-22 17:22:41 +06:00
d4d8508abf add middlewares sample in custom bot 2024-10-15 13:52:26 +06:00
9acb64fda9 Merge pull request #291 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.22.4
2024-09-26 07:59:14 +06:00
renovate[bot]
760ae36207 Update dependency dev.inmo:micro_utils.ktor.server to v0.22.4 2024-09-26 01:59:00 +00:00
5c6b1b7171 Merge pull request #290 from InsanusMokrassar/renovate/serialization_version
Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.3
2024-09-26 07:58:43 +06:00
6e06357541 Merge pull request #265 from InsanusMokrassar/renovate/ktor-monorepo
Update dependency io.ktor:ktor-client-logging-jvm to v2.3.12
2024-09-26 07:58:28 +06:00
38f46dfa3b Merge pull request #292 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v18.2.1
2024-09-26 07:57:57 +06:00
renovate[bot]
e7f7ef16ac Update telegram_bot_api_version to v18.2.1 2024-09-25 21:35:02 +00:00
renovate[bot]
d100a5a336 Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.3 2024-09-19 18:27:24 +00:00
5f0f2ce76d Merge pull request #289 from InsanusMokrassar/18.2.0
18.2.0
2024-09-09 02:33:18 +06:00
14235e7bd4 update GiveawaysBot 2024-09-08 23:38:19 +06:00
6eafd89542 add println of giveaway content 2024-09-08 19:44:51 +06:00
ed2922045c add giveaways bot 2024-09-08 19:33:43 +06:00
21ec50c773 add opportunity to use test server in custom bot 2024-09-08 15:53:46 +06:00
ab362e8c3b start updating up to 18.2.0 2024-09-07 02:43:05 +06:00
346755b41c Merge pull request #282 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v18.1.0
2024-09-05 03:17:20 +06:00
renovate[bot]
a601674d71 Update telegram_bot_api_version to v18.1.0 2024-09-04 21:02:48 +00:00
renovate[bot]
cea610a0f8 Update dependency io.ktor:ktor-client-logging-jvm to v2.3.12 2024-09-02 02:00:00 +00:00
b6c92f754f Merge pull request #287 from InsanusMokrassar/18.0.0
18.0.0
2024-09-02 07:59:08 +06:00
023b810d07 update micro_utils dependency 2024-09-02 01:24:11 +06:00
0ec543d5c5 update sample of MemberUpdatedWatcher bot 2024-08-30 23:32:29 +06:00
777604e5a0 update new samples 2024-08-30 19:06:38 +06:00
999c33b2f5 Merge pull request #286 from Nik-mmzd/chatmemberupdated
Add MemberUpdatedWatcherBot example utilizing new 18.0.0 extensions
2024-08-30 18:42:18 +06:00
ca0427bfdd Merge branch '18.0.0' into chatmemberupdated 2024-08-30 18:41:59 +06:00
a62a14a599 migration onto 18.0.0 2024-08-30 18:40:39 +06:00
McModder
3efd3463a3 Add MemberUpdatedWatcherBot example utilizing new 18.0.0 extensions 2024-08-29 22:40:47 +03:00
590f9ec6d8 Merge pull request #280 from InsanusMokrassar/17.0.0
17.0.0
2024-08-15 19:17:08 +06:00
acdbd4d2ea update HelloBot 2024-08-15 01:51:10 +06:00
d2d913fca8 update telegram bots api 2024-08-14 23:33:43 +06:00
75726cac89 Merge pull request #276 from InsanusMokrassar/16.0.0
16.0.0
2024-08-12 08:13:59 +06:00
71b64689d0 Update gradle.properties 2024-08-12 02:32:28 +06:00
5ba2fc5bab update githab workflow 2024-08-11 19:52:15 +06:00
51a5bfb81a update gradle wrapper 2024-08-11 19:49:31 +06:00
35e330c016 update kotlin 2024-08-11 19:49:31 +06:00
90d447fbcf update up to 16.0.0 2024-08-11 19:49:31 +06:00
2c5da5da9f Merge pull request #278 from InsanusMokrassar/15.3.0
15.3.0
2024-08-02 19:29:33 +06:00
f79e43364a fixes in sample of business_connections bot 2024-08-02 16:48:56 +06:00
f5a9efa3e7 add pin/unpin in business connection 2024-08-02 00:39:37 +06:00
b70b6d1e2b Merge pull request #273 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v15.2.0
2024-07-29 15:48:37 +06:00
renovate[bot]
3f36a04ac2 Update telegram_bot_api_version to v15.2.0 2024-07-15 15:22:03 +00:00
62b830d31b Merge pull request #274 from InsanusMokrassar/15.1.0
15.1.0
2024-07-12 14:55:52 +06:00
06459ebc0a improve boosts info bot 2024-07-11 21:14:53 +06:00
673424b234 Merge pull request #275 from bpavuk/bpavuk.code-cleanup
code cleanup
2024-07-11 19:55:13 +06:00
bpavuk
5d156f6708 reverted explicit class cast removal 2024-07-11 16:48:54 +03:00
bpavuk
529f4156fd reverted elvis fold 2024-07-11 16:47:57 +03:00
bpavuk
7842ac0dac Added description for StarTransactionsBot 2024-07-10 19:51:07 +03:00
bpavuk
358f2d27d3 reverted CustomBot body 2024-07-10 19:33:40 +03:00
bpavuk
7964dc4eea code cleanup
optimized imports here and there, made CustomBot working as intended
2024-07-10 19:25:00 +03:00
9fb6570d21 add refund reply and paid media info content additional println 2024-07-10 17:48:43 +06:00
f750589fd3 add sendPaid in StarTransactions bot 2024-07-10 17:07:36 +06:00
481533bee2 start update up to 15.1.0 2024-07-09 17:00:26 +06:00
a1a4338869 Merge pull request #272 from InsanusMokrassar/15.0.0
15.0.0
2024-07-07 22:27:40 +06:00
d8e5825ccf update gradle parameters 2024-07-07 19:35:21 +06:00
3a4c0c4226 start 15.0.0 2024-06-24 18:13:19 +06:00
b85d7a697c update dependencies 2024-06-21 22:40:19 +06:00
ad57e4142c Merge pull request #268 from InsanusMokrassar/14.0.0
14.0.0
2024-06-01 20:32:53 +06:00
c7068182e3 make initialization of web app server more verbose 2024-06-01 20:31:51 +06:00
a5740e6315 change telegram bot api version to release one 2024-06-01 13:37:31 +06:00
3a35995bc7 several fixes and improvements 2024-06-01 13:19:05 +06:00
41fc5a9a4c Merge pull request #264 from InsanusMokrassar/13.0.0
13.0.0
2024-05-10 20:56:51 +06:00
79700f24e5 small updates 2024-05-10 19:53:21 +06:00
73c1af15b3 add CustomBot 2024-05-10 18:46:47 +06:00
aa9ca976f0 update polls to include showing of polls question and explanation text sources passing 2024-05-10 16:45:47 +06:00
238533a350 update polls sample 2024-05-10 16:35:07 +06:00
6f2a8bb0be Update gradle.properties 2024-04-26 10:45:55 +06:00
99232b53d7 Merge pull request #260 from InsanusMokrassar/12.0.0
12.0.0
2024-04-21 00:25:25 +06:00
30358f7d2f Merge pull request #258 from InsanusMokrassar/11.0.0
11.0.0
2024-04-21 00:24:07 +06:00
11a97c520a update 2024-04-19 19:34:48 +06:00
d6a6ad8d37 improvements in samples 2024-04-19 18:10:46 +06:00
c04a367375 fixes and improvements 2024-04-18 20:06:59 +06:00
b660bf5f42 upgrade to support new ktgbotapi 2024-04-18 17:03:53 +06:00
57dd2380cd fix up to 11.0.0 2024-03-18 13:46:15 +06:00
fbb41c7714 start update up to tgbotapi 11.0.0 2024-03-18 12:32:15 +06:00
9170d30b2f Update gradle-wrapper.properties 2024-03-02 00:08:22 +06:00
2f3fd2e53b Update gradle.properties 2024-03-02 00:05:34 +06:00
88697fb5a6 Merge pull request #253 from InsanusMokrassar/10.1.0
10.1.0
2024-02-17 13:38:13 +06:00
578d00cac6 Update gradle.properties 2024-02-17 13:37:50 +06:00
13ecb3f0df update dependencies 2024-02-17 01:51:32 +06:00
a008d861da Merge pull request #248 from InsanusMokrassar/10.0.0
10.0.0
2024-01-12 14:57:51 +06:00
6f3766dff6 fixes in samples and update up to 10.0.0 2024-01-12 14:23:27 +06:00
fda366d820 add boosts sample 2024-01-12 00:49:59 +06:00
578887ac63 update userChatShared bot 2024-01-12 00:29:48 +06:00
6a04b3980c improvements in users requests 2024-01-10 23:10:28 +06:00
984ffb8bae update dependencies 2024-01-10 16:47:31 +06:00
2bcec6487d fixes 2024-01-09 18:30:54 +06:00
a5e3bfc3fe update dependency of tgbotapi 2024-01-09 18:05:30 +06:00
941afd0902 update LinkPreviewsBot 2024-01-09 17:57:49 +06:00
94c014b308 add LinkPreviewsBot 2024-01-09 17:56:33 +06:00
538cc9d44f improvement of ReactionsInfoBot 2024-01-09 13:55:35 +06:00
cb29726487 add showing of reactions count in println 2024-01-09 13:06:18 +06:00
262ef26239 updates and fixes 2024-01-08 19:23:06 +06:00
41efe5e141 update tgbotapi version 2024-01-08 15:55:05 +06:00
05e289975a add reactions info bot 2024-01-08 15:46:23 +06:00
753d686fab build fixes 2024-01-08 13:21:41 +06:00
281243c7e5 update dependencies 2024-01-08 10:14:08 +06:00
3609ae6bc2 Merge pull request #240 from InsanusMokrassar/renovate/serialization_version
Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.6.2
2023-12-25 09:00:04 +06:00
4f128f3421 Merge pull request #239 from InsanusMokrassar/renovate/io.ktor-ktor-client-logging-jvm-2.x
Update dependency io.ktor:ktor-client-logging-jvm to v2.3.7
2023-12-25 08:59:47 +06:00
ada6cd61d7 Merge pull request #247 from InsanusMokrassar/renovate/kotlin-monorepo
Update kotlin monorepo to v1.9.22
2023-12-25 08:59:28 +06:00
renovate[bot]
051d647004 Update kotlin monorepo to v1.9.22 2023-12-21 14:11:32 +00:00
renovate[bot]
d21606860a Update dependency io.ktor:ktor-client-logging-jvm to v2.3.7 2023-12-07 12:54:06 +00:00
renovate[bot]
93c0fcb5bd Update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.6.2 2023-11-30 16:56:11 +00:00
b1b8d0eb75 Merge pull request #243 from InsanusMokrassar/9.4.0
9.4.0
2023-11-26 18:17:08 +06:00
2ac23f70ab Update gradle.properties 2023-11-26 15:28:56 +06:00
e155373655 small improvement in GetMe 2023-11-25 12:57:11 +06:00
d842dab5b8 update dependencies 2023-11-25 12:44:05 +06:00
7186d5e624 Merge pull request #215 from InsanusMokrassar/renovate/io.ktor-ktor-client-logging-jvm-2.x
Update dependency io.ktor:ktor-client-logging-jvm to v2.3.5
2023-11-06 13:27:49 +06:00
renovate[bot]
8fefb17599 Update dependency io.ktor:ktor-client-logging-jvm to v2.3.5 2023-11-06 07:23:57 +00:00
bcf4ae5888 Merge pull request #237 from InsanusMokrassar/9.3.0
9.3.0
2023-11-06 13:23:44 +06:00
7090db148e Update gradle.properties 2023-11-05 13:53:10 +06:00
7d786f0e06 improvements 2023-11-05 12:33:45 +06:00
c88f84011f Update build.yml 2023-11-05 03:41:21 +06:00
b8cc8854ea Update gradle-wrapper.properties 2023-11-05 02:43:40 +06:00
13470999e8 Update gradle.properties 2023-11-05 02:43:13 +06:00
af04a854ef fixes 2023-10-25 15:33:05 +06:00
44e86c9349 small fixes in ResenderBot Lib 2023-10-21 00:50:03 +06:00
65c32d97d5 update native buildings configuration 2023-10-21 00:28:12 +06:00
9b7605591e update dependencies to work with linux arm 64 2023-10-20 22:50:36 +06:00
89d5a4f911 add support of arch target 2023-10-20 22:29:59 +06:00
53cf212175 fixes 2023-10-17 23:28:13 +06:00
28301a92c9 update webapp sample 2023-10-17 23:23:24 +06:00
f814b11777 update up to tgbotapi 9.3.0 2023-10-15 23:29:02 +06:00
9773a74890 update ktgbotapi version 9.2.2 2023-10-11 15:21:44 +06:00
a81cfaaba9 Merge pull request #234 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v9.2.1
2023-09-30 07:26:47 +06:00
renovate[bot]
ee599611f3 Update telegram_bot_api_version to v9.2.1 2023-09-29 19:24:07 +00:00
d3d6cd16c6 Merge pull request #235 from InsanusMokrassar/9.2.0
Migration onto 9.2.0
2023-09-28 19:37:41 +06:00
02c3d3da1a update webapp sample to use cloud storage 2023-09-25 23:20:48 +06:00
0ad8e61c0c one more improvement 2023-09-25 16:30:17 +06:00
8f80b7e066 small fixes and improvements 2023-09-25 16:28:49 +06:00
48d1077ce4 update webapp 2023-09-25 16:20:25 +06:00
6922a6d667 fixes in rights bot 2023-09-25 14:55:37 +06:00
676ce0df80 start adding channels rights changer 2023-09-25 00:19:34 +06:00
d97c2a0562 start migration onto 9.2.0 2023-09-23 01:51:04 +06:00
35e0cb4a46 update tgbotapi 2023-09-07 22:41:02 +06:00
30f5513f54 Merge pull request #230 from InsanusMokrassar/9.1.0
Update up to 9.1.0
2023-08-20 14:31:47 +06:00
fff8edde5f update PollsBot 2023-08-20 14:30:55 +06:00
e28a795796 Update PollsBot.kt 2023-08-20 11:38:56 +06:00
d289c2101d add polls sample bot 2023-08-20 02:31:23 +06:00
2ce47074d8 update dependencies 2023-08-19 18:32:43 +06:00
281f0840eb Merge pull request #224 from InsanusMokrassar/9.0.0
9.0.0
2023-07-01 16:26:53 +06:00
34ed962104 Update gradle-wrapper.properties 2023-07-01 16:26:45 +06:00
aa3337bf3a update tgbotapi version 2023-07-01 13:52:48 +06:00
31d29712be small improvements 2023-07-01 03:54:42 +06:00
88b348376f add local folders and files into gitignore 2023-06-30 22:53:12 +06:00
0d9e295baa start migration onto 9.0.0 2023-06-30 17:49:19 +06:00
ea08bac6e8 Merge pull request #214 from InsanusMokrassar/8.0.0
8.0.0
2023-06-09 01:54:39 +06:00
a85fdc227e Update gradle.properties 2023-06-08 22:47:48 +06:00
43482ee94e start 8.0.0 2023-05-28 21:22:40 +06:00
4addb6c755 Merge pull request #212 from InsanusMokrassar/7.1.3
7.1.3
2023-05-20 22:13:25 +06:00
7d958b6edb Update gradle.properties 2023-05-20 22:12:52 +06:00
323c21f415 upgrade of hello bot 2023-05-19 22:45:46 +06:00
6350581739 Merge pull request #209 from InsanusMokrassar/7.1.2
7.1.2
2023-05-06 15:38:14 +06:00
ea1d40fd05 downgrade microutils 2023-05-06 13:27:34 +06:00
8cee63a0fb update dependencies 2023-05-06 13:13:38 +06:00
d42ef2c6cb Merge pull request #208 from InsanusMokrassar/7.1.1
7.1.1
2023-05-04 08:42:52 +06:00
c7fe90ddd7 update dependencies 2023-05-01 02:16:20 +06:00
acb382d3f7 Merge pull request #206 from InsanusMokrassar/7.1.0
7.1.0
2023-04-22 20:31:55 +06:00
0cfe60fd77 Update gradle.properties 2023-04-22 20:31:41 +06:00
6719b9e17c Update Bot.kt 2023-04-22 16:39:59 +06:00
8d33dc0ab2 Update README.md 2023-04-22 16:36:14 +06:00
3e2ccf9cf1 add answerInlineQuery with WebAppInfo 2023-04-22 11:06:03 +06:00
eccbe71e68 finish checking update 2023-04-22 00:17:39 +06:00
24c74f3b1a in keyboards bot add sample with sending of inline query 2023-04-22 00:11:56 +06:00
d7a7e7153e add inline queries sample 2023-04-21 23:21:15 +06:00
0b37acb7a9 Merge pull request #202 from InsanusMokrassar/7.0.2
7.0.2
2023-04-20 03:55:59 +06:00
3925ef9423 add opportunity to set port different with 8080 in WebAppServer sample 2023-04-19 21:46:35 +06:00
c6019b1862 complete sample with native 2023-04-19 20:20:11 +06:00
7b996fe1de rollback ktor version in native sample 2023-04-18 11:31:28 +06:00
4e3c186952 update dependencies 2023-04-18 03:27:56 +06:00
8fdf715419 Update build.yml 2023-04-04 09:57:28 +06:00
fca8704cec Update build.yml 2023-04-04 09:56:41 +06:00
bf499ee780 Update gradle.properties 2023-04-04 01:45:15 +06:00
9b10749411 update dependencies and other attributes 2023-04-04 01:02:18 +06:00
d3cb8a32ef add partially working native sample 2023-04-04 01:01:22 +06:00
0f0ad5a1af Update gradle.properties 2023-03-18 19:24:40 +06:00
c3bc55a15c Merge pull request #200 from InsanusMokrassar/7.0.0
7.0.0
2023-03-11 23:18:32 +06:00
253328f49a complete sample with sticker set handler example 2023-03-11 21:39:08 +06:00
8ef50537ae some functionality of sticker set handler bot 2023-03-11 15:55:09 +06:00
7e7bbfaa93 update version of telegram bot api and start including of stickers sets handling bot 2023-03-11 15:24:18 +06:00
f152ede9b5 improve sticker info bot 2023-03-11 01:00:31 +06:00
fcedbf30da Merge pull request #186 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.17.3
2023-03-08 08:40:26 +06:00
renovate[bot]
fd030a92e3 Update dependency dev.inmo:micro_utils.ktor.server to v0.17.3 2023-03-07 20:44:36 +00:00
d54abf0b32 Merge pull request #196 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v6.0.3
2023-03-03 10:56:54 +06:00
renovate[bot]
877a20188f Update telegram_bot_api_version to v6.0.3 2023-03-02 21:17:02 +00:00
d0151ff048 Update gradle.properties 2023-02-28 22:57:26 +06:00
f8f517cfbb Merge pull request #195 from InsanusMokrassar/renovate/ktor_version
Update dependency io.ktor:ktor-server-cio to v2.2.4
2023-02-28 22:57:06 +06:00
renovate[bot]
b3cbbac917 Update dependency io.ktor:ktor-server-cio to v2.2.4 2023-02-28 13:44:59 +00:00
3e85bb4b22 Merge pull request #194 from InsanusMokrassar/6.0.0
6.0.0
2023-02-28 14:05:54 +06:00
4e0fb1c137 update dependencies 2023-02-27 22:39:17 +06:00
d8c90ef377 check and a little update 2023-02-27 22:38:05 +06:00
cb84fd0884 update dependencies 2023-02-27 20:36:03 +06:00
4379862c78 Merge pull request #188 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v5.2.1
2023-02-22 19:06:01 +06:00
renovate[bot]
ac1d812db0 Update telegram_bot_api_version to v5.2.1 2023-02-21 17:43:27 +00:00
f52590868c fix according to update of tgbotapi version 2023-02-17 16:04:32 +06:00
51c300c734 add LiveLocationsBot sample and update dependencies 2023-02-17 15:58:25 +06:00
f6082cff30 Merge pull request #189 from InsanusMokrassar/5.1.0
5.1.0
2023-02-06 15:47:11 +06:00
a40c16fe05 improvements and fixes 2023-02-06 14:07:44 +06:00
a7fe62f4af fix readmes 2023-02-06 12:11:02 +06:00
b9c745a21e add RightsChanger bot 2023-02-06 12:08:25 +06:00
1c2b068a94 start rights checker 2023-02-06 08:42:20 +06:00
51c2cb1b0e complete sample with request buttons 2023-02-05 23:43:27 +06:00
cfd4e2fcd5 Merge pull request #185 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v5.0.1
2023-01-19 00:29:38 +06:00
renovate[bot]
76ceeac757 Update telegram_bot_api_version to v5.0.1 2023-01-18 18:28:56 +00:00
340de11b0a Merge pull request #179 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.16.6
2023-01-19 00:28:23 +06:00
68a59ca5c8 Merge pull request #184 from InsanusMokrassar/renovate/ktor_version
Update dependency io.ktor:ktor-server-cio to v2.2.2
2023-01-19 00:27:56 +06:00
renovate[bot]
cdb8581318 Update dependency dev.inmo:micro_utils.ktor.server to v0.16.6 2023-01-18 16:58:56 +00:00
renovate[bot]
8b3a2ac1ed Update dependency io.ktor:ktor-server-cio to v2.2.2 2023-01-03 18:54:03 +00:00
a5b925fc59 Merge pull request #182 from InsanusMokrassar/0.5.0
0.5.0
2023-01-01 22:53:07 +06:00
9ec9f7a68c Update gradle.properties 2022-12-31 16:29:22 +06:00
0f2829945f add topics example 2022-12-31 15:45:48 +06:00
4eb80ea53c start 0.5.0 2022-12-30 22:12:12 +06:00
17cff21847 Update gradle.properties 2022-12-18 10:25:51 +06:00
431069d190 Merge pull request #178 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.16.2
2022-12-18 10:24:54 +06:00
renovate[bot]
fdbac78603 Update dependency dev.inmo:micro_utils.ktor.server to v0.16.2 2022-12-16 09:24:13 +00:00
da73acd379 Merge pull request #175 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.16.1
2022-12-13 09:00:47 +06:00
renovate[bot]
6e3880f152 Update dependency dev.inmo:micro_utils.ktor.server to v0.16.1 2022-12-09 14:50:53 +00:00
1ede6e58e6 Update gradle.properties 2022-12-08 11:27:03 +06:00
0e46f176fb Merge pull request #169 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v4.2.0
2022-12-05 12:29:53 +06:00
renovate[bot]
2bd449b8b8 Update telegram_bot_api_version to v4.2.0 2022-12-05 06:28:52 +00:00
82f9da0529 Merge pull request #172 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.15.0
2022-12-05 12:06:41 +06:00
78b7d468f2 Merge pull request #174 from InsanusMokrassar/renovate/kotlin-monorepo
Update kotlin monorepo to v1.7.22
2022-12-05 12:06:26 +06:00
renovate[bot]
08059f8174 Update dependency dev.inmo:micro_utils.ktor.server to v0.15.0 2022-12-04 17:47:48 +00:00
renovate[bot]
16766046d7 Update kotlin monorepo to v1.7.22 2022-11-28 14:43:44 +00:00
91ea20a269 Merge pull request #171 from InsanusMokrassar/4.1.3
4.1.3
2022-11-28 18:19:45 +06:00
11e280d177 Update gradle-wrapper.properties 2022-11-28 18:16:41 +06:00
a8d4a307ef Update gradle.properties 2022-11-28 16:32:46 +06:00
2bd2328a38 temporal update of version 2022-11-18 23:10:24 +06:00
139de35db9 Merge pull request #170 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.14.2
2022-11-15 21:32:13 +06:00
renovate[bot]
5dd22e1da2 Update dependency dev.inmo:micro_utils.ktor.server to v0.14.2 2022-11-15 07:32:25 +00:00
4186ab8270 Merge pull request #168 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v4.1.0
2022-11-11 00:41:58 +06:00
5aa69d7990 Update SimpleFSMBot.kt 2022-11-11 00:35:16 +06:00
renovate[bot]
df952c69b2 Update telegram_bot_api_version to v4.1.0 2022-11-10 18:28:56 +00:00
9a03a02bac Merge pull request #166 from InsanusMokrassar/renovate/kotlin-monorepo
Update kotlin monorepo to v1.7.21
2022-11-11 00:28:38 +06:00
0e7c050e9e Merge pull request #164 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.14.1
2022-11-11 00:27:53 +06:00
renovate[bot]
bdec902b58 Update dependency dev.inmo:micro_utils.ktor.server to v0.14.1 2022-11-10 14:27:54 +00:00
renovate[bot]
cc3c87590d Update kotlin monorepo to v1.7.21 2022-11-09 10:48:11 +00:00
910f892b89 Merge pull request #165 from InsanusMokrassar/4.0.0
4.0.0
2022-11-09 13:40:37 +06:00
8232cb4d62 updates 2022-11-08 17:49:41 +06:00
3b26971152 fixes 2022-11-08 17:00:53 +06:00
c0019bcbf8 update up to 4.0.0 2022-11-08 12:19:23 +06:00
c3dcb4d738 Merge pull request #163 from InsanusMokrassar/3.3.1
3.3.1
2022-11-01 16:49:56 +06:00
6a9921d4bd Update gradle.properties 2022-11-01 16:45:05 +06:00
33b14f320c update up to 3.3.1 2022-10-30 19:57:11 +06:00
50ad281132 Merge pull request #160 from InsanusMokrassar/3.3.0
3.3.0
2022-10-23 11:41:44 +06:00
6abfc3d369 3.3.0 2022-10-22 17:48:35 +06:00
f73620afec Merge pull request #154 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.12.17
2022-10-06 11:56:00 +06:00
a3d5112c83 Merge pull request #156 from InsanusMokrassar/renovate/ktor_version
Update dependency io.ktor:ktor-server-cio to v2.1.2
2022-10-06 11:55:26 +06:00
renovate[bot]
23abae07b7 Update dependency dev.inmo:micro_utils.ktor.server to v0.12.17 2022-10-06 05:55:14 +00:00
c5a9f657cf Merge pull request #157 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v3.2.7
2022-10-06 11:54:59 +06:00
renovate[bot]
51f7715915 Update telegram_bot_api_version to v3.2.7 2022-10-01 20:54:04 +00:00
renovate[bot]
bdfb900ce3 Update dependency io.ktor:ktor-server-cio to v2.1.2 2022-09-30 09:29:21 +00:00
27790a2576 Merge pull request #153 from InsanusMokrassar/3.2.6
3.2.6
2022-09-19 16:03:13 +06:00
e722055871 several small improvements 2022-09-19 14:58:43 +06:00
29e1552618 migration 2022-09-19 14:37:01 +06:00
7ef942a2b4 start migration onto 3.2.6 2022-09-19 11:19:21 +06:00
fdd4dfdbcf Merge pull request #151 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v3.2.4
2022-09-17 09:36:13 +06:00
renovate[bot]
ba88205249 Update telegram_bot_api_version to v3.2.4 2022-09-16 20:14:33 +00:00
9a38fe51f9 Merge pull request #150 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v3.2.3
2022-09-15 19:32:43 +06:00
renovate[bot]
c172bd1fa7 Update telegram_bot_api_version to v3.2.3 2022-09-15 12:48:34 +00:00
7392e85f1b Merge pull request #149 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.12.13
2022-09-15 10:07:05 +06:00
renovate[bot]
f2c7fd79d9 Update dependency dev.inmo:micro_utils.ktor.server to v0.12.13 2022-09-14 22:47:35 +00:00
9d77b61ea3 Merge pull request #148 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.12.12
2022-09-14 10:53:26 +06:00
renovate[bot]
d567b1382b Update dependency dev.inmo:micro_utils.ktor.server to v0.12.12 2022-09-13 19:26:38 +00:00
557d4ad3ca Merge pull request #147 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.12.11
2022-09-11 14:15:31 +06:00
renovate[bot]
f070fb804b Update dependency dev.inmo:micro_utils.ktor.server to v0.12.11 2022-09-08 22:34:27 +00:00
53de520c3a Merge pull request #135 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.12.10
2022-09-08 23:45:47 +06:00
f164ba901d Merge pull request #145 from InsanusMokrassar/renovate/ktor_version
Update dependency io.ktor:ktor-server-cio to v2.1.1
2022-09-08 23:45:28 +06:00
renovate[bot]
2a04b4979d Update dependency dev.inmo:micro_utils.ktor.server to v0.12.10 2022-09-08 17:45:25 +00:00
4a0279c89e Merge pull request #146 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v3.2.1
2022-09-08 23:45:08 +06:00
renovate[bot]
fcef9f1f64 Update telegram_bot_api_version to v3.2.1 2022-09-08 17:10:59 +00:00
renovate[bot]
33e191816a Update dependency io.ktor:ktor-server-cio to v2.1.1 2022-09-06 16:51:02 +00:00
e0b707ceba Update gradle.properties 2022-09-04 02:02:04 +06:00
115c76a9d4 Merge pull request #143 from InsanusMokrassar/3.2.0
3.2.0
2022-08-26 18:41:39 +06:00
0ebfb3c44a small fixes 2022-08-26 16:35:36 +06:00
9fa3690db6 add DeepLinkBot 2022-08-26 12:21:24 +06:00
a8c5d403c6 Merge pull request #141 from InsanusMokrassar/3.1.1
3.1.1
2022-08-15 09:41:20 +06:00
0c5186a37d add toggle closing confirmation button 2022-08-15 01:28:59 +06:00
7e9968ced9 add work with alerts in webapp 2022-08-15 01:12:39 +06:00
f1e8ed88a8 Merge pull request #139 from InsanusMokrassar/3.1.0
3.1.0
2022-08-13 16:58:34 +06:00
068dc79ac8 migration onto 3.1.0 and including new example 2022-08-13 14:39:24 +06:00
36273adfd1 Merge pull request #136 from InsanusMokrassar/3.0.0
3.0.0
2022-08-06 08:33:15 +06:00
bca2ae905b Update gradle-wrapper.properties 2022-08-06 08:19:13 +06:00
19a713a3e3 Update gradle.properties 2022-08-06 08:18:55 +06:00
e039f90961 updates according to 3.0.0 2022-08-05 19:45:52 +06:00
21692d16ca fixes in forward chat info bot 2022-08-04 22:32:36 +06:00
d547dce2ab update dependencies 2022-08-04 21:41:55 +06:00
9bfe88a79c Merge pull request #134 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v2.2.2
2022-07-31 21:48:05 +06:00
renovate[bot]
bfa327acf3 Update telegram_bot_api_version to v2.2.2 2022-07-31 15:35:38 +00:00
88a031b05a Update gradle.properties 2022-07-22 20:26:06 +06:00
20e942b2ac Merge pull request #133 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.11.13
2022-07-22 20:14:02 +06:00
renovate[bot]
3e20cbd22c Update dependency dev.inmo:micro_utils.ktor.server to v0.11.13 2022-07-22 14:10:56 +00:00
bcd35de038 Merge pull request #131 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v2.2.0
2022-07-11 12:05:16 +06:00
renovate[bot]
2aec45d453 Update telegram_bot_api_version to v2.2.0 2022-07-11 06:03:16 +00:00
ee55378e7a Update gradle.properties 2022-07-10 00:34:53 +06:00
b402c7b6e7 Merge pull request #130 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.11.12
2022-07-02 12:07:54 +06:00
renovate[bot]
32b7c7b9a4 Update dependency dev.inmo:micro_utils.ktor.server to v0.11.12 2022-07-02 06:02:52 +00:00
0b2d6b20de Merge pull request #129 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.11.10
2022-06-30 13:50:34 +06:00
renovate[bot]
182ee7a865 Update dependency dev.inmo:micro_utils.ktor.server to v0.11.10 2022-06-30 05:26:51 +00:00
0652a95d11 Merge pull request #128 from InsanusMokrassar/renovate/telegram_bot_api_version
Update telegram_bot_api_version to v2.1.2
2022-06-29 18:41:36 +06:00
renovate[bot]
f6066c60c0 Update telegram_bot_api_version to v2.1.2 2022-06-29 12:41:23 +00:00
2fa340292c Merge pull request #127 from InsanusMokrassar/renovate/ktor_version
Update dependency io.ktor:ktor-server-cio to v2.0.3
2022-06-29 18:41:04 +06:00
7d94007905 Merge pull request #126 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.11.9
2022-06-29 18:40:53 +06:00
renovate[bot]
9638174d48 Update dependency dev.inmo:micro_utils.ktor.server to v0.11.9 2022-06-29 11:53:40 +00:00
renovate[bot]
18bacaea2e Update dependency io.ktor:ktor-server-cio to v2.0.3 2022-06-28 16:35:40 +00:00
0f2b3760dd Merge pull request #125 from InsanusMokrassar/2.1.1
2.1.1
2022-06-26 15:24:30 +06:00
730923f55c actualization 2022-06-26 13:25:26 +06:00
9cf8bd9f28 migration onto 2.1.1 2022-06-26 13:03:52 +06:00
370fa45dba Merge pull request #124 from InsanusMokrassar/renovate/micro_utils_version
Update dependency dev.inmo:micro_utils.ktor.server to v0.11.6
2022-06-24 08:27:13 +06:00
renovate[bot]
72e7a73e40 Update dependency dev.inmo:micro_utils.ktor.server to v0.11.6 2022-06-23 23:54:36 +00:00
10dd9bd851 Merge pull request #123 from InsanusMokrassar/2.1.0
2.1.0
2022-06-22 00:43:36 +06:00
159 changed files with 11760 additions and 356 deletions

View File

@@ -8,9 +8,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Install dependencies
run: |
sudo apt update
sudo apt install -y libcurl4-openssl-dev
- name: Set up JDK 17
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 17
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew build --no-daemon

4
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.kotlin
out/* out/*
*.iml *.iml
target target
@@ -10,3 +11,6 @@ build/
out/ out/
kotlin-js-store/ kotlin-js-store/
local.*
local.*/

2
.template/bot/.env Normal file
View File

@@ -0,0 +1,2 @@
title=$prompt
subtitle=Subtitle of {{$title}}

View File

@@ -0,0 +1,9 @@
# {{$title}}
## Launch
```bash
../gradlew run --args="BOT_TOKEN"
```

View File

@@ -0,0 +1,22 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="{{$title}}Kt"
{{$subtitle}}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,34 @@
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.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/**
* This place can be the playground for your code.
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
val isDebug = args.any { it == "debug" }
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
// start here!!
val me = getMe()
println(me)
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
}.second.join()
}

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env kotlin
/**
* Generates files and folders as they have been put in the folder. Envs uses common syntax, but
* values may contains {{${'$'}sampleVariable}} parts, where {{${'$'}sampleVariable}} will be replaced with variable value.
* Example:
*
* .env:
* sampleVariable=${'$'}prompt # require request from command line
* sampleVariable2=just some value
* sampleVariable3=${'$'}{sampleVariable}.${'$'}{sampleVariable2}
*
* Result variables:
* sampleVariable=your input in console # lets imagine you typed it
* sampleVariable2=just some value
* sampleVariable3=your input in console.just some value
*
* To use these variables in template, you will need to write {{${'$'}sampleVariable}}.
* You may use it in text of files as well as in files/folders names.
*
* Usage: kotlin generator.kts [args] folders...
* Args:
* -e, --env: Path to file with args for generation; Use "${'$'}prompt" as values to read variable value from console
* -o, --outputFolder: Folder where templates should be used. Folder of calling by default
* folders: Folders-templates
*/
import java.io.File
val console = System.console()
fun String.replaceWithVariables(envs: Map<String, String>): String {
var currentString = this
var changed = false
do {
changed = false
envs.forEach { (k, v) ->
val previousString = currentString
currentString = currentString.replace("{{$${k}}}", v)
changed = changed || currentString != previousString
}
} while (changed)
return currentString
}
fun requestVariable(variableName: String, defaultValue: String?): String {
console.printf("Enter value for variable $variableName${defaultValue ?.let { " [$it]" } ?: ""}: ")
return console.readLine().ifEmpty { defaultValue } ?: ""
}
fun readEnvs(content: String, presets: Map<String, String>): Map<String, String> {
val initialEnvs = mutableMapOf<String, String>()
content.split("\n").forEach {
val withoutComment = it.replace(Regex("\\#.*"), "")
runCatching {
val (key, value) = withoutComment.split("=")
val existsValue = presets[key]
if (value == "\$prompt") {
initialEnvs[key] = requestVariable(key, existsValue)
} else {
initialEnvs[key] = requestVariable(key, value.replaceWithVariables(initialEnvs))
}
}
}
var i = 0
val readEnvs = initialEnvs.toMutableMap()
while (i < readEnvs.size) {
val key = readEnvs.keys.elementAt(i)
val currentValue = readEnvs.getValue(key)
val withReplaced = currentValue.replaceWithVariables(readEnvs)
var changed = false
if (withReplaced != currentValue) {
i = 0
readEnvs[key] = withReplaced
} else {
i++
}
}
return presets + readEnvs
}
var envFile: File? = null
var outputFolder: File = File("./") // current folder by default
val templatesFolders = mutableListOf<File>()
var extensions: List<String>? = null
fun readParameters() {
var i = 0
while (i < args.size) {
val arg = args[i]
when (arg) {
"--env",
"-e" -> {
i++
envFile = File(args[i])
}
"--extensions",
"-ex" -> {
i++
extensions = args[i].split(",")
}
"--outputFolder",
"-o" -> {
i++
outputFolder = File(args[i])
}
"--help",
"-h" -> {
println("""
Generates files and folders as the have been put in the folder. Envs uses common syntax, but
values may contains {{${'$'}sampleVariable}} parts, where {{${'$'}sampleVariable}} will be replaced with variable value.
Example:
.env:
sampleVariable=${'$'}prompt # require request from command line
sampleVariable2=just some value
sampleVariable3=${'$'}{sampleVariable}.${'$'}{sampleVariable2}
Result variables:
sampleVariable=your input in console # lets imagine you typed it
sampleVariable2=just some value
sampleVariable3=your input in console.just some value
To use these variables in template, you will need to write {{${'$'}sampleVariable}}.
You may use it in text of files as well as in files/folders names.
Usage: kotlin generator.kts [args] folders...
Args:
-e, --env: Path to file with args for generation; Use "${'$'}prompt" as values to read variable value from console
-o, --outputFolder: Folder where templates should be used. Folder of calling by default
folders: Folders-templates
""".trimIndent())
Runtime.getRuntime().exit(0)
}
else -> {
val potentialFile = File(arg)
println("Potential file/folder as template: ${potentialFile.absolutePath}")
runCatching {
if (potentialFile.exists()) {
println("Adding file/folder as template: ${potentialFile.absolutePath}")
templatesFolders.add(potentialFile)
}
}.onFailure { e ->
println("Unable to use folder $arg as template folder")
e.printStackTrace()
}
}
}
i++
}
}
readParameters()
val envs: MutableMap<String, String> = envFile ?.let { readEnvs(it.readText(), emptyMap()) } ?.toMutableMap() ?: mutableMapOf()
println(
"""
Result environments:
${envs.toList().joinToString("\n ") { (k, v) -> "$k=$v" }}
Result extensions:
${extensions ?.joinToString()}
Input folders:
${templatesFolders.joinToString("\n ") { it.absolutePath }}
Output folder:
${outputFolder.absolutePath}
""".trimIndent()
)
fun File.handleTemplate(targetFolder: File, envs: Map<String, String>) {
println("Handling $absolutePath")
val localEnvs = File(absolutePath, ".env").takeIf { it.exists() } ?.let {
println("Reading .env in ${absolutePath}")
readEnvs(it.readText(), envs)
} ?: envs
println(
"""
Local environments:
${localEnvs.toList().joinToString("\n ") { (k, v) -> "$k=$v" }}
""".trimIndent()
)
val newName = name.replaceWithVariables(localEnvs)
println("New name $newName")
when {
!exists() -> return
isFile -> {
val content = useLines {
it.map { it.replaceWithVariables(localEnvs) }.toList()
}.joinToString("\n")
val targetFile = File(targetFolder, newName)
targetFile.writeText(content)
println("Target file: ${targetFile.absolutePath}")
}
else -> {
val folder = File(targetFolder, newName)
println("Target folder: ${folder.absolutePath}")
folder.mkdirs()
listFiles() ?.forEach { fileOrFolder ->
fileOrFolder.handleTemplate(folder, localEnvs)
}
}
}
}
templatesFolders.forEach { folderOrFile ->
folderOrFile.handleTemplate(outputFolder, envs)
}

36
BoostsInfoBot/README.md Normal file
View File

@@ -0,0 +1,36 @@
# 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.
## Behavior
1. Open a private chat with the bot and send `/start` (the command takes no arguments).
2. The bot replies with a **Click me :)** keyboard button. Pressing it opens Telegram's channel picker. The picker is restricted to channels where the bot is already a member.
3. After a channel is selected, Telegram sends its identifier to the bot in a `chat_shared` service message. The bot accepts only the response associated with its channel-request button (request ID `1`).
4. The bot calls `getUserChatBoosts` for the selected channel and the user who selected it. It replies with each boost's added and expiration dates plus the unformatted boost object.
If that user has no boosts in the channel, the bot says so. If Telegram rejects the request or another error occurs while obtaining the boosts, it replies with `Unable to take info about boosts in shared chat`.
Separately, every `chat_boost` update received while the bot is running is printed as an unformatted object to standard output. These updates represent boosts that were added or changed; removed-boost updates are not handled by this example. This console output is produced whether or not debug logging is enabled.
## Telegram setup and permissions
- Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
- Use `/start` in a private chat. Telegram's request-chat keyboard buttons are available only in private chats.
- Before selecting a channel, add the bot to it and promote it to administrator. The button requires the bot to be a member, but it does not request administrator rights. Telegram requires administrator rights both for `getUserChatBoosts` and for receiving `chat_boost` updates.
- The query returns only boosts added by the user interacting with the bot, not every boost on the selected channel.
- The example uses long polling and automatically deletes any existing webhook for the bot token at startup. Do not run another long-polling consumer for the same token at the same time.
## Launch
From the repository root, pass the bot token as the first application argument:
```bash
./gradlew :BoostsInfoBot:run --args="<BOT_TOKEN>"
```
An optional second argument, exactly `debug`, routes the library's default KSLog output to standard output:
```bash
./gradlew :BoostsInfoBot:run --args="<BOT_TOKEN> debug"
```

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="BoostsInfoKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,83 @@
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.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.get.getUserChatBoosts
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatBoostUpdated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatShared
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatReplyKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.requestChannelButton
import dev.inmo.tgbotapi.types.request.RequestId
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"
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
val requestChatId = RequestId(1)
val bot = telegramBot(args.first())
bot.buildBehaviourWithLongPolling (defaultExceptionsHandler = { it.printStackTrace() }) {
onChatBoostUpdated {
println(it)
}
onCommand("start") {
reply(
it,
replyMarkup = flatReplyKeyboard {
requestChannelButton(
"Click me :)",
requestChatId,
botIsMember = true
)
}
) {
regular("Select chat to get know about your boosts")
}
}
onChatShared(initialFilter = { it.chatEvent.requestId == requestChatId }) {
val boostsInfoContrainer = runCatching {
getUserChatBoosts(it.chatEvent.chatId, it.chat.id)
}.getOrNull()
reply(it) {
when {
boostsInfoContrainer == null -> +"Unable to take info about boosts in shared chat"
boostsInfoContrainer.boosts.isEmpty() -> +"There is no any boosts in passed chat"
else -> {
boostsInfoContrainer.boosts.forEach {
regular("Boost added: ${DateFormat.FORMAT1.format(it.addDate.asDate)}; Boost expire: ${DateFormat.FORMAT1.format(it.expirationDate.asDate)}; Unformatted: $it") + "\n"
}
}
}
}
}
}.join()
}

View File

@@ -0,0 +1,77 @@
# BotSubscriptionsBot
Demonstrates the [`subscription`](https://core.telegram.org/bots/api#update) update added in Telegram Bot API
10.2. Telegram sends a [`BotSubscriptionUpdated`](https://core.telegram.org/bots/api#botsubscriptionupdated) when a
user cancels a recurring payment subscription to the bot, re-enables a canceled subscription, or a subscription
payment fails.
This example only observes subscription changes. It does not create an invoice or start a subscription.
## Behavior
At startup, the bot calls `getMe` and prints its own information. It then receives updates through long polling and
prints every received update object to standard output.
For each subscription update, the bot demonstrates three tgbotapi interfaces:
- `onBotSubscriptionUpdated` handles `BotSubscriptionUpdated` directly. It prints the user ID, invoice payload, and
typed state, then makes a best-effort attempt to notify that user in a private chat. A send failure is logged and
does not stop polling.
- `botSubscriptionUpdatedUpdatesFlow` exposes the underlying `BotSubscriptionUpdatedUpdate`; this example prints its
update ID, user ID, and state. Consequently, the same event appears in the typed-handler, subscription-flow, and
generic all-update logs.
- `waitBotSubscriptionUpdated().first()` waits for one matching event in the `/wait_subscription` command handler.
The known tgbotapi states are `Active`, `Canceled`, and `Failed`. Unknown state strings are preserved as `Unknown`, so
the example remains compatible if Telegram adds another state.
## Command
- `/wait_subscription` — a standalone command with no arguments. It replies that it is waiting, then waits without a
timeout for the next subscription update and replies in the command's chat with that update's state and invoice
payload. The update is not restricted to the command sender, so this unprotected diagnostic command should not be
copied into a production bot as-is.
There is no `/start` handler and the bot ignores other commands apart from printing their received update objects.
## Setup
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
2. Use a complete payment implementation for that same bot to create a recurring Telegram Stars (`XTR`) invoice link
with [`createInvoiceLink`](https://core.telegram.org/bots/api#createinvoicelink) and a `subscription_period`
(currently 2,592,000 seconds, or 30 days), then let a user subscribe. This example has neither an invoice creator nor
a `pre_checkout_query` handler, so it cannot establish a new subscription by itself; it is intended to observe state
changes for subscriptions created through that payment flow.
3. Have each subscriber start the bot and leave its private chat unblocked if you want the direct status notification
to succeed.
No group or channel membership and no administrator permissions are required for bot payment subscriptions. If you
run `/wait_subscription` in a group, the bot only needs to receive the command and be allowed to send its replies.
These events concern recurring payments toward the bot. They are different from paid channel subscription invite
links.
## Arguments
The bot token is required and must be the first argument. The remaining optional flags are exact, case-sensitive
strings and can be supplied in either order:
| Argument | Effect |
| --- | --- |
| `BOT_TOKEN` | Token of the bot to run. |
| `debug` | Sends tgbotapi/KSLog diagnostic output to standard output. |
| `testServer` | Uses Telegram's Bot API test environment (`/test`) instead of the production environment. |
## Launch
From the repository root:
```bash
./gradlew :BotSubscriptionsBot:run --args="BOT_TOKEN"
```
For example, to enable both optional modes:
```bash
./gradlew :BotSubscriptionsBot:run --args="BOT_TOKEN debug testServer"
```

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="BotSubscriptionsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,100 @@
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitBotSubscriptionUpdated
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBotSubscriptionUpdated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.types.payments.BotSubscriptionUpdated
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
/**
* Runs a long-polling demonstration of bot payment-subscription updates introduced in Telegram Bot API 10.2.
*
* Telegram sends a `subscription` update carrying a [BotSubscriptionUpdated] when a user cancels a recurring
* payment subscription to the bot, re-enables a canceled subscription, or a subscription payment fails. This
* example consumes those updates; it does not create recurring invoices.
*
* Key concepts demonstrated:
* - [onBotSubscriptionUpdated] — trigger whose handler receives a [BotSubscriptionUpdated] (`user`,
* `invoicePayload`, `state`) and makes a best-effort status notification to the subscriber
* - [BotSubscriptionUpdated.State] — the typed sealed state: [BotSubscriptionUpdated.State.Active],
* [BotSubscriptionUpdated.State.Canceled], [BotSubscriptionUpdated.State.Failed] (data objects) and the
* [BotSubscriptionUpdated.State.Unknown] value-class fallback for any future state
* - `botSubscriptionUpdatedUpdatesFlow` — the raw update flow of
* [dev.inmo.tgbotapi.types.update.BotSubscriptionUpdatedUpdate] (available directly because a
* BehaviourContext is a `FlowsUpdatesFilter`); each emission's payload is its `data`
* - [waitBotSubscriptionUpdated] — expectation returning a flow of [BotSubscriptionUpdated]; the
* `/wait_subscription` handler takes its next value without a timeout and replies in the command's chat
*
* The first command-line argument is always treated as the bot token. Later arguments equal to `debug` and
* `testServer` enable console diagnostic logging and Telegram's Bot API test environment, respectively.
*
* @param args bot token followed by optional, case-sensitive `debug` and `testServer` flags
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
val isDebug = args.any { it == "debug" }
val isTestServer = args.any { it == "testServer" }
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
telegramBotWithBehaviourAndLongPolling(
botToken,
CoroutineScope(Dispatchers.IO),
testServer = isTestServer,
) {
val me = getMe()
println("Bot info: $me")
// subscription update: react to the typed BotSubscriptionUpdated.State
onBotSubscriptionUpdated { update ->
val user = update.user
val payload = update.invoicePayload
val stateText = when (val state = update.state) {
BotSubscriptionUpdated.State.Active -> "active ✅"
BotSubscriptionUpdated.State.Canceled -> "canceled ❌"
BotSubscriptionUpdated.State.Failed -> "payment failed ⚠️"
// Unknown is a value class carrying the raw state name — future-proof fallback
is BotSubscriptionUpdated.State.Unknown -> "unknown (${state.name})"
}
println("Subscription update from ${user.id}: payload=$payload, state=${update.state.name}")
// notify the subscriber (only works if they have an open chat with the bot)
runCatchingLogging {
send(user.id, "Your subscription (payload: $payload) is now: $stateText")
}
}
// Raw flow variant of the same updates. BehaviourContext : FlowsUpdatesFilter, so the flow is
// available directly; each emission is a BotSubscriptionUpdatedUpdate whose payload is `.data`.
botSubscriptionUpdatedUpdatesFlow.subscribeSafelyWithoutExceptions(this) { update ->
println("[flow] update ${update.updateId}: user=${update.data.user.id}, state=${update.data.state.name}")
}
// waitBotSubscriptionUpdated expectation: suspend until the next subscription update
onCommand("wait_subscription") {
reply(it, "Waiting for the next subscription update...")
val update = waitBotSubscriptionUpdated().first()
reply(it, "Subscription update: state=${update.state.name}, payload=${update.invoicePayload}")
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it)
}
}.second.join()
}

View File

@@ -0,0 +1,77 @@
# Business Connections Bot
This example demonstrates how a bot can manage a connected Telegram Business account. It handles business-connection updates, mirrors business messages, exposes inline actions for marking messages as read or deleting them, and exercises account, Stars, gifts, stories, and checklist APIs.
This is a feature demonstration, not a production-ready bot. Several commands change the connected account or transfer its Stars, and the bot keeps connection IDs only in memory.
## Telegram setup and rights
1. Create a bot with [@BotFather](https://t.me/BotFather) and obtain its token.
2. Enable Business/Secretary Mode for the bot in BotFather. Telegram's name for the setting can vary by client version.
3. Start this example, then connect the bot to a Telegram Business account and allow it to manage the desired private chats. The account owner should also open a private chat with the bot; all commands below are accepted only there.
4. Grant the business rights needed by the features you want to try:
| Business right | Used by |
| --- | --- |
| Reply/send messages (`can_reply`) | Mirroring and replying to business messages and resending checklists |
| Read messages (`can_read_messages`) | **Read message** inline button |
| Delete all messages (`can_delete_all_messages`) | **Delete message** for incoming customer messages |
| Delete sent messages (`can_delete_sent_messages`) | Deleting messages sent by the bot itself |
| Edit name (`can_edit_name`) | `/set_business_account_name` |
| Edit username (`can_edit_username`) | `/set_business_account_username` |
| Edit bio (`can_edit_bio`) | `/set_business_account_bio` |
| Edit profile photo (`can_edit_profile_photo`) | Both profile-photo commands |
| View gifts and Stars (`can_view_gifts_and_stars`) | Balance and gift-list commands |
| Transfer Stars (`can_transfer_stars`) | `/transfer_business_account_stars` |
| Manage stories (`can_manage_stories`) | `/post_story` and `/delete_story` |
The account-management methods used here do not require the connected account to have Telegram Premium as of Bot API 9.0. Sending checklists still depends on the account and client being able to create them. Enable Bot-to-Bot Communication Mode in BotFather if you want to exercise the special reply path for a bot that contacts the managed business account.
See Telegram's [business-bot overview](https://core.telegram.org/bots/features#business-mode) and [`BusinessBotRights`](https://core.telegram.org/bots/api#businessbotrights) for the platform rules. The Bot API generally restricts business replies and reads to private chats active in the last 24 hours.
## Launch
From the repository root, pass the token as the first application argument:
```bash
./gradlew :BusinessConnectionsBot:run --args="<BOT_TOKEN>"
```
Pass the literal `debug` as the optional second argument to print verbose library logs:
```bash
./gradlew :BusinessConnectionsBot:run --args="<BOT_TOKEN> debug"
```
The bot prints its own `getMe` result, discards updates accumulated before startup, and then starts long polling. Because business connection IDs are cached only in memory, run the bot before creating/enabling the connection. After a restart, disable and re-enable the connection if owner commands do not respond.
## Automatic business update handling
- When a business connection is enabled or disabled, the bot records/removes its IDs and notifies the account owner in their private chat.
- A text business message starting with `/pin` or `/unpin` pins or unpins the accessible message it replies to.
- Other new business messages are resent to the sender's chat and receive a short diagnostic reply. Incoming customer messages also produce a notification in the business owner's bot chat with **Read message** and **Delete message** buttons.
- When the sender is another bot, the example sends a bot-to-bot diagnostic reply and skips the owner notification.
- Edited business messages are resent with an edit diagnostic. Deleted-business-message updates are reported to the account owner with the affected chat and message IDs.
- A received checklist is resent to the same chat on behalf of the business account when the connection ID can be resolved.
The inline **Read message** button calls `readBusinessMessage`. **Delete message** calls `deleteBusinessMessages`; for an incoming customer message, this requires the right to delete all managed-chat messages.
## Private-chat commands
These commands must be sent by the connected account owner in their private chat with the bot. Except for `/get_business_account_info`, handlers silently stop when that chat is not associated with an in-memory business connection.
| Command | Behavior |
| --- | --- |
| `/get_business_account_info` | Prints the current `BusinessConnection` as formatted JSON, or reports that no connection is known. |
| `/set_business_account_name <first_name> [last_name]` | Changes the connected account's first name and optional last name. Each name is parsed as one whitespace-separated argument. |
| `/set_business_account_username <username>` | Changes the account username to the single supplied argument. |
| `/get_business_account_star_balance` | Prints the account's current Telegram Stars balance. |
| `/transfer_business_account_stars <count>` | Transfers an integer number of Stars from the business account to the bot. Telegram accepts values from 1 through 10,000; this example leaves range validation to Telegram. |
| `/get_business_account_gifts` | Fetches every page of owned gifts and prints their Kotlin representations, splitting long output across messages. |
| `/set_business_account_bio <text>` | Saves the current bio, sets the complete text after the command as the new bio, waits 15 seconds, and attempts to restore the saved value. An empty text clears it temporarily. |
| `/set_business_account_profile_photo` | Reply to a photo with this command to set it as the main profile photo; after 15 seconds the bot removes that photo. |
| `/set_business_account_profile_photo_public` | Reply to a photo to set it as the public profile photo; after 15 seconds the bot removes it. |
| `/post_story` | Reply to a photo, video, or live photo to post it as a six-hour story with a fixed test caption and link area. |
| `/delete_story` | Reply to a story message to delete that story. Telegram only permits the bot to delete stories it posted for the business account. |
The profile-photo cleanup removes the newly current photo; it does not upload a saved copy of the previous photo. Telegram may promote the previous main photo after removal. Also note that the success text from `/post_story` currently mentions `/remove_story`; the implemented deletion command is `/delete_story`.

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="BusinessConnectionsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,532 @@
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.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
import dev.inmo.tgbotapi.extensions.api.business.deleteBusinessMessages
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGifts
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
import dev.inmo.tgbotapi.extensions.api.business.readBusinessMessage
import dev.inmo.tgbotapi.extensions.api.business.removeBusinessAccountProfilePhoto
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountBio
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountName
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountProfilePhoto
import dev.inmo.tgbotapi.extensions.api.business.setBusinessAccountUsername
import dev.inmo.tgbotapi.extensions.api.business.transferBusinessAccountStars
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage
import dev.inmo.tgbotapi.extensions.api.chat.modify.unpinChatMessage
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
import dev.inmo.tgbotapi.extensions.api.get.getBusinessConnection
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
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.extendedPrivateChatOrThrow
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.flushAccumulatedUpdates
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.requests.abstracts.multipartFile
import dev.inmo.tgbotapi.requests.business_connection.InputProfilePhoto
import dev.inmo.tgbotapi.requests.stories.PostStory
import dev.inmo.tgbotapi.types.ChatId
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.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
import dev.inmo.tgbotapi.utils.botCommand
import dev.inmo.tgbotapi.utils.code
import dev.inmo.tgbotapi.utils.extensions.splitForText
import dev.inmo.tgbotapi.utils.row
import korlibs.time.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
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"
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
val businessConnectionsChats = mutableMapOf<BusinessConnectionId, ChatId>()
val chatsBusinessConnections = mutableMapOf<ChatId, BusinessConnectionId>()
val businessConnectionsChatsMutex = Mutex()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
val me = getMe()
println(me)
flushAccumulatedUpdates()
onBusinessConnectionEnabled {
businessConnectionsChatsMutex.withLock {
businessConnectionsChats[it.id] = it.userChatId
chatsBusinessConnections[it.userChatId] = it.id
}
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been enabled")
}
onBusinessConnectionDisabled {
businessConnectionsChatsMutex.withLock {
businessConnectionsChats.remove(it.id)
chatsBusinessConnections.remove(it.userChatId)
}
send(it.userChatId, "Business connection ${it.businessConnectionId.string} has been disabled")
}
onContentMessage {
it.ifBusinessContentMessage { businessContentMessage ->
if (businessContentMessage.content.textContentOrNull() ?.text ?.startsWith("/pin") == true) {
businessContentMessage.replyTo ?.ifAccessibleMessage {
pinChatMessage(it)
return@ifBusinessContentMessage
}
}
if (businessContentMessage.content.textContentOrNull() ?.text ?.startsWith("/unpin") == true) {
businessContentMessage.replyTo ?.ifAccessibleMessage {
unpinChatMessage(it)
return@ifBusinessContentMessage
}
}
val sent = execute(it.content.createResend(businessContentMessage.from.id))
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",
)
send(
chatId = businessConnectionsChats[it.businessConnectionId] ?: return@ifBusinessContentMessage,
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
replyMarkup = inlineKeyboard {
row {
dataButton("Read message", "read ${it.chat.id.chatId.long} ${it.messageId.long}")
dataButton("Delete message", "delete ${it.chat.id.chatId.long} ${it.messageId.long}")
}
}
)
}
}
}
onEditedContentMessage {
it.ifBusinessContentMessage { businessContentMessage ->
val sent = execute(businessContentMessage.content.createResend(businessContentMessage.from.id))
if (businessContentMessage.sentByBusinessConnectionOwner) {
reply(sent, "You have edited this message in the ${businessContentMessage.businessConnectionId.string} related chat")
} else {
reply(sent, "User have edited this message to you in the ${businessContentMessage.businessConnectionId.string} related chat")
}
}
}
onBusinessMessagesDeleted {
var businessConnectionOwnerChat = businessConnectionsChatsMutex.withLock {
businessConnectionsChats[it.businessConnectionId]
}
if (businessConnectionOwnerChat == null) {
val businessConnection = getBusinessConnection(it.businessConnectionId)
businessConnectionsChatsMutex.withLock {
businessConnectionsChats[businessConnection.businessConnectionId] = businessConnection.userChatId
}
businessConnectionOwnerChat = businessConnection.userChatId
}
send(businessConnectionOwnerChat, "There are several removed messages in chat ${it.chat.id}: ${it.messageIds}")
}
onCommand("get_business_account_info", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id]
val businessConnectionInfo = businessConnectionId ?.let { getBusinessConnection(it) }
reply(it) {
if (businessConnectionInfo == null) {
+"There is no business connection for current chat"
} else {
+(Json { prettyPrint = true; encodeDefaults = true }.encodeToString(businessConnectionInfo))
}
}
}
onMessageDataCallbackQuery(Regex("read \\d+ \\d+")) {
val (_, chatIdString, messageIdString) = it.data.split(" ")
val chatId = chatIdString.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: return@onMessageDataCallbackQuery
val messageId = messageIdString.toLongOrNull() ?.let(::MessageId) ?: return@onMessageDataCallbackQuery
val businessConnectionId = chatsBusinessConnections[it.message.chat.id]
val readResponse = businessConnectionId ?.let { readBusinessMessage(it, chatId, messageId) }
answer(
it,
if (readResponse == null) {
"There is no business connection for current chat"
} else {
"Message has been read"
}
)
}
onMessageDataCallbackQuery(Regex("delete \\d+ \\d+")) {
val (_, chatIdString, messageIdString) = it.data.split(" ")
val chatId = chatIdString.toLongOrNull() ?.let(::RawChatId) ?.let(::ChatId) ?: return@onMessageDataCallbackQuery
val messageId = messageIdString.toLongOrNull() ?.let(::MessageId) ?: return@onMessageDataCallbackQuery
val businessConnectionId = chatsBusinessConnections[it.message.chat.id]
val readResponse = businessConnectionId ?.let { deleteBusinessMessages(it, listOf(messageId)) }
answer(
it,
if (readResponse == null) {
"There is no business connection for current chat"
} else {
"Message has been deleted"
}
)
}
// 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)
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
val set = runCatching {
setBusinessAccountName(
businessConnectionId,
firstName,
secondName
)
}.map {
true
}.getOrElse { false }
reply(it) {
if (set) {
+"Account name has been set"
} else {
+"Account name has not been set"
}
}
}
onCommandWithArgs("set_business_account_username", initialFilter = { it.chat is PrivateChat }) { it, args ->
val username = args[0]
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
val set = runCatching {
setBusinessAccountUsername(
businessConnectionId,
username
)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (set) {
+"Account username has been set"
} else {
+"Account username has not been set"
}
}
}
onCommand("get_business_account_star_balance", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val starAmount = runCatching {
getBusinessAccountStarBalance(businessConnectionId)
}.getOrElse {
it.printStackTrace()
null
}
reply(it) {
if (starAmount != null) {
+"Account stars amount: $starAmount"
} else {
+"Account stars amount has not been got"
}
}
}
onCommandWithArgs("transfer_business_account_stars", initialFilter = { it.chat is PrivateChat }) { it, args ->
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommandWithArgs
val count = args.firstOrNull() ?.toIntOrNull() ?: reply(it) {
"Pass amount of stars to transfer to bot with command"
}.let {
return@onCommandWithArgs
}
val transferred = runCatching {
transferBusinessAccountStars(businessConnectionId, count)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (transferred) {
+"Stars have been transferred"
} else {
+"Stars have not been transferred"
}
}
}
onCommand("get_business_account_gifts", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val giftsFlow = runCatching {
getBusinessAccountGiftsFlow(businessConnectionId)
}.getOrElse {
it.printStackTrace()
null
}
if (giftsFlow == null) {
reply(it) {
+"Error in receiving of gifts"
}
} else {
giftsFlow.collect { giftsPage ->
giftsPage.gifts.joinToString {
it.toString()
}.splitForText().forEach { message ->
reply(it, message)
}
}
}
}
onCommand("set_business_account_bio", requireOnlyCommandInMessage = false, initialFilter = { it.chat is PrivateChat }) {
val initialBio = getChat(it.chat).extendedPrivateChatOrThrow().bio
val bio = it.content.text.removePrefix("/set_business_account_bio").trim()
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val set = runCatching {
setBusinessAccountBio(
businessConnectionId,
bio
)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (set) {
+"Account bio has been set. It will be reset within 15 seconds.\n\nInitial bio: " + code(initialBio)
} else {
+"Account bio has not been set"
}
}
delay(15.seconds)
val reset = runCatching {
setBusinessAccountBio(
businessConnectionId,
initialBio
)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (reset) {
+"Account bio has been reset"
} else {
+"Account bio has not been set. Set it manually: " + code(initialBio)
}
}
}
suspend fun handleSetProfilePhoto(it: ChatContentMessage<TextContent>, isPublic: Boolean) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<PhotoContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo for using of this command"
}
return@handleSetProfilePhoto
}
val set = runCatching {
val file = downloadFileToTemp(replyTo.content)
setBusinessAccountProfilePhoto(
businessConnectionId,
InputProfilePhoto.Static(
file.multipartFile()
),
isPublic = isPublic
)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (set) {
+"Account profile photo has been set. It will be reset within 15 seconds"
} else {
+"Account profile photo has not been set"
}
}
if (set == false) { return@handleSetProfilePhoto }
delay(15.seconds)
val reset = runCatching {
removeBusinessAccountProfilePhoto(
businessConnectionId,
isPublic = isPublic
)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (reset) {
+"Account profile photo has been reset"
} else {
+"Account profile photo has not been set. Set it manually"
}
}
}
onCommand("set_business_account_profile_photo", initialFilter = { it.chat is PrivateChat }) {
handleSetProfilePhoto(it, false)
}
onCommand("set_business_account_profile_photo_public", initialFilter = { it.chat is PrivateChat }) {
handleSetProfilePhoto(it, true)
}
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo or video for using of this command"
}
return@onCommand
}
val posted = runCatching {
val file = downloadFileToTemp(replyTo.content)
postStory(
businessConnectionId,
when (replyTo.content) {
is PhotoContent -> Photo(
file.multipartFile()
)
is VideoContent -> Video(
file.multipartFile()
)
is LivePhotoContent -> Video(
file.multipartFile(),
isAnimation = true
)
},
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
areas = listOf(
StoryArea(
StoryAreaPosition(
x = Percentage.of100(50.0),
y = Percentage.of100(50.0),
width = Percentage.of100(8.0),
height = Percentage.of100(8.0),
rotationAngle = 45.0,
cornerRadius = Percentage.of100(4.0),
),
StoryAreaType.Link(
"https://github.com/InsanusMokrassar/TelegramBotAPI-examples/blob/master/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt"
)
)
)
) {
+"It is test of postStory :)"
}
}.getOrElse {
it.printStackTrace()
null
}
reply(it) {
if (posted != null) {
+"Story has been posted. You may unpost it with " + botCommand("remove_story")
} else {
+"Story has not been posted"
}
}
}
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<StoryContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo or video for using of this command"
}
return@onCommand
}
val deleted = runCatching {
deleteStory(businessConnectionId, replyTo.content.story.id)
}.map {
true
}.getOrElse {
it.printStackTrace()
false
}
reply(it) {
if (deleted) {
+"Story has been deleted"
} else {
+"Story has not been deleted"
}
}
}
// Will work when some premium user sending to some other user checklist
onChecklistContent {
execute(
it.content.createResend(
it.chat.id,
businessConnectionId = it.chat.id.businessConnectionId ?: chatsBusinessConnections[it.chat.id] ?: return@onChecklistContent
)
)
}
}.second.join()
}

View File

@@ -0,0 +1,44 @@
# 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.
## Behavior
- 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.
## Arguments
| Position | Argument | Required | Description |
| --- | --- | --- | --- |
| 1 | `BOT_TOKEN` | Yes | Bot API token issued by BotFather. |
## Launch
From the repository root, run:
```bash
./gradlew :ChatAvatarSetter:run --args="<BOT_TOKEN>"
```
The process continues polling until it is stopped.

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="ChatAvatarSetterKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,42 @@
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.chat.modify.setChatPhoto
import dev.inmo.tgbotapi.extensions.api.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
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())
bot.buildBehaviourWithLongPolling(scope = CoroutineScope(Dispatchers.IO)) {
onPhoto {
val bytes = downloadFile(it.content)
runCatchingLogging {
setChatPhoto(
it.chat.id,
bytes.asMultipartFile("sample.jpg")
)
}.onSuccess { _ ->
reply(it, "Done")
}.onFailure { e ->
e.printStackTrace()
reply(it, "Something went wrong (see logs)")
}
}
}.join()
}

View File

@@ -0,0 +1,48 @@
# 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"
```

View File

@@ -0,0 +1,21 @@
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"
}

View File

@@ -0,0 +1,152 @@
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()
}

75
ChecklistsBot/README.md Normal file
View File

@@ -0,0 +1,75 @@
# 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.
## Behaviour
The bot uses long polling and installs these handlers:
| 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. |
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.
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.
## Telegram setup
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.
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.
## Run
From the repository root:
```bash
./gradlew :ChecklistsBot: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.

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="ChecklistsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,129 @@
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.subscribeLoggingDropExceptions
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
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.resend
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.api.suggested.approveSuggestedPost
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitSuggestedPostApproved
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitSuggestedPostDeclined
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChannelDirectMessagesConfigurationChanged
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChecklistContent
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChecklistTasksAdded
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChecklistTasksDone
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.onSuggestedPostApprovalFailed
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostApproved
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostDeclined
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostPaid
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onSuggestedPostRefunded
import dev.inmo.tgbotapi.extensions.utils.channelDirectMessagesContentMessageOrNull
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.content.ChecklistContent
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
import dev.inmo.tgbotapi.types.update.abstracts.Update
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.code
import dev.inmo.tgbotapi.utils.firstOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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()
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.Default),
testServer = isTestServer,
) {
val me = getMe()
println(me)
fun ChecklistContent.textBuilderTextSources(): TextSourcesList {
return buildEntities {
+checklist.textSources + "\n\n"
checklist.tasks.forEach { task ->
+""
code(
if (task.completionDate != null) {
"[x] "
} else {
"[ ] "
}
)
bold(task.textSources) + "\n"
}
}
}
onChecklistContent { messageWithContent ->
reply(messageWithContent) {
+messageWithContent.content.textBuilderTextSources()
}
}
onChecklistTasksDone { eventMessage ->
reply(
eventMessage,
checklistTaskId = eventMessage.chatEvent.markedAsDone ?.firstOrNull()
) {
eventMessage.chatEvent.checklistMessage.content.checklist
+eventMessage.chatEvent.checklistMessage.content.textBuilderTextSources()
}
}
onChecklistTasksAdded { messageWithContent ->
reply(
messageWithContent.chatEvent.checklistMessage,
checklistTaskId = messageWithContent.chatEvent.tasks.firstOrNull() ?.id
) {
+messageWithContent.chatEvent.checklistMessage.content.textBuilderTextSources()
}
}
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)
}
}.second.join()
}

53
CommunitiesBot/README.md Normal file
View File

@@ -0,0 +1,53 @@
# CommunitiesBot
This long-polling example demonstrates Communities support introduced in Telegram Bot API 10.2 and extended in 10.3: typed service events when a chat joins or leaves a community, an event when a user joins a chat from a community, and inspection of a chat's current community.
## 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_chat_joined` service message | `onCommunityChatJoined` logs the source community and replies with a welcome message. This means a user joined the current chat from a community; it is distinct from adding the chat itself to a community. |
| `/community` | Calls `getChat` and replies with the current community name/ID, or says the chat is not in a community. |
| `/wait_community_added` | Waits without a timeout for the next added event in the command's chat, then replies with the community name/ID. |
| `/wait_community_removed` | Waits without a timeout for the next removed event in the command's chat, then replies with the chat ID. |
| `/wait_community_joined` | Waits without a timeout for the next user-from-community join event in the command's chat, then replies with the source community name/ID. |
Commands use no positional arguments; other commands only appear in the generic update log. Each wait first sends a waiting reply, filters events with `sameChat`, and takes the first match.
## API concepts
- `CommunityChatAdded` carries a `Community` with a `CommunityId` and name; `CommunityChatRemoved` carries no fields.
- `CommunityChatJoined` carries the community through which a user joined the current chat.
- `onCommunityChatAdded`, `onCommunityChatRemoved`, and `onCommunityChatJoined` provide typed handlers for the service events.
- `getChat(...).community` exposes the nullable community on `ExtendedChat` without a subtype cast.
- `waitCommunityChatAddedEventsMessages`, `waitCommunityChatRemovedEventsMessages`, and `waitCommunityChatJoinedEventsMessages` 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"
```

View File

@@ -0,0 +1,21 @@
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"
}

View File

@@ -0,0 +1,121 @@
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.waitCommunityChatAddedEventsMessages
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatJoinedEventsMessages
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommunityChatRemovedEventsMessages
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatAdded
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatJoined
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommunityChatRemoved
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import kotlinx.coroutines.CoroutineScope
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, while [onCommunityChatJoined] reports a user joining
* the chat from a community. `/community` reads
* [dev.inmo.tgbotapi.types.chat.ExtendedChat.community] with [getChat]. The three wait commands use typed event-message
* expectations to take the first same-chat event without a timeout. The bot prints its [getMe] result and every
* received update.
*
* @param args the bot token followed by the optional, case-sensitive `debug` and `testServer` flags; unknown trailing
* arguments are ignored
* @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")
}
// community_chat_joined: a user joined this chat through a community
onCommunityChatJoined { message ->
val community = message.chatEvent.community
println("A user joined chat ${message.chat.id} from community '${community.name}' (id=${community.id.long})")
reply(message, "Welcome! You joined from the ${community.name} community.")
}
// Inspect the current chat's community on demand
onCommand("community") {
val community = getChat(it.chat.id).community
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 removed from a community...")
waitCommunityChatRemovedEventsMessages().filter { it.sameChat(origin) }.first()
reply(origin, "Chat removed from its community (${origin.chat.id})")
}
// Suspend until a user joins this chat from a community.
onCommand("wait_community_joined") { origin ->
reply(origin, "Waiting for somebody to join this chat from a community...")
val event = waitCommunityChatJoinedEventsMessages().filter { it.sameChat(origin) }.first().chatEvent
reply(origin, "A user joined from ${event.community.name} (id=${event.community.id.long})")
}
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
println(it)
}
}.second.join()
}

48
CustomBot/README.md Normal file
View File

@@ -0,0 +1,48 @@
# 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.
## Commands and updates
| 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. |
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.
## Run
Create a bot, obtain its token, and run this command from the repository root:
```bash
./gradlew :CustomBot: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.

21
CustomBot/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="CustomBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,142 @@
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.bot.getMyStarBalance
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.get.getUserProfileAudios
import dev.inmo.tgbotapi.extensions.api.send.media.sendPaidMedia
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.replyWithAudio
import dev.inmo.tgbotapi.extensions.api.send.replyWithPlaylist
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.onChannelDirectMessagesConfigurationChanged
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatOwnerChanged
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatOwnerLeft
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
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.update.abstracts.Update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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<*>
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
*/
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,
builder = {
includeMiddlewares {
addMiddleware {
doOnRequestReturnResult { result, request, _ ->
println("Result of $request:\n\n$result")
null
}
}
}
},
subcontextInitialAction = buildSubcontextInitialAction {
add {
data.update = it
}
}
) {
// start here!!
val me = getMe()
println(me)
onCommand("start") {
println(data.update)
println(data.commonMessage)
println(getChat(it.chat))
var currentOffset = 0
val pageSize = 2
do {
val userAudios = getUserProfileAudios(userId = it.chat.id, offset = currentOffset, limit = pageSize)
currentOffset += pageSize
println(userAudios)
when (userAudios.audios.size) {
1 -> {
replyWithAudio(
it,
userAudios.audios.first().fileId
)
}
0 -> {
// do nothing
}
else -> {
replyWithPlaylist(
it,
userAudios.audios.map {
it.toTelegramMediaAudio()
}
)
}
}
} while (currentOffset < userAudios.totalCount && userAudios.audios.isNotEmpty())
}
onCommand(
"additional_command",
additionalSubcontextInitialAction = { update, commonMessage ->
data.commonMessage = commonMessage
}
) {
println(data.update)
println(data.commonMessage)
}
onCommand("getMyStarBalance") {
reply(
to = it,
text = getMyStarBalance().toString()
)
}
onChannelDirectMessagesConfigurationChanged {
println(it.chatEvent)
}
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)
}
}.second.join()
}

37
DeepLinksBot/README.md Normal file
View File

@@ -0,0 +1,37 @@
# 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.
## Behavior
- 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.
Messages containing a bot command are excluded from link generation. The bot has
no persistence and runs until the process is stopped.
## Requirements
- 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.
## Run
From the repository root:
```bash
./gradlew :DeepLinksBot: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.

21
DeepLinksBot/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="DeepLinksBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,51 @@
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.expectations.waitDeepLinks
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.onDeepLink
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onText
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
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
telegramBotWithBehaviourAndLongPolling(botToken) {
val me = bot.getMe()
val username = me.username
println(me)
if (username == null) {
error("Unable to start bot work: it have no username")
}
onText(
initialFilter = { it.content.textSources.none { it is BotCommandTextSource } } // excluding messages with commands
) {
reply(it, makeTelegramDeepLink(username, it.content.text))
}
onCommand("start", requireOnlyCommandInMessage = true) { // handling of `start` without args
reply(it, "Hi :) Send me any text and I will try hard to create deeplink for you")
}
onDeepLink { (it, deepLink) ->
reply(it, "Ok, I got deep link \"${deepLink}\" in trigger")
}
waitDeepLinks().subscribeLoggingDropExceptions(this) { (it, deepLink) ->
reply(it, "Ok, I got deep link \"${deepLink}\" in waiter")
println(triggersHolder.handleableCommandsHolder.handleable)
}
}.second.join()
}

54
DraftsBot/README.md Normal file
View File

@@ -0,0 +1,54 @@
# 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 all examples. Bot API 10.3's stoppable generation controls
and `stopped_message_generation` updates are included.
## Commands
- `/test_draft_flow` publishes progressively longer 50-character prefixes every
500 ms, then sends the complete text as a normal message.
- `/test_empty_draft` first publishes an empty draft, waits 1.5 seconds, streams
the same prefixes, and then sends the complete text.
- `/test_stoppable_draft` continuously streams progressively longer revisions
with `canStop = true` and `keepOnStop = true`. Telegram displays a stop control;
stopping keeps the most recent draft revision and causes the flow helper to
return `false`. Before streaming, the handler subscribes to
`waitMessageGenerationStopped` and filters the expectation by chat and draft
ID, then logs the matched stop event. It deliberately sends no confirmation
message, because sending one would immediately remove the draft retained by
`keepOnStop`. If streaming ends because of another request failure and no
matching update arrives within five seconds, the bot logs that distinction
instead of waiting forever.
The bot advertises all three commands in Telegram's command menu for private
chats and filters each handler to private chats, as required by Telegram's draft
methods.
The typed `onMessageGenerationStopped` handler logs the chat, optional topic ID,
and draft ID from every generation-stopped update, independently of the scoped
expectation used by `/test_stoppable_draft`. Both apply to text and rich-message
drafts sent by this bot token.
## Setup
1. Obtain a bot token and keep it out of source control.
2. Start a private chat with the bot. Telegram doesn't accept a group or channel
ID for `sendMessageDraft`.
3. No administrator rights are required by this example.
The first program argument is required and must be the bot token. Omitting it
causes startup to fail; any later arguments are ignored.
## Run
From the repository root, run:
```bash
./gradlew :DraftsBot:run --args="<BOT_TOKEN>"
```
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.

21
DraftsBot/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="DraftsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,177 @@
import com.benasher44.uuid.uuid4
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.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.chat.forum.*
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlow
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageGenerationStopped
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageGenerationStopped
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicClosed
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicCreated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicEdited
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onForumTopicReopened
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGeneralForumTopicHidden
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGeneralForumTopicUnhidden
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPrivateForumTopicCreated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPrivateForumTopicEdited
import dev.inmo.tgbotapi.extensions.utils.forumChatOrNull
import dev.inmo.tgbotapi.extensions.utils.forumContentMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.privateChatOrNull
import dev.inmo.tgbotapi.extensions.utils.privateForumChatOrNull
import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.flushAccumulatedUpdates
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.ForumTopic
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.commands.BotCommandScope
import dev.inmo.tgbotapi.utils.DraftIdAllocator
import io.ktor.client.plugins.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withTimeoutOrNull
/** Sample text streamed as a draft and then sent as the completed message. */
const val testText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
private val stoppableDraftIds = DraftIdAllocator()
/**
* Starts DraftsBot with long polling and registers the draft demonstration commands.
*
* 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(),
CoroutineScope(Dispatchers.Default),
defaultExceptionsHandler = {
it.printStackTrace()
},
builder = {
client = client.config {
install(HttpTimeout) {
requestTimeoutMillis = 30000
socketTimeoutMillis = 30000
connectTimeoutMillis = 30000
}
}
}
) {
onCommand("test_draft_flow", initialFilter = { it.chat is PrivateChat }) {
sendMessageDraftFlowWithTexts(
it.chat.id,
flow<String> {
val step = 50
var currentLength = step
while (isActive && testText.length > currentLength) {
delay(500L)
emit(testText.take(currentLength))
currentLength += step
}
},
)
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", initialFilter = { it.chat is PrivateChat }) {
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)
}
// Bot API 10.3 lets the user stop generation. The expectation is subscribed before streaming starts so it
// cannot miss a fast stop update; matching both chat and draft ID also avoids consuming another draft's event.
onCommand("test_stoppable_draft", initialFilter = { it.chat is PrivateChat }) { origin ->
val draftId = stoppableDraftIds.allocate()
val stoppedUpdate = async(start = CoroutineStart.UNDISPATCHED) {
waitMessageGenerationStopped()
.filter { it.chat.id == origin.chat.id && it.draftId == draftId }
.first()
}
try {
val completed = sendMessageDraftFlowWithTexts(
origin.chat.id,
flow<String> {
val step = 20
var currentLength = step
while (isActive) {
delay(500L)
emit(testText.take(currentLength.coerceAtMost(testText.length)))
currentLength = (currentLength + step).coerceAtMost(testText.length)
}
},
draftId = draftId,
canStop = true,
keepOnStop = true,
)
if (!completed) {
val stopped = withTimeoutOrNull(5_000L) { stoppedUpdate.await() }
if (stopped == null) {
println("Draft streaming ended without a matching stopped_message_generation update")
} else {
// A normal message here would immediately remove the kept draft, so report only to stdout.
println(
"Expectation matched stopped draft ${stopped.draftId.long} in ${stopped.chat.id}; " +
"Telegram kept its last revision temporarily."
)
}
}
} finally {
stoppedUpdate.cancel()
stoppableDraftIds.free(draftId)
}
}
// The stopped_message_generation update includes the chat, optional topic and stopped draft ID.
onMessageGenerationStopped { stopped ->
println(
"Message generation stopped in ${stopped.chat.id}; " +
"thread=${stopped.messageThreadId}, draft=${stopped.draftId}"
)
}
setMyCommands(
BotCommand("test_draft_flow", "Start draft testing with flow"),
BotCommand("test_empty_draft", "Draft starting from empty text (TG Bot API 9.0)"),
BotCommand("test_stoppable_draft", "Stream a draft that the user can stop"),
scope = BotCommandScope.AllPrivateChats
)
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)
}
}.second.join()
}

View File

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

View File

@@ -0,0 +1,21 @@
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"
}

View File

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

View File

@@ -1,10 +1,44 @@
# FSM # FSMBot
This bot contains an example of working with FSM included in project FSMBot demonstrates the finite-state-machine (FSM) support provided by
[MicroUtils](https://github.com/InsanusMokrassar/MicroUtils) [MicroUtils](https://github.com/InsanusMokrassar/MicroUtils) and TelegramBotAPI's
behaviour builder.
## Launch ## Behaviour
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.
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.
## Commands
- `/start` — start or restart the content-resending chain.
- `/stop` — stop the active chain while the bot is waiting for content.
The bot does not register its command menu automatically; commands can be typed
directly or configured separately with BotFather.
## Requirements and permissions
- 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.
## Run
From the repository root:
```bash ```bash
../gradlew run --args="BOT_TOKEN" ./gradlew :FSMBot:run --args="<BOT_TOKEN>"
``` ```
`BOT_TOKEN` is the required first positional argument. The bot uses long polling;
no webhook or additional configuration is needed.

View File

@@ -1,21 +1,47 @@
import dev.inmo.micro_utils.coroutines.AccumulatorFlow import dev.inmo.micro_utils.coroutines.awaitFirst
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.micro_utils.fsm.common.State import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.tgbotapi.extensions.api.send.sendMessage import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitAnyContentMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitCommandMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndFSMAndStartLongPolling
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithParams import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.command
import dev.inmo.tgbotapi.extensions.utils.formatting.* import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.containsCommand
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithArgs
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.content.TextContent import dev.inmo.tgbotapi.types.message.content.TextContent
import kotlinx.coroutines.* import dev.inmo.tgbotapi.utils.botCommand
import dev.inmo.tgbotapi.utils.firstOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
/** State hierarchy for a chat-scoped content-resending conversation. */
sealed interface BotState : State sealed interface BotState : State
data class ExpectContentOrStopState(override val context: ChatId, val sourceMessage: CommonMessage<TextContent>) : BotState
data class StopState(override val context: ChatId) : BotState
/**
* 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 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>) { suspend fun main(args: Array<String>) {
val botToken = args.first() val botToken = args.first()
@@ -36,32 +62,58 @@ suspend fun main(args: Array<String>) {
} }
) { ) {
strictlyOn<ExpectContentOrStopState> { strictlyOn<ExpectContentOrStopState> {
sendMessage( send(
it.context, it.context,
buildEntities { ) {
+"Send me some content or " + botCommand("stop") + " if you want to stop sending" +"Send me some content or " + botCommand("stop") + " if you want to stop sending"
} }
)
val contentMessage = firstOf(
{
waitCommandMessage("stop").filter { message ->
message.sameThread(it.sourceMessage)
}.first()
null
},
{
waitAnyContentMessage().filter { message ->
message.sameThread(it.sourceMessage)
}.filter {
containsCommand(
"stop",
it.withContentOrNull<TextContent>() ?.content ?.textSources ?: return@filter false
) == false
}.first()
}
) ?: return@strictlyOn StopState(it.context)
val contentMessage = waitContentMessage().first()
val content = contentMessage.content val content = contentMessage.content
when { execute(content.createResend(it.context))
content is TextContent && content.parseCommandsWithParams().keys.contains("stop") -> StopState(it.context) it
else -> {
execute(content.createResend(it.context))
it
}
}
} }
strictlyOn<StopState> { strictlyOn<StopState> {
sendMessage(it.context, "You have stopped sending of content") send(it.context) { +"You have stopped sending of content" }
null null
} }
command("start") { command(
"start"
) {
startChain(ExpectContentOrStopState(it.chat.id, it)) startChain(ExpectContentOrStopState(it.chat.id, it))
} }
onContentMessage(
{
it.content.textContentOrNull() ?.text == "/start"
}
) {
startChain(ExpectContentOrStopState(it.chat.id, it.withContentOrNull() ?: return@onContentMessage))
}
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)
}
}.second.join() }.second.join()
} }

View File

@@ -1,9 +1,42 @@
# FilesLoaderBot # FilesLoaderBot
This bot will download incoming files 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.
## Launch ## Behavior
- `/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.
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.
## Setup
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.
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.
## Run
From the repository root:
```bash ```bash
../gradlew run --args="BOT_TOKEN[ optional/folder/path]" ./gradlew :FilesLoaderBot:run --args='<BOT_TOKEN>'
./gradlew :FilesLoaderBot:run --args='<BOT_TOKEN> /absolute/output/directory'
``` ```
Arguments:
1. `BOT_TOKEN` (required): the Telegram bot token.
2. `OUTPUT_DIRECTORY` (optional): the local destination directory; defaults to
`/tmp/`.

View File

@@ -1,29 +1,131 @@
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.files.downloadFile import dev.inmo.tgbotapi.extensions.api.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMedia import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMedia
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.*
import dev.inmo.tgbotapi.utils.filenameFromUrl import dev.inmo.tgbotapi.utils.filenameFromUrl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.io.File import java.io.File
/** /**
* This bot will download incoming files * 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/`.
*/ */
suspend fun main(args: Array<String>) { suspend fun main(args: Array<String>) {
val botToken = args.first() val botToken = args.first()
val directoryOrFile = args.getOrNull(1) ?.let { File(it) } ?: File("") val directoryOrFile = args.getOrNull(1) ?.let { File(it) } ?: File("/tmp/")
directoryOrFile.mkdirs() directoryOrFile.mkdirs()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) { telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
onMedia(initialFilter = null) { onCommand("start") {
val pathedFile = bot.getFileAdditionalInfo(it.content.media) reply(it, "Send me any media (like photo or video) to download it")
val outFile = File(directoryOrFile, pathedFile.filePath.filenameFromUrl)
bot.downloadFile(it.content.media, outFile)
reply(it, "Saved to ${outFile.absolutePath}")
} }
onContentMessage { println(it) } onMedia(initialFilter = null) {
val content = it.content
val pathedFile = bot.getFileAdditionalInfo(content.media)
val outFile = File(directoryOrFile, pathedFile.filePath.filenameFromUrl)
withTypingAction(it.chat.id) {
runCatching {
bot.downloadFile(content.media, outFile)
}.onFailure {
it.printStackTrace()
}.onSuccess { _ ->
reply(it, "Saved to ${outFile.absolutePath}")
}
}.onSuccess { _ ->
val action = when (content) {
is PhotoContent -> UploadPhotoAction
is AnimationContent,
is LivePhotoContent,
is VideoContent -> UploadVideoAction
is StickerContent -> ChooseStickerAction
is MediaGroupContent<*> -> UploadPhotoAction
is DocumentContent -> UploadDocumentAction
is VoiceContent,
is AudioContent -> RecordVoiceAction
is VideoNoteContent -> UploadVideoNoteAction
}
withAction(it.chat.id, action) {
when (content) {
is PhotoContent -> replyWithPhoto(
it,
outFile.asMultipartFile()
)
is AnimationContent -> replyWithAnimation(
it,
outFile.asMultipartFile()
)
is VideoContent -> replyWithVideo(
it,
outFile.asMultipartFile()
)
is StickerContent -> replyWithSticker(
it,
outFile.asMultipartFile()
)
is MediaGroupContent<*> -> replyWithMediaGroup(
it,
content.group.mapNotNull {
when (val innerContent = it.content) {
is AudioContent -> TelegramMediaAudio(
downloadFileToTemp(innerContent.media).asMultipartFile()
)
is DocumentContent -> TelegramMediaDocument(
downloadFileToTemp(innerContent.media).asMultipartFile()
)
is PhotoContent -> TelegramMediaPhoto(
downloadFileToTemp(innerContent.media).asMultipartFile()
)
is VideoContent -> TelegramMediaVideo(
downloadFileToTemp(innerContent.media).asMultipartFile()
)
is LivePhotoContent -> TelegramMediaLivePhoto(
downloadFileToTemp(innerContent.media).asMultipartFile(),
innerContent.media.photo ?.fileId ?: return@mapNotNull null
)
}
}
)
is AudioContent -> replyWithAudio(
it,
outFile.asMultipartFile()
)
is DocumentContent -> replyWithDocument(
it,
outFile.asMultipartFile()
)
is VoiceContent -> replyWithVoice(
it,
outFile.asMultipartFile()
)
is VideoNoteContent -> replyWithVideoNote(
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) }
}.second.join() }.second.join()
} }

View File

@@ -1,9 +1,35 @@
# ForwarderBot # ForwardInfoSenderBot
The main purpose of this bot is just to send info about forwarder when bot receive any update 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.
## Launch ## Behavior
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`:
- 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.
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.
## Setup and permissions
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.
Run from the repository root:
```bash ```bash
../gradlew run --args="BOT_TOKEN" ./gradlew :ForwardInfoSenderBot: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.

View File

@@ -1,29 +1,36 @@
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
import dev.inmo.tgbotapi.extensions.utils.formatting.* import dev.inmo.tgbotapi.extensions.utils.formatting.makeLink
import dev.inmo.tgbotapi.types.chat.CommonBot import dev.inmo.tgbotapi.types.chat.CommonBot
import dev.inmo.tgbotapi.types.chat.CommonUser import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.chat.ExtendedBot import dev.inmo.tgbotapi.types.chat.ExtendedBot
import dev.inmo.tgbotapi.types.message.* import dev.inmo.tgbotapi.types.message.*
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.code
import dev.inmo.tgbotapi.utils.link
import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.* import kotlinx.coroutines.*
/** /**
* This bot will always return message about forwarder. In cases when sent message was not a forward message it will * Starts a long-polling bot that replies to each received content message with its forward-source metadata.
* send suitable message *
* Messages without forward metadata receive a corresponding fallback response.
*
* @param args the bot token as the required first element; any remaining elements are ignored
*/ */
suspend fun main(vararg args: String) { suspend fun main(vararg args: String) {
val botToken = args.first() val botToken = args.first()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) { telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
onContentMessage(subcontextUpdatesFilter = { _, _ -> true }) { onContentMessage {
val toAnswer = buildEntities { val toAnswer = buildEntities {
when (val forwardInfo = it.forwardInfo) { when (val forwardInfo = it.forwardInfo) {
null -> +"There is no forward info" null -> +"There is no forward info"
is AnonymousForwardInfo -> { is ForwardInfo.ByAnonymous -> {
regular("Anonymous user which signed as \"") + code(forwardInfo.senderName) + "\"" regular("Anonymous user which signed as \"") + code(forwardInfo.senderName) + "\""
} }
is UserForwardInfo -> { is ForwardInfo.ByUser -> {
val user = forwardInfo.from val user = forwardInfo.from
when (user) { when (user) {
is CommonUser -> { is CommonUser -> {
@@ -33,12 +40,21 @@ suspend fun main(vararg args: String) {
regular("User ") regular("User ")
} }
} }
is CommonBot, is CommonBot,
is ExtendedBot -> regular("Bot ") is ExtendedBot -> regular("Bot ")
} + code(user.id.chatId.toString()) + " (${user.firstName} ${user.lastName}: ${user.username ?.username ?: "Without username"})" } + code(user.id.chatId.toString()) + " (${user.firstName} ${user.lastName}: ${user.username?.username ?: "Without username"})"
} }
is ForwardFromChannelInfo -> regular("Channel (") + code((forwardInfo.channelChat).title) + ")" is ForwardInfo.PublicChat.FromChannel -> {
is ForwardFromSupergroupInfo -> regular("Supergroup (") + code((forwardInfo.group).title) + ")" regular("Channel (") + (forwardInfo.channelChat.username ?.let {
link(
forwardInfo.channelChat.title,
makeLink(it)
)
} ?: code(forwardInfo.channelChat.title)) + ")"
}
is ForwardInfo.PublicChat.FromSupergroup -> regular("Supergroup (") + code(forwardInfo.group.title) + ")"
is ForwardInfo.PublicChat.SentByChannel -> regular("Sent by channel (") + code(forwardInfo.channelChat.title) + ")"
} }
} }
reply(it, toAnswer) reply(it, toAnswer)

View File

@@ -1,9 +0,0 @@
# GetMeBot
This is one of the most easiest bot - it will just print information about itself
## Launch
```bash
../gradlew run --args="BOT_TOKEN"
```

View File

@@ -1,13 +0,0 @@
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
/**
* This is one of the most easiest bot - it will just print information about itself
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
val bot = telegramBot(botToken)
println(bot.getMe())
}

50
GiftsBot/README.md Normal file
View File

@@ -0,0 +1,50 @@
# GiftsBot
Demonstrates the paginated owned-gift APIs by listing gifts for the chat in which the command is received.
## Behavior
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:
- 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.
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`.
The bot also handles `UniqueGiftInfo` service messages with `onUniqueGiftSentOrReceived`. It logs and replies with
Bot API 10.3's `text`, parsed `textSources` (`entities`), and `isPrivate` fields. When entities are present, the reply
reuses them so the gift text keeps its formatting.
## Command
- `/start` — lists the owned gifts selected by the current chat type. It must be the only command in the message and
takes no arguments.
Other commands and ordinary non-command messages are ignored; unique-gift service messages are handled separately.
## Setup and permissions
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.
The example performs no access checks or error recovery, so Telegram API or permission errors end that command
handler.
## 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:
```bash
./gradlew :GiftsBot:run --args="<BOT_TOKEN>"
```

21
GiftsBot/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="GiftsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,143 @@
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.business.getBusinessAccountGiftsFlow
import dev.inmo.tgbotapi.extensions.api.gifts.getChatGiftsFlow
import dev.inmo.tgbotapi.extensions.api.gifts.getUserGiftsFlow
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.withTypingAction
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onUniqueGiftSentOrReceived
import dev.inmo.tgbotapi.types.chat.BusinessChat
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.chat.PublicChat
import dev.inmo.tgbotapi.types.chat.UnknownChatType
import dev.inmo.tgbotapi.types.gifts.OwnedGift
import dev.inmo.tgbotapi.types.message.textsources.splitForText
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/**
* 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. Unique-gift service messages also expose the Bot API 10.3 text, entities and privacy flag.
* The bot prints its [getMe] result at startup.
*
* @param args the bot token followed by optional, case-sensitive `debug` and `testServer` flags; unknown trailing
* arguments are ignored
* @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, testServer = isTestServer) {
// start here!!
val me = getMe()
println(me)
onUniqueGiftSentOrReceived { message ->
val uniqueGiftInfo = message.chatEvent
println(
"Unique gift ${uniqueGiftInfo.gift.name.value}: " +
"text=${uniqueGiftInfo.text}, textSources=${uniqueGiftInfo.textSources}, " +
"isPrivate=${uniqueGiftInfo.isPrivate}"
)
reply(
message,
buildEntities {
bold("Unique gift") + ": ${uniqueGiftInfo.gift.name.value}\n"
bold("Private") + ": ${uniqueGiftInfo.isPrivate}\n"
bold("Text") + ": "
if (uniqueGiftInfo.textSources.isEmpty()) {
regular(uniqueGiftInfo.text ?: "(None)")
} else {
+uniqueGiftInfo.textSources
}
}
)
}
onCommand("start") {
val giftsFlow = when (val chat = it.chat) {
is BusinessChat -> {
getBusinessAccountGiftsFlow(
chat.id.businessConnectionId
)
}
is PrivateChat -> {
getUserGiftsFlow(it.chat.id)
}
is UnknownChatType,
is PublicChat -> {
getChatGiftsFlow(it.chat.id)
}
}
withTypingAction(it.chat) {
val texts = buildEntities {
giftsFlow.collect { ownedGifts ->
ownedGifts.gifts.forEach {
when (it) {
is OwnedGift.Regular.Common -> {
bold("Type") + ": Regular common\n"
bold("Id") + ": ${it.gift.id.string}\n"
bold("Text") + ": ${it.text ?: "(None)"}\n"
bold("Stars cost") + ": ${it.gift.starCount}\n"
}
is OwnedGift.Unique.Common -> {
bold("Type") + ": Unique common\n"
bold("Id") + ": ${it.gift.id ?.string ?: "(None)"}\n"
bold("Name") + ": ${it.gift.name.value}\n"
bold("Model") + ": ${it.gift.model.name}\n"
bold("Number") + ": ${it.gift.number}\n"
}
is OwnedGift.Regular.OwnedByBusinessAccount -> {
bold("Type") + ": Regular owned by business\n"
bold("Id") + ": ${it.gift.id.string}\n"
bold("Text") + ": ${it.text ?: "(None)"}\n"
bold("Stars cost") + ": ${it.gift.starCount}\n"
}
is OwnedGift.Unique.OwnedByBusinessAccount -> {
bold("Type") + ": Unique owned by business\n"
bold("Id") + ": ${it.gift.id ?.string ?: "(None)"}\n"
bold("Name") + ": ${it.gift.name.value}\n"
bold("Model") + ": ${it.gift.model.name}\n"
bold("Number") + ": ${it.gift.number}\n"
}
}
}
}
}
val preparedTexts = texts.splitForText()
if (preparedTexts.isEmpty()) {
reply(it, "This chat have no any gifts")
} else {
preparedTexts.forEach { preparedText -> reply(it, preparedText) }
}
}
}
// allUpdatesFlow.subscribeLoggingDropExceptions(this) {
// println(it)
// }
}.second.join()
}

43
GiveawaysBot/README.md Normal file
View File

@@ -0,0 +1,43 @@
# GiveawaysBot
A long-polling example that prints giveaway-related Telegram updates to standard output.
## Behavior
At startup, the bot calls `getMe` and prints its own user information. It then prints
updates matched by these TelegramBotAPI handlers:
- `onGiveawayCreated` — a giveaway was created;
- `onGiveawayCompleted` — a giveaway was completed;
- `onGiveawayWinners` — the giveaway winners were published;
- `onGiveawayContent` — a message contains giveaway content.
The bot sends no replies and defines no bot commands.
## Setup and permissions
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.
The example does not call admin-only methods, store data, or configure a webhook.
## Run
From the repository root:
```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"
```

21
GiveawaysBot/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="GiveawaysBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,62 @@
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.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCompleted
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayContent
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCreated
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayWinners
import 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
*/
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) {
// start here!!
val me = getMe()
println(me)
onGiveawayCreated {
println(it)
}
onGiveawayCompleted {
println(it)
}
onGiveawayWinners {
println(it)
}
onGiveawayContent {
println(it)
}
// allUpdatesFlow.subscribeLoggingDropExceptions(this) {
// println(it)
// }
}.second.join()
}

50
GuestQueryBot/README.md Normal file
View File

@@ -0,0 +1,50 @@
# 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"
```

View File

@@ -0,0 +1,21 @@
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"
}

View File

@@ -0,0 +1,100 @@
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.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()
}

View File

@@ -1,9 +1,43 @@
# HelloBot # HelloBot
The main purpose of this bot is just to answer "Oh, hi, " and add user mention here HelloBot is a small long-polling example that greets the chat or sender when a
message addresses the bot by username.
## Launch ## Trigger and replies
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.
- 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.
Every received update is also printed to standard output for demonstration and
debugging.
## Setup
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.
## Run
From the repository root, pass the token as the first positional argument:
```bash ```bash
../gradlew run --args="BOT_TOKEN" ./gradlew :HelloBot: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.

View File

@@ -1,45 +1,81 @@
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions 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.get.getChat
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.sendTextMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMentionWithAnyContent
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.sender_chat
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
import dev.inmo.tgbotapi.extensions.utils.formatting.linkMarkdownV2 import dev.inmo.tgbotapi.extensions.utils.formatting.linkMarkdownV2
import dev.inmo.tgbotapi.extensions.utils.formatting.textMentionMarkdownV2 import dev.inmo.tgbotapi.extensions.utils.formatting.textMentionMarkdownV2
import dev.inmo.tgbotapi.extensions.utils.ifFromChannelGroupContentMessage
import dev.inmo.tgbotapi.types.chat.* import dev.inmo.tgbotapi.types.chat.*
import dev.inmo.tgbotapi.types.chat.GroupChat
import dev.inmo.tgbotapi.types.chat.PrivateChat
import dev.inmo.tgbotapi.types.chat.SupergroupChat
import dev.inmo.tgbotapi.types.message.MarkdownV2 import dev.inmo.tgbotapi.types.message.MarkdownV2
import dev.inmo.tgbotapi.utils.PreviewFeature import dev.inmo.tgbotapi.utils.PreviewFeature
import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
/** /**
* The main purpose of this bot is just to answer "Oh, hi, " and add user mention here * 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
*/ */
@OptIn(PreviewFeature::class) @OptIn(PreviewFeature::class)
suspend fun main(vararg args: String) { suspend fun main(vararg args: String) {
val botToken = args.first() val botToken = args.first()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) { telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
onContentMessage { message -> val me = getMe()
val chat = message.chat onContentMessage(
if (chat is ChannelChat) { initialFilter = initialFilter@{ it.text ?.contains(me.username ?.full ?: return@initialFilter false) == true }
val answer = "Hi everybody in this channel \"${chat.title}\"" ) { message ->
sendTextMessage(chat, answer, MarkdownV2) val answerText = when (val chat = message.chat) {
return@onContentMessage is PreviewChannelChat -> {
} val sender = message.sender_chat
val answerText = "Oh, hi, " + when (chat) { val answer = "Hi everybody in this channel \"${chat.title}\"" + if (sender != null) {
is User -> "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id) " and you, " + when (sender) {
is PrivateChat -> "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id) is BusinessChat -> "business chat (wat) ${sender.original}"
is SupergroupChat -> (chat.username ?.username ?: getChat(chat).inviteLink) ?.let { is PrivateChat -> "${sender.lastName} ${sender.firstName}"
chat.title.linkMarkdownV2(it) is GroupChat -> "group ${sender.title}"
} ?: chat.title is ChannelChat -> "channel ${sender.title}"
is GroupChat -> bot.getChat(chat).inviteLink ?.let { is UnknownChatType -> "wat chat (${sender})"
chat.title.linkMarkdownV2(it) }
} ?: chat.title } else {
else -> "Unknown :(".escapeMarkdownV2Common() ""
}
reply(message, answer.escapeMarkdownV2Common(), MarkdownV2)
return@onContentMessage
}
is PreviewPrivateChat -> {
reply(message, "Hi, " + "${chat.firstName} ${chat.lastName}".textMentionMarkdownV2(chat.id), MarkdownV2)
return@onContentMessage
}
is PreviewGroupChat -> {
message.ifFromChannelGroupContentMessage<Unit> {
val answer = "Hi, ${it.senderChat.title}"
reply(message, answer, MarkdownV2)
return@onContentMessage
}
"Oh, hi, " + when (chat) {
is SupergroupChat -> (chat.username ?.username ?: getChat(chat).inviteLink) ?.let {
chat.title.linkMarkdownV2(it)
} ?: chat.title
else -> bot.getChat(chat).inviteLink ?.let {
chat.title.linkMarkdownV2(it)
} ?: chat.title
}
}
is PreviewBusinessChat -> {
reply(message, "Hi, " + "${chat.original.firstName} ${chat.original.lastName} (as business chat :) )".textMentionMarkdownV2(chat.original.id), MarkdownV2)
return@onContentMessage
}
is UnknownChatType -> "Unknown :(".escapeMarkdownV2Common()
} }
reply( reply(
message, message,
@@ -47,6 +83,6 @@ suspend fun main(vararg args: String) {
MarkdownV2 MarkdownV2
) )
} }
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) } allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
}.second.join() }.second.join()
} }

View File

@@ -0,0 +1,51 @@
# 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.
## Behavior
For every inline query, the bot:
- 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.
The bot also prints its own account information at startup, logs received updates, and prints polling exceptions.
## Setup
Create a bot and obtain its token, then enable inline mode for it in BotFather (for example, with `/setinline`). Keep the
token private.
Both launchers require the bot token as the first command-line argument. Starting either launcher without an argument
fails immediately; additional arguments are ignored.
## Launch from the repository root
### JVM
```bash
./gradlew :InlineQueriesBot:runJvm --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`.

View File

@@ -0,0 +1,37 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
plugins {
id "org.jetbrains.kotlin.multiplatform"
}
apply from: "$nativePartTemplate"
kotlin {
jvm {
binaries {
executable {
mainClass.set("InlineQueriesBotKt")
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api "dev.inmo:tgbotapi:$telegram_bot_api_version"
api "io.ktor:ktor-client-logging:$ktor_version"
}
}
}
}

View File

@@ -0,0 +1,67 @@
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.telegramBot
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onDeepLink
import dev.inmo.tgbotapi.requests.answers.InlineQueryResultsButton
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.types.inlineQueryAnswerResultsLimit
import dev.inmo.tgbotapi.utils.buildEntities
/**
* Starts the inline-query bot with [token] and suspends until long polling stops.
*/
suspend fun doInlineQueriesBot(token: String) {
val bot = telegramBot(token)
bot.buildBehaviourWithLongPolling(
defaultExceptionsHandler = { it.printStackTrace() },
) {
onBaseInlineQuery {
val page = it.offset.toIntOrNull() ?: 0
val results = (0 until inlineQueryAnswerResultsLimit.last).map {
(page * inlineQueryAnswerResultsLimit.last) + it
}
answer(
it,
results = results.map { resultNumber ->
val inlineQueryId = InlineQueryId(resultNumber.toString())
InlineQueryResultArticle(
inlineQueryId,
"Title $resultNumber",
InputTextMessageContent(
buildEntities {
+"Result text of " + resultNumber.toString() + " result:\n"
+it.query
}
),
description = "Description of $resultNumber result"
)
},
cachedTime = 0,
isPersonal = true,
button = InlineQueryResultsButton.Start(
"Text of button with page $page",
"deep_link_for_page_$page"
),
nextOffset = (page + 1).toString()
)
}
onDeepLink { (message, deepLink) ->
reply(message, deepLink)
}
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it)
}
println(getMe())
}.join()
}

View File

@@ -0,0 +1,4 @@
/** JVM entry point; [args] must contain the bot token as its first element. */
suspend fun main(args: Array<String>) {
doInlineQueriesBot(args.first())
}

View File

@@ -0,0 +1,8 @@
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())
}
}

View File

@@ -0,0 +1,48 @@
# 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"
```

View File

@@ -0,0 +1,21 @@
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"
}

View File

@@ -0,0 +1,95 @@
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()
}

View File

@@ -1,23 +1,35 @@
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.edit.text.editMessageText import dev.inmo.tgbotapi.extensions.api.edit.text.editMessageText
import dev.inmo.tgbotapi.extensions.api.send.* import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
import dev.inmo.tgbotapi.extensions.utils.formatting.botCommand
import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities
import dev.inmo.tgbotapi.extensions.utils.types.buttons.* import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.extensions.utils.withContent import dev.inmo.tgbotapi.extensions.utils.withContent
import dev.inmo.tgbotapi.types.BotCommand 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.InputTextMessageContent
import dev.inmo.tgbotapi.types.InlineQueryId
import dev.inmo.tgbotapi.types.buttons.KeyboardButtonStyle
import dev.inmo.tgbotapi.types.message.content.TextContent import dev.inmo.tgbotapi.types.message.content.TextContent
import kotlinx.coroutines.* import dev.inmo.tgbotapi.utils.PreviewFeature
import dev.inmo.tgbotapi.utils.botCommand
private const val nextPageData = "next" import dev.inmo.tgbotapi.utils.regular
private const val previousPageData = "previous" import dev.inmo.tgbotapi.utils.row
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>? { fun String.parsePageAndCount(): Pair<Int, Int>? {
val (pageString, countString) = split(" ").takeIf { it.count() > 1 } ?: return null val (pageString, countString) = split(" ").takeIf { it.count() > 1 } ?: return null
return Pair( return Pair(
@@ -26,6 +38,15 @@ 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) { fun InlineKeyboardBuilder.includePageButtons(page: Int, count: Int) {
val numericButtons = listOfNotNull( val numericButtons = listOfNotNull(
page - 1, page - 1,
@@ -36,28 +57,55 @@ fun InlineKeyboardBuilder.includePageButtons(page: Int, count: Int) {
val numbersRange = 1 .. count val numbersRange = 1 .. count
numericButtons.forEach { numericButtons.forEach {
if (it in numbersRange) { if (it in numbersRange) {
dataButton(it.toString(), "$it $count") if (it == page) {
disabledButton(it.toString(), style = KeyboardButtonStyle.Primary)
} else {
dataButton(it.toString(), "$it $count")
}
} }
} }
} }
row {
copyTextButton("Command copy button", "/inline $page $count")
}
row { row {
if (page - 1 > 2) { if (page - 1 > 2) {
dataButton("<<", "1 $count") dataButton("<<", "1 $count", style = KeyboardButtonStyle.Danger)
} }
if (page - 1 > 1) { if (page - 1 > 1) {
dataButton("<", "${page - 2} $count") dataButton("<", "${page - 2} $count", style = KeyboardButtonStyle.Primary)
} }
if (page + 1 < count) { if (page + 1 < count) {
dataButton(">", "${page + 2} $count") dataButton(">", "${page + 2} $count", style = KeyboardButtonStyle.Success)
} }
if (page + 2 < count) { if (page + 2 < count) {
dataButton(">>", "$count $count") dataButton(">>", "$count $count", style = KeyboardButtonStyle.Danger)
} }
} }
row {
inlineQueryInChosenChatButton(
"Send somebody page",
query = "$page $count",
allowUsers = true,
allowBots = true,
allowGroups = true,
allowChannels = true,
)
}
} }
/**
* 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( suspend fun activateKeyboardsBot(
token: String, token: String,
print: (Any) -> Unit print: (Any) -> Unit
@@ -68,16 +116,17 @@ suspend fun activateKeyboardsBot(
bot.buildBehaviourWithLongPolling(CoroutineScope(currentCoroutineContext() + SupervisorJob())) { bot.buildBehaviourWithLongPolling(CoroutineScope(currentCoroutineContext() + SupervisorJob())) {
onCommandWithArgs("inline") { message, args -> onCommandWithArgs("inline") { message, args ->
val numberOfPages = args.firstOrNull() ?.toIntOrNull() ?: 10 val numberArgs = args.mapNotNull { it.toIntOrNull() }
val numberOfPages = numberArgs.getOrNull(1) ?: numberArgs.firstOrNull() ?: 10
val page = numberArgs.firstOrNull()?.takeIf { numberArgs.size > 1 }?.coerceAtLeast(1) ?: 1
reply( reply(
message, message,
"Your inline keyboard with $numberOfPages pages", replyMarkup = inlineKeyboard(forceReply = true) {
replyMarkup = inlineKeyboard { includePageButtons(page, numberOfPages)
row {
includePageButtons(1, numberOfPages)
}
} }
) ) {
regular("Your inline keyboard with $numberOfPages pages")
}
} }
onMessageDataCallbackQuery { onMessageDataCallbackQuery {
@@ -86,39 +135,76 @@ suspend fun activateKeyboardsBot(
return@onMessageDataCallbackQuery return@onMessageDataCallbackQuery
} }
val text = "This is $page of $count" edit(
editMessageText(
it.message.withContent<TextContent>() ?: it.let { it.message.withContent<TextContent>() ?: it.let {
answer(it, "Unsupported message type :(") answer(it, "Unsupported message type :(")
return@onMessageDataCallbackQuery return@onMessageDataCallbackQuery
}, },
text, replyMarkup = inlineKeyboard(forceReply = true) {
replyMarkup = inlineKeyboard { includePageButtons(page, count)
row {
includePageButtons(page, count)
}
} }
) {
regular("This is $page of $count")
}
answer(it)
}
onInlineMessageIdDataCallbackQuery {
val (page, count) = it.data.parsePageAndCount() ?: it.let {
answer(it, "Unsupported data :(")
return@onInlineMessageIdDataCallbackQuery
}
editMessageText(
it.inlineMessageId,
replyMarkup = inlineKeyboard(forceReply = true) {
includePageButtons(page, count)
}
) {
regular("This is $page of $count")
}
answer(it)
}
onBaseInlineQuery {
val page = it.query.takeWhile { it.isDigit() }.toIntOrNull() ?: return@onBaseInlineQuery
val count = it.query.removePrefix(page.toString()).dropWhile { !it.isDigit() }.takeWhile { it.isDigit() }
.toIntOrNull() ?: return@onBaseInlineQuery
answer(
it,
results = listOf(
InlineQueryResultArticle(
InlineQueryId(it.query),
"Send buttons",
InputTextMessageContent("It is sent via inline mode inline buttons"),
replyMarkup = inlineKeyboard(forceReply = true) {
includePageButtons(page, count)
}
)
)
) )
} }
onUnhandledCommand { onUnhandledCommand {
reply( reply(
it, it,
buildEntities { replyMarkup = replyKeyboard(
+"Use " + botCommand("inline") + " to get pagination inline keyboard" resizeKeyboard = true,
}, oneTimeKeyboard = true,
replyMarkup = replyKeyboard(resizeKeyboard = true, oneTimeKeyboard = true) { forceReply = true,
) {
row { row {
simpleButton("/inline") simpleButton("/inline", style = KeyboardButtonStyle.Primary)
} }
} }
) ) {
+"Use " + botCommand("inline") + " to get pagination inline keyboard"
}
} }
setMyCommands(BotCommand("inline", "Creates message with pagination inline keyboard")) setMyCommands(BotCommand("inline", "Creates message with pagination inline keyboard"))
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
println(it) println(it)
} }
}.join() }.join()

View File

@@ -4,6 +4,12 @@ import org.w3c.dom.*
private val scope = CoroutineScope(Dispatchers.Default) 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() { fun main() {
document.addEventListener( document.addEventListener(
"DOMContentLoaded", "DOMContentLoaded",

66
KeyboardsBot/README.md Normal file
View File

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

View File

@@ -1,7 +1,27 @@
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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext 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>) { suspend fun main(args: Array<String>) {
val isDebug = args.any { it == "debug" }
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
withContext(Dispatchers.IO) { // IO for inheriting of it in side of activateKeyboardsBot withContext(Dispatchers.IO) { // IO for inheriting of it in side of activateKeyboardsBot
activateKeyboardsBot(args.first()) { activateKeyboardsBot(args.first()) {
println(it) println(it)

47
LinkPreviewsBot/README.md Normal file
View File

@@ -0,0 +1,47 @@
# LinkPreviewsBot
A long-polling example that resends text-bearing content with every demonstrated
`LinkPreviewOptions` variant.
## Behavior
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:
- 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.
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.
## Setup and permissions
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.
The example uses no administrator-only methods and does not configure a webhook.
## Run
From the repository root:
```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"
```

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="LinkPreviewsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,89 @@
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.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
import dev.inmo.tgbotapi.extensions.utils.textLinkTextSourceOrNull
import dev.inmo.tgbotapi.extensions.utils.uRLTextSourceOrNull
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.LinkPreviewOptions
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.
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
val isDebug = args.getOrNull(1) == "debug"
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
val bot = telegramBot(botToken)
bot.buildBehaviourWithLongPolling {
onContentMessage { contentMessage ->
val url = contentMessage.withContentOrNull<TextedContent>() ?.let { message ->
message.content.textSources.firstNotNullOfOrNull {
it.textLinkTextSourceOrNull() ?.url ?: it.uRLTextSourceOrNull() ?.source
}
} ?: null.apply {
reply(contentMessage) {
regular("I am support only content with text contains url only")
}
} ?: return@onContentMessage
contentMessage.withContentOrNull<TextedContent>() ?.let {
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Disabled
)
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Large(url, showAboveText = true)
)
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Large(url, showAboveText = false)
)
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Small(url, showAboveText = true)
)
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Small(url, showAboveText = false)
)
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Default(url, showAboveText = true)
)
send(
it.chat,
it.content.textSources,
linkPreviewOptions = LinkPreviewOptions.Default(url, showAboveText = false)
)
}
}
}.join()
}

View File

@@ -0,0 +1,39 @@
# LiveLocationsBot
A long-polling example that sends, updates, and stops a live-location message.
## Commands and behavior
- `/start` begins a live-location sequence in the command's chat.
- `Cancel`, an inline button on the live-location message, stops that sequence.
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.
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.
Every received update is printed to standard output. The `/start` handler is not
separately registered in Telegram's command menu.
## Setup and permissions
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.
The coordinates do not come from the user's device. The example needs no
administrator-only methods and does not configure a webhook.
## Run
From the repository root:
```bash
./gradlew :LiveLocationsBot: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`.

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="LiveLocationsBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,71 @@
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.tgbotapi.extensions.api.EditLiveLocationInfo
import dev.inmo.tgbotapi.extensions.api.edit.location.live.stopLiveLocation
import dev.inmo.tgbotapi.extensions.api.handleLiveLocation
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.LocationContent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
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.
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
val locationsFlow = flow {
var i = 0
while (isActive) {
val newInfo = EditLiveLocationInfo(
latitude = i.toDouble(),
longitude = i.toDouble(),
replyMarkup = flatInlineKeyboard {
dataButton("Cancel", "cancel")
}
)
emit(newInfo)
i++
delay(3000L) // 3 seconds
}
}
onCommand("start") {
// in this flow will be actual message with live location
val currentMessageState = MutableStateFlow<ContentMessage<LocationContent>?>(null)
val sendingJob = launch {
handleLiveLocation(
it.chat.id,
locationsFlow,
sentMessageFlow = { currentMessageState.emit(it) },
)
}
waitMessageDataCallbackQuery().filter {
it.message.sameMessage(
currentMessageState.value ?: return@filter false
) && it.data == "cancel"
}.first()
sendingJob.cancel() // ends live location
currentMessageState.value ?.let {
stopLiveLocation(it, replyMarkup = null)
}
}
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
}.second.join()
}

60
LivePhotosBot/README.md Normal file
View File

@@ -0,0 +1,60 @@
# 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 by file ID, downloads both components, edits the resent message with new multipart files, and uploads the files again as paid content costing 1 Star. |
| Live Photo gallery | Logs every item, downloads each main and cover file, and re-uploads the gallery with `sendMediaGroup`. |
| Paid-media message containing Live Photos | Logs each Live Photo and replies with the number found. Paid-media messages without a Live Photo get no reply from this handler. |
| Edited Live Photo | Logs the file ID and updated caption. |
| Media group containing at least one regular photo and one regular video | Uses the first photo as the cover and the first video as the motion part, then replies with a Live Photo. Albums missing either type are ignored by this handler. |
## Live Photo handling
The initial `sendLivePhoto` call reuses Telegram file IDs. The regular-media edit,
paid-media send, and gallery resend then demonstrate ktgbotapi 37.0.0 collecting
the secondary Live Photo `photo` attachment as well as the main multipart file.
If Telegram supplies no cover photo, the code falls back to the main file ID for
the `photo` field. Downloaded files are held in memory and are not transformed.
The standalone handler performs its requests in order: resend, download, edit,
then send paid media. The edit can therefore succeed in a private or group chat
before the channel-only paid-media request fails.
## Telegram setup and permissions
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"
```

View File

@@ -0,0 +1,21 @@
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"
}

View File

@@ -0,0 +1,198 @@
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.files.downloadFile
import dev.inmo.tgbotapi.extensions.api.send.media.sendLivePhoto
import dev.inmo.tgbotapi.extensions.api.send.media.sendMediaGroup
import dev.inmo.tgbotapi.extensions.api.send.media.sendPaidMedia
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.requests.abstracts.asMultipartFile
import dev.inmo.tgbotapi.types.message.content.LivePhotoContent
import dev.inmo.tgbotapi.types.message.payments.PaidMedia
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
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}")
// Download both Live Photo components once. ktgbotapi 37.0.0 collects the secondary `photo`
// MultipartFile alongside the main file for edits, media groups, and paid-media requests.
val livePhotoBytes = downloadFile(livePhotoFile)
val coverPhotoBytes = livePhotoFile.photo?.let { downloadFile(it) }
// editMessageMedia with InputMediaLivePhoto (TelegramMediaLivePhoto):
// edit the previously sent message using newly uploaded main and cover files.
val sentAsMedia = sent.withContentOrNull<LivePhotoContent>()
if (sentAsMedia != null) {
editMessageMedia(
message = sentAsMedia,
media = TelegramMediaLivePhoto(
file = livePhotoBytes.asMultipartFile("edited-live-photo.mp4"),
photo = coverPhotoBytes?.asMultipartFile("edited-live-photo-cover.jpg")
?: livePhotoFile.photo?.fileId
?: livePhotoFile.fileId,
text = "Edited with newly uploaded Live Photo files"
)
)
}
// InputPaidMediaLivePhoto (TelegramPaidMediaLivePhoto): upload both files as paid media (1 star).
// Telegram currently restricts sendPaidMedia to channel chats.
sendPaidMedia(
chatId = message.chat.id,
starCount = 1,
media = listOf(
TelegramPaidMediaLivePhoto(
file = livePhotoBytes.asMultipartFile("paid-live-photo.mp4"),
photo = coverPhotoBytes?.asMultipartFile("paid-live-photo-cover.jpg")
?: livePhotoFile.photo?.fileId
?: livePhotoFile.fileId
)
),
text = "Paid live photo uploaded as new files (1 star)"
)
}
// Demonstrates: sendMediaGroup with live photos, InputMediaLivePhoto (TelegramMediaLivePhoto)
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 newly uploaded main and cover files for every TelegramMediaLivePhoto.
sendMediaGroup(
chatId = mediaGroupContent.group.first().sourceMessage.chat.id,
media = mediaGroupContent.group.mapIndexed { index, groupMember ->
val livePhotoFile = groupMember.content.media
val coverPhoto = livePhotoFile.photo
TelegramMediaLivePhoto(
file = downloadFile(livePhotoFile).asMultipartFile("gallery-live-photo-$index.mp4"),
photo = coverPhoto?.let {
downloadFile(it).asMultipartFile("gallery-live-photo-cover-$index.jpg")
} ?: 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()
}

53
ManagedBotsBot/README.md Normal file
View File

@@ -0,0 +1,53 @@
# ManagedBotsBot
A long-polling playground for creating and administering managed bots, inspecting a
user's personal-channel messages, and trying bot-to-bot messages.
## Commands and triggers
- `/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!`.
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.
## Setup, permissions, and safety
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.
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.
## Run
The intended command from the repository root is:
```bash
./gradlew :ManagedBotsBot: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.

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="CustomBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,220 @@
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.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
import dev.inmo.tgbotapi.extensions.utils.groupContentMessageOrNull
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.request.RequestId
import dev.inmo.tgbotapi.types.toChatId
import dev.inmo.tgbotapi.types.update.abstracts.Update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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<*>
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.
*/
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,
builder = {
includeMiddlewares {
addMiddleware {
doOnRequestReturnResult { result, request, _ ->
println("Result of $request:\n\n$result")
null
}
}
}
},
subcontextInitialAction = buildSubcontextInitialAction {
add {
data.update = it
}
}
) {
// start here!!
val me = getMe()
println(me)
onCommand("start") {
println(data.update)
println(data.commonMessage)
println(getChat(it.chat))
}
onCommand("canManageBots") {
val me = getMe()
reply(it, if (me.canManageBots) "Yes" else "No")
}
val requestId = RequestId(0)
onCommand("keyboard") {
reply(
it,
"Keyboard",
replyMarkup = flatReplyKeyboard(
resizeKeyboard = true,
oneTimeKeyboard = true,
) {
requestManagedBotButton(
"Add managed bot",
KeyboardButtonRequestManagedBot(
requestId = requestId,
suggestedName = "SampleName",
suggestedUsername = Username("@some_sample_bot")
)
)
}
)
}
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")
}
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")
}
onCommand("replaceToken") {
val reply = it.replyTo ?.chatEventMessageOrNull() ?: return@onCommand
val managedBotCreated = reply.chatEvent.managedBotCreatedOrNull() ?: return@onCommand
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)
}
}.second.join()
}

View File

@@ -0,0 +1,49 @@
# 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.
## Behavior
The bot handles these transitions:
- **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.
The bot also identifies updates about itself:
- 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.
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.
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.
## Telegram setup, permissions, and privacy
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.
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.
## 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"
```

View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="MemberUpdatedWatcherKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,107 @@
import dev.inmo.kslog.common.*
import dev.inmo.tgbotapi.extensions.api.*
import dev.inmo.tgbotapi.extensions.api.bot.*
import dev.inmo.tgbotapi.extensions.api.send.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
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.triggers_handling.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.*
import dev.inmo.tgbotapi.extensions.utils.*
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()
val isDebug = args.any { it == "debug" }
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
val internalLogger = KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag ?: "ChatMemberUpdates", message, throwable))
}
val bot = telegramBot(token)
bot.buildBehaviourWithLongPolling {
val me = getMe()
val filterSelfUpdates = SimpleFilter<ChatMemberUpdated> {
it.member.id == me.id
}
// This bot updates
onChatMemberJoined(initialFilter = filterSelfUpdates) {
internalLogger.i("Bot was added to chat")
send(it.chat.id, "I was added to chat. Please grant me admin permissions to make me able to watch other users' events")
}
onChatMemberGotPromoted(initialFilter = filterSelfUpdates) {
internalLogger.i("Bot was granted admin permissions")
send(it.chat.id, "I was promoted to admin. I now can watch other users' events")
}
onChatMemberGotDemoted(initialFilter = filterSelfUpdates) {
internalLogger.i("Admin permissions were revoked")
send(it.chat.id, "I'm no longer an admin. Admin permissions are required to watch other users' events")
}
// All users updates
onChatMemberJoined {
val member = it.member
internalLogger.i("${member.firstName} joined the chat: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
send(it.chat.id, "Welcome ${member.firstName}")
}
onChatMemberLeft {
val member = it.member
internalLogger.i("${member.firstName} left the chat: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
send(it.chat.id, "Goodbye ${member.firstName}")
}
onChatMemberGotPromoted {
val newState = it.newChatMemberState.administratorChatMemberOrThrow()
internalLogger.i("${newState.user.firstName} got promoted to ${newState.customTitle ?: "Admin"}: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
send(it.chat.id, "${newState.user.firstName} is now an ${newState.customTitle ?: "Admin"}")
}
onChatMemberGotDemoted {
val member = it.member
internalLogger.i("${member.firstName} got demoted: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}")
send(it.chat.id, "${member.firstName} is now got demoted back to member")
}
onChatMemberGotPromotionChanged {
val member = it.member
val message = "${member.firstName} has the permissions changed: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}"
internalLogger.i(message)
send(it.chat.id, message)
}
onChatMemberUpdated(
initialFilter = chatMemberGotRestrictedFilter + chatMemberGotRestrictionsChangedFilter,
) {
val member = it.member
val message = "${member.firstName} has the permissions changed: ${it.oldChatMemberState::class.simpleName} => ${it.newChatMemberState::class.simpleName}"
internalLogger.i(message)
send(it.chat.id, message)
}
}.join()
}

45
MyBot/README.md Normal file
View File

@@ -0,0 +1,45 @@
# MyBot
A long-polling example that prints information about the bot and lets Telegram users replace or remove its global profile photo.
## Behavior
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.
## Arguments
The first argument is always the required bot token. Optional arguments can follow it in any order:
- `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.
Other arguments are ignored. Argument matching is case-sensitive.
## Launch
The intended command from the repository root is:
```bash
./gradlew :MyBot: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.

21
MyBot/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
apply plugin: 'kotlin'
apply plugin: 'application'
mainClassName="GetMeBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}

View File

@@ -0,0 +1,97 @@
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.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.removeMyProfilePhoto
import dev.inmo.tgbotapi.extensions.api.bot.setMyProfilePhoto
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.files.downloadFileToTemp
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.sendMessageDraftFlowWithTexts
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitPhotoMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
import dev.inmo.tgbotapi.requests.business_connection.InputProfilePhoto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
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
*/
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 bot = telegramBot(botToken)
telegramBotWithBehaviourAndLongPolling(
botToken,
CoroutineScope(Dispatchers.Default),
testServer = isTestServer,
) {
val me = bot.getMe()
println(me)
println(bot.getChat(me))
onCommand("setMyProfilePhoto") { commandMessage ->
reply(commandMessage, "ok, send me new photo")
val newPhotoMessage = waitPhotoMessage().filter { potentialPhotoMessage ->
potentialPhotoMessage.sameChat(commandMessage)
}.first()
val draftMessagesChannel = Channel<String>(capacity = 1)
launchLoggingDropExceptions {
sendMessageDraftFlowWithTexts(commandMessage.chat.id, draftMessagesChannel.consumeAsFlow())
}.invokeOnCompletion {
draftMessagesChannel.close(it)
}
draftMessagesChannel.send("Start downloading photo")
val photoFile = downloadFileToTemp(newPhotoMessage.content)
draftMessagesChannel.send("Photo file have been downloaded. Start set my profile photo")
setMyProfilePhoto(
InputProfilePhoto.Static(
photoFile.asMultipartFile()
)
)
reply(commandMessage, "New photo have been set")
}
onCommand("removeMyProfilePhoto") {
runCatchingLogging {
removeMyProfilePhoto()
reply(it, "Photo have been removed")
}.onFailure { e ->
e.printStackTrace()
reply(it, "Something web wrong. See logs for details.")
}
}
}.second.join()
}

57
PollsBot/README.md Normal file
View File

@@ -0,0 +1,57 @@
# 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.
## Commands
| 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. |
`/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.
## Poll lifecycle and triggers
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.
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.
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.
## Setup and permissions
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.
## Run
The intended command from the repository root is:
```bash
./gradlew :PollsBot: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.

View File

@@ -0,0 +1,406 @@
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.setMyCommands
import dev.inmo.tgbotapi.extensions.api.send.polls.sendQuizPoll
import dev.inmo.tgbotapi.extensions.api.send.polls.sendRegularPoll
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
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.onPollAnswer
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOptionAdded
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
import dev.inmo.tgbotapi.utils.underline
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
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.
*/
suspend fun main(vararg args: String) {
val botToken = args.first()
val isDebug = args.any { it == "debug" }
if (isDebug) {
setDefaultKSLog(
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
println(defaultMessageFormatter(level, tag, message, throwable))
}
)
}
telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) {
val pollToChat = mutableMapOf<PollId, IdChatIdentifier>()
val pollToChatMutex = Mutex()
onCommand("anonymous", requireOnlyCommandInMessage = false) {
val customEmoji = it.content.parseCommandsWithArgsSources()
.toList()
.firstOrNull { it.first.command == "anonymous" }
?.second
?.firstNotNullOfOrNull { it.customEmojiTextSourceOrNull() }
val sentPoll = sendRegularPoll(
it.chat.id,
buildEntities {
regular("Test regular anonymous poll")
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
},
(1 .. 10).map {
InputPollOption {
regular(it.toString()) + " "
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
}
},
isAnonymous = true,
replyParameters = ReplyParameters(it)
)
pollToChatMutex.withLock {
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
}
}
onCommand("public", requireOnlyCommandInMessage = false) {
val customEmoji = it.content.parseCommandsWithArgsSources()
.toList()
.firstOrNull { it.first.command == "public" }
?.second
?.firstNotNullOfOrNull { it.customEmojiTextSourceOrNull() }
val sentPoll = sendRegularPoll(
it.chat.id,
buildEntities {
regular("Test regular non anonymous poll")
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
},
(1 .. 10).map {
InputPollOption {
regular(it.toString()) + " "
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
}
},
isAnonymous = false,
replyParameters = ReplyParameters(it),
allowAddingOptions = true,
hideResultsUntilCloses = true,
)
pollToChatMutex.withLock {
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
}
}
onCommand("quiz", requireOnlyCommandInMessage = false) {
val customEmoji = it.content.parseCommandsWithArgsSources()
.toList()
.firstOrNull { it.first.command == "quiz" }
?.second
?.firstNotNullOfOrNull { it.customEmojiTextSourceOrNull() }
val correctAnswer = mutableListOf<Int>()
(1 until Random.nextInt(9)).forEach {
val option = Random.nextInt(10)
if (correctAnswer.contains(option)) return@forEach
correctAnswer.add(option)
}
val sentPoll = sendQuizPoll(
it.chat.id,
questionEntities = buildEntities {
regular("Test quiz poll")
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
},
descriptionTextSources = buildEntities {
regular("Test quiz poll description:")
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
},
options = (1 .. 10).map {
InputPollOption {
regular(it.toString()) + " "
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
}
},
isAnonymous = false,
replyParameters = ReplyParameters(it),
correctOptionIds = correctAnswer.sorted(),
allowsMultipleAnswers = correctAnswer.size > 1,
allowsRevoting = true,
shuffleOptions = true,
hideResultsUntilCloses = true,
explanationTextSources = buildEntities {
regular("Random solved it to be ") + underline((correctAnswer + 1).toString()) + " "
if (customEmoji != null) {
customEmoji(customEmoji.customEmojiId, customEmoji.subsources)
}
}
)
println("Sent poll data: $sentPoll")
pollToChatMutex.withLock {
pollToChat[sentPoll.content.poll.id] = sentPoll.chat.id
}
}
// 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
when(it) {
is PollAnswer.Public -> send(chatId, "[onPollAnswer] User ${it.user} have answered")
is PollAnswer.Anonymous -> send(chatId, "[onPollAnswer] Chat ${it.voterChat} have answered")
}
}
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}")
}
}
send(chatId, pollInfo)
}
onPollOptionAdded {
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { pollMessage ->
reply(pollMessage) {
+"Poll option added: \n"
+it.chatEvent.optionTextSources
}
}
}
onPollOptionDeleted {
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { pollMessage ->
reply(pollMessage) {
+"Poll option deleted: \n"
+it.chatEvent.optionTextSources
}
}
}
onContentMessage {
val replyPollOptionId = it.replyInfo ?.pollOptionId ?: return@onContentMessage
it.replyTo ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { replied ->
reply(replied, pollOptionId = replyPollOptionId) {
+"Reply to poll option"
}
}
}
setMyCommands(
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)
}
}.second.join()
}

View File

@@ -1,32 +1,69 @@
# TelegramBotAPI-examples # TelegramBotAPI examples
This repository contains several examples of simple bots which are using TelegramBotAPI Runnable examples for [TelegramBotAPI](https://github.com/InsanusMokrassar/TelegramBotAPI), currently targeting tgbotapi 37.0.0 and Telegram Bot API 10.3. Each module focuses on a small Telegram Bot API feature and has its own README with detailed behavior, setup, permissions, and optional arguments.
## How to use this repository ## Running an example
This repository contains several important things: 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.
* Example subprojects 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`.
* Commits
* Structure
### Example subproject Native targets on Linux require libcurl development files, for example:
Each example subproject contains information about how to run this example and what is it ```bash
doing. Usually, it is some simple thing like sending "hello" message to the user which sudo apt install libcurl4-gnutls-dev
wrote to the bot. ```
### Commits ## Modules
Commits can contains some things like migration onto new version (especially it is actual | Module | What it demonstrates | Launch shortcut |
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 | [BoostsInfoBot](BoostsInfoBot/) | Requests a channel, lists the requesting user's boosts, and logs boost updates. | `./gradlew :BoostsInfoBot:run --args="<BOT_TOKEN> debug"` |
between library version | [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 chat/community membership and user-from-community join events. | `./gradlew :CommunitiesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [CustomBot](CustomBot/) | Provides a diagnostics playground for contexts, request logging, profile audio, and Stars balance. | `./gradlew :CustomBot:run --args="<BOT_TOKEN> debug testServer"` |
| [DeepLinksBot](DeepLinksBot/) | Generates bot deep links and consumes their start payloads. | `./gradlew :DeepLinksBot:run --args="<BOT_TOKEN>"` |
| [DraftsBot](DraftsBot/) | Streams empty or stoppable message drafts and handles generation-stopped updates. | `./gradlew :DraftsBot:run --args="<BOT_TOKEN>"` |
| [EphemeralMessagesBot](EphemeralMessagesBot/) | Sends, replaces, and edits rich, photo, or Live Photo ephemeral messages. | `./gradlew :EphemeralMessagesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [FSMBot](FSMBot/) | Implements a conversational finite-state machine with chat-scoped in-memory state. | `./gradlew :FSMBot:run --args="<BOT_TOKEN>"` |
| [FilesLoaderBot](FilesLoaderBot/) | Downloads incoming media to disk and sends it back to the chat. | `./gradlew :FilesLoaderBot:run --args="<BOT_TOKEN> <OUTPUT_DIRECTORY>"` |
| [ForwardInfoSenderBot](ForwardInfoSenderBot/) | Reports the forward-origin metadata of received content. | `./gradlew :ForwardInfoSenderBot:run --args="<BOT_TOKEN>"` |
| [GiftsBot](GiftsBot/) | Lists owned gifts and renders unique-gift service-message metadata. | `./gradlew :GiftsBot:run --args="<BOT_TOKEN> debug testServer"` |
| [GiveawaysBot](GiveawaysBot/) | Logs giveaway creation, completion, and winner events. | `./gradlew :GiveawaysBot:run --args="<BOT_TOKEN> debug testServer"` |
| [GuestQueryBot](GuestQueryBot/) | Handles guest queries in chats where the bot is not a member. | `./gradlew :GuestQueryBot:run --args="<BOT_TOKEN> debug testServer"` |
| [HelloBot](HelloBot/) | Greets users, groups, channels, or business chats when mentioned. | `./gradlew :HelloBot:run --args="<BOT_TOKEN>"` |
| [InlineQueriesBot](InlineQueriesBot/) | Answers inline queries and supplies a deep-link result. | `./gradlew :InlineQueriesBot:runJvm --args="<BOT_TOKEN>"` |
| [JoinRequestQueriesBot](JoinRequestQueriesBot/) | Queues or approves join requests, optionally delegating the decision to a Web App. | `./gradlew :JoinRequestQueriesBot:run --args="<BOT_TOKEN> https://example.com/verify debug testServer"` |
| [KeyboardsBot](KeyboardsBot/) | Demonstrates reply, inline, disabled, forced-reply, paged, and inline-mode keyboards. | `./gradlew :KeyboardsBot:jvm_launcher:run --args="<BOT_TOKEN> debug"` |
| [LinkPreviewsBot](LinkPreviewsBot/) | Sends the same link using multiple link-preview configurations. | `./gradlew :LinkPreviewsBot:run --args="<BOT_TOKEN> debug"` |
| [LiveLocationsBot](LiveLocationsBot/) | Sends, updates, cancels, and stops a live-location message. | `./gradlew :LiveLocationsBot:run --args="<BOT_TOKEN>"` |
| [LivePhotosBot](LivePhotosBot/) | Receives, uploads, groups, edits, and sells Telegram Live Photos. | `./gradlew :LivePhotosBot:run --args="<BOT_TOKEN> debug testServer"` |
| [ManagedBotsBot](ManagedBotsBot/) † | Creates and administers managed bots and inspects personal-channel messages. | `./gradlew :ManagedBotsBot:run --args="<BOT_TOKEN> debug testServer"` |
| [MemberUpdatedWatcherBot](MemberUpdatedWatcherBot/) | Logs and reports bot/member status transitions in chats. | `./gradlew :MemberUpdatedWatcherBot:run --args="<BOT_TOKEN> debug"` |
| [MyBot](MyBot/) † | Replaces or removes the bot's global profile photo and prints diagnostics. | `./gradlew :MyBot:run --args="<BOT_TOKEN> debug testServer"` |
| [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 markup/blocks, buttons, documents, drafts, queries, and media. | `./gradlew :RichMessagesBot:run --args="<BOT_TOKEN> debug testServer"` |
| [RightsChangerBot](RightsChangerBot/) | Uses an FSM and inline keyboards to change member and administrator rights, including welcome messages. | `./gradlew :RightsChangerBot:run --args="<BOT_TOKEN> <ALLOWED_USER_ID> debug"` |
| [SlotMachineDetectorBot](SlotMachineDetectorBot/) | Detects slot-machine dice and decodes their reel values. | `./gradlew :SlotMachineDetectorBot:run --args="<BOT_TOKEN>"` |
| [StarTransactionsBot](StarTransactionsBot/) | Demonstrates Stars invoices, transaction history, paid media, and refunds. | `./gradlew :StarTransactionsBot:run --args="<BOT_TOKEN> <ADMIN_USER_ID> debug testServer"` |
| [StickerInfoBot](StickerInfoBot/) † | Looks up sticker-set metadata for stickers and custom emoji. | `./gradlew :StickerInfoBot:jvm_launcher:run --args="<BOT_TOKEN>"` |
| [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"` |
### Structure † 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.
Structure of this repository fully representative (it is the reason why this repo ## Repository as a reference
contains multiplatform subprojects) and you can use it as some template (but I am strongly
recommend you to use my 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).
[TelegramBot template](https://github.com/InsanusMokrassar/TelegramBotAPI-bot_template) or
[Multiplatform Project template](https://github.com/InsanusMokrassar/KotlinMultiplatformProjectTemplate))

View File

@@ -1,9 +1,76 @@
# RandomFileSenderBot # RandomFileSenderBot
This bot will send random file from input folder OR from bot working folder 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.
## Launch ## Behavior
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.
## Arguments
Both launchers interpret arguments in the same order:
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.
Additional arguments are ignored.
## Launch from the repository root
### JVM
```bash ```bash
../gradlew run --args="BOT_TOKEN[ optional/folder/path]" ./gradlew :RandomFileSenderBot:runJvm --args="<BOT_TOKEN> /absolute/path/to/files"
``` ```
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:
```bash
./gradlew :RandomFileSenderBot:linkDebugExecutableNative
./RandomFileSenderBot/build/bin/native/debugExecutable/RandomFileSenderBot.kexe "<BOT_TOKEN>" "/absolute/path/to/files"
```
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.

View File

@@ -8,14 +8,29 @@ buildscript {
} }
} }
apply plugin: 'kotlin' plugins {
apply plugin: 'application' id "org.jetbrains.kotlin.multiplatform"
mainClassName="RandomFileSenderBotKt"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
} }
kotlin {
jvm {
binaries {
executable {
mainClass.set("RandomFileSenderBotKt")
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api "dev.inmo:tgbotapi:$telegram_bot_api_version"
}
}
}
}
apply from: "$nativePartTemplate"

View File

@@ -1,5 +1,5 @@
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filesize import dev.inmo.micro_utils.common.filesize
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
@@ -7,6 +7,7 @@ import dev.inmo.tgbotapi.extensions.api.send.media.sendDocument
import dev.inmo.tgbotapi.extensions.api.send.media.sendDocumentsGroup import dev.inmo.tgbotapi.extensions.api.send.media.sendDocumentsGroup
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.withUploadDocumentAction import dev.inmo.tgbotapi.extensions.api.send.withUploadDocumentAction
import dev.inmo.tgbotapi.extensions.api.telegramBot
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommandWithArgs import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommandWithArgs
import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
@@ -14,30 +15,26 @@ import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.chat.Chat import dev.inmo.tgbotapi.types.chat.Chat
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
import dev.inmo.tgbotapi.types.mediaCountInMediaGroup import dev.inmo.tgbotapi.types.mediaCountInMediaGroup
import java.io.File
private const val command = "send_file" private const val command = "send_file"
/** /**
* This bot will send files inside of working directory OR from directory in the second argument. * Selects a file by recursively choosing random children below [currentRoot].
* 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, * @return the selected file, or `null` when the picker cannot continue from the current root
* /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 main(args: Array<String>) { expect fun pickFile(currentRoot: MPPFile): MPPFile?
val botToken = args.first()
val directoryOrFile = args.getOrNull(1) ?.let { File(it) } ?: File("")
fun pickFile(currentRoot: File = directoryOrFile): File? { /**
if (currentRoot.isFile) { * Runs the long-polling random-file bot using [token] and serving selections rooted at [folder].
return currentRoot *
} else { * `/send_file` selects one non-empty file, while `/send_file N` selects `N` files and splits them into valid Telegram
return pickFile(currentRoot.listFiles() ?.takeIf { it.isNotEmpty() } ?.random() ?: return null) * media-group sizes.
} */
} suspend fun doRandomFileSenderBot(token: String, folder: MPPFile) {
val bot = telegramBot(token)
suspend fun TelegramBot.sendFiles(chat: Chat, files: List<File>) { suspend fun TelegramBot.sendFiles(chat: Chat, files: List<MPPFile>) {
when (files.size) { when (files.size) {
1 -> sendDocument( 1 -> sendDocument(
chat.id, chat.id,
@@ -52,8 +49,6 @@ suspend fun main(args: Array<String>) {
} }
} }
val bot = telegramBot(botToken)
bot.buildBehaviourWithLongPolling (defaultExceptionsHandler = { it.printStackTrace() }) { bot.buildBehaviourWithLongPolling (defaultExceptionsHandler = { it.printStackTrace() }) {
onCommandWithArgs(command) { message, args -> onCommandWithArgs(command) { message, args ->
@@ -62,10 +57,10 @@ suspend fun main(args: Array<String>) {
var sent = false var sent = false
var left = count var left = count
val chosen = mutableListOf<File>() val chosen = mutableListOf<MPPFile>()
while (left > 0) { while (left > 0) {
val picked = pickFile() ?.takeIf { it.filesize > 0 } ?: continue val picked = pickFile(folder) ?.takeIf { it.filesize > 0 } ?: continue
chosen.add(picked) chosen.add(picked)
left-- left--
if (chosen.size >= mediaCountInMediaGroup.last) { if (chosen.size >= mediaCountInMediaGroup.last) {
@@ -81,7 +76,7 @@ suspend fun main(args: Array<String>) {
} }
if (!sent) { if (!sent) {
bot.reply(message, "Nothing selected :(") reply(message, "Nothing selected :(")
} }
} }
} }

View File

@@ -0,0 +1,11 @@
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
} else {
return pickFile(currentRoot.listFiles() ?.takeIf { it.isNotEmpty() } ?.random() ?: return null)
}
}

View File

@@ -0,0 +1,6 @@
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) ?: ""))
}

View File

@@ -0,0 +1,11 @@
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
} else {
return pickFile(FileSystem.SYSTEM.list(currentRoot).takeIf { it.isNotEmpty() } ?.random() ?: return null)
}
}

View File

@@ -0,0 +1,9 @@
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())
}
}

Some files were not shown because too many files have changed in this diff Show More