Skip to content

Commit 52b6df8

Browse files
Merge pull request #362 from InsanusMokrassar/34.0.0
34.0.0
2 parents 89e1eec + 8a024a3 commit 52b6df8

25 files changed

Lines changed: 881 additions & 67 deletions

File tree

BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import dev.inmo.kslog.common.LogLevel
33
import dev.inmo.kslog.common.defaultMessageFormatter
44
import dev.inmo.kslog.common.setDefaultKSLog
55
import dev.inmo.micro_utils.common.Percentage
6+
import dev.inmo.tgbotapi.types.chat.PreviewBot
67
import dev.inmo.tgbotapi.extensions.api.answers.answer
78
import dev.inmo.tgbotapi.extensions.api.bot.getMe
89
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountStarBalance
@@ -27,7 +28,8 @@ import dev.inmo.tgbotapi.extensions.api.stories.deleteStory
2728
import dev.inmo.tgbotapi.extensions.api.stories.postStory
2829
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
2930
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
30-
import dev.inmo.tgbotapi.extensions.utils.commonMessageOrNull
31+
import dev.inmo.tgbotapi.extensions.utils.chatContentMessageOrNull
32+
import dev.inmo.tgbotapi.extensions.utils.chatMessageOrNull
3133
import dev.inmo.tgbotapi.extensions.utils.extendedPrivateChatOrThrow
3234
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
3335
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
@@ -44,13 +46,15 @@ import dev.inmo.tgbotapi.types.MessageId
4446
import dev.inmo.tgbotapi.types.RawChatId
4547
import dev.inmo.tgbotapi.types.business_connection.BusinessConnectionId
4648
import dev.inmo.tgbotapi.types.chat.PrivateChat
47-
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
49+
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
50+
import dev.inmo.tgbotapi.types.message.content.LivePhotoContent
4851
import dev.inmo.tgbotapi.types.message.content.PhotoContent
4952
import dev.inmo.tgbotapi.types.message.content.StoryContent
5053
import dev.inmo.tgbotapi.types.message.content.TextContent
5154
import dev.inmo.tgbotapi.types.message.content.VideoContent
5255
import dev.inmo.tgbotapi.types.message.content.VisualMediaGroupPartContent
5356
import dev.inmo.tgbotapi.types.stories.InputStoryContent
57+
import dev.inmo.tgbotapi.types.stories.InputStoryContent.*
5458
import dev.inmo.tgbotapi.types.stories.StoryArea
5559
import dev.inmo.tgbotapi.types.stories.StoryAreaPosition
5660
import dev.inmo.tgbotapi.types.stories.StoryAreaType
@@ -120,6 +124,15 @@ suspend fun main(args: Array<String>) {
120124
if (businessContentMessage.sentByBusinessConnectionOwner) {
121125
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
122126
} else {
127+
// Since TG Bot API 9.0: business bots can reply to other bots in business context
128+
// when bot-to-bot communication is enabled for both bots
129+
if (businessContentMessage.from is PreviewBot) {
130+
reply(
131+
to = sent,
132+
text = "Replying to bot ${businessContentMessage.from.firstName} in business context (bot-to-bot reply)",
133+
)
134+
return@ifBusinessContentMessage
135+
}
123136
reply(
124137
to = sent,
125138
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
@@ -203,6 +216,8 @@ suspend fun main(args: Array<String>) {
203216
}
204217
)
205218
}
219+
// Since TG Bot API 9.0: the following account management commands no longer require
220+
// the connected user to have a Telegram Premium subscription.
206221
onCommandWithArgs("set_business_account_name", initialFilter = { it.chat is PrivateChat }) { it, args ->
207222
val firstName = args[0]
208223
val secondName = args.getOrNull(1)
@@ -349,9 +364,9 @@ suspend fun main(args: Array<String>) {
349364
}
350365
}
351366
}
352-
suspend fun handleSetProfilePhoto(it: CommonMessage<TextContent>, isPublic: Boolean) {
367+
suspend fun handleSetProfilePhoto(it: ChatContentMessage<TextContent>, isPublic: Boolean) {
353368
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
354-
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>()
369+
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<PhotoContent>()
355370
if (replyTo == null) {
356371
reply(it) {
357372
+"Reply to photo for using of this command"
@@ -411,7 +426,7 @@ suspend fun main(args: Array<String>) {
411426

412427
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
413428
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
414-
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
429+
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
415430
if (replyTo == null) {
416431
reply(it) {
417432
+"Reply to photo or video for using of this command"
@@ -424,12 +439,16 @@ suspend fun main(args: Array<String>) {
424439
postStory(
425440
businessConnectionId,
426441
when (replyTo.content) {
427-
is PhotoContent -> InputStoryContent.Photo(
442+
is PhotoContent -> Photo(
428443
file.multipartFile()
429444
)
430-
is VideoContent -> InputStoryContent.Video(
445+
is VideoContent -> Video(
431446
file.multipartFile()
432447
)
448+
is LivePhotoContent -> Video(
449+
file.multipartFile(),
450+
isAnimation = true
451+
)
433452
},
434453
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
435454
areas = listOf(
@@ -465,7 +484,7 @@ suspend fun main(args: Array<String>) {
465484

466485
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
467486
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
468-
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<StoryContent>()
487+
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<StoryContent>()
469488
if (replyTo == null) {
470489
reply(it) {
471490
+"Reply to photo or video for using of this command"

ChatManagementBot/build.gradle

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
buildscript {
2+
repositories {
3+
mavenCentral()
4+
}
5+
6+
dependencies {
7+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
8+
}
9+
}
10+
11+
apply plugin: 'kotlin'
12+
apply plugin: 'application'
13+
14+
mainClassName="ChatManagementBotKt"
15+
16+
17+
dependencies {
18+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
19+
20+
implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
21+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import dev.inmo.kslog.common.KSLog
2+
import dev.inmo.kslog.common.LogLevel
3+
import dev.inmo.kslog.common.defaultMessageFormatter
4+
import dev.inmo.kslog.common.setDefaultKSLog
5+
import dev.inmo.tgbotapi.extensions.api.bot.getMe
6+
import dev.inmo.tgbotapi.extensions.api.chat.get.getChatAdministrators
7+
import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember
8+
import dev.inmo.tgbotapi.extensions.api.send.deleteAllUserMessageReactions
9+
import dev.inmo.tgbotapi.extensions.api.send.deleteUserMessageReaction
10+
import dev.inmo.tgbotapi.extensions.api.send.reply
11+
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.chatMemberGotRestrictedFilter
12+
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.chatMemberGotRestrictionsChangedFilter
13+
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
14+
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatMemberUpdated
15+
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
16+
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onContentMessage
17+
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.plus
18+
import dev.inmo.tgbotapi.extensions.utils.fromUserChatMessageOrNull
19+
import dev.inmo.tgbotapi.extensions.utils.fromUserMessageOrNull
20+
import dev.inmo.tgbotapi.extensions.utils.publicChatOrNull
21+
import dev.inmo.tgbotapi.extensions.utils.requireRestrictedChatMember
22+
import dev.inmo.tgbotapi.extensions.utils.restrictedMemberChatMemberOrNull
23+
import dev.inmo.tgbotapi.extensions.utils.specialRightsChatMemberOrNull
24+
import dev.inmo.tgbotapi.types.chat.CommonBot
25+
import dev.inmo.tgbotapi.types.chat.ChatPermissions
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Dispatchers
28+
29+
/**
30+
* This bot demonstrates Chat Management API features added in Bot API 9.x:
31+
*
32+
* 1. `can_react_to_messages` field in `ChatMemberRestricted` — printed when a member's
33+
* restrictions are changed (requires the bot to be an admin in the group).
34+
* `RestrictedMemberChatMember` also implements `ChatPermissions`, so the same field
35+
* covers both `ChatMemberRestricted` and `ChatPermissions` from the spec.
36+
*
37+
* 2. `return_bots` in `getChatAdministrators` — `/admins` command lists all admins
38+
* including other bots (retrieveOtherBots = true).
39+
*
40+
* 3. `deleteAllMessageReactions` — `/deleteallreactions` in reply to a message removes
41+
* all reactions that the replied message's author has left across the entire chat.
42+
*
43+
* 4. `deleteMessageReaction` — `/deletereaction` in reply to a message removes the
44+
* reaction the replied message's author placed on that specific message.
45+
*
46+
* 5. Seeing messages from other bots in groups — demonstrated via `canReadAllGroupMessages`
47+
* from `getMe()`. When true (privacy mode off), the bot receives messages from other bots.
48+
* All such messages are logged.
49+
*
50+
* Usage: pass the bot token as the first argument. Optional: `debug`, `testServer`.
51+
*/
52+
suspend fun main(vararg args: String) {
53+
val botToken = args.first()
54+
val isDebug = args.any { it == "debug" }
55+
val isTestServer = args.any { it == "testServer" }
56+
57+
if (isDebug) {
58+
setDefaultKSLog(
59+
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
60+
println(defaultMessageFormatter(level, tag, message, throwable))
61+
}
62+
)
63+
}
64+
65+
telegramBotWithBehaviourAndLongPolling(
66+
botToken,
67+
CoroutineScope(Dispatchers.IO),
68+
testServer = isTestServer
69+
) {
70+
val me = getMe()
71+
println("Bot: ${me.firstName} (@${me.username?.username})")
72+
73+
// Feature 5: canReadAllGroupMessages (can_read_all_group_messages) from getMe()
74+
// When true, the bot receives messages from other bots in groups (privacy mode off)
75+
println("canReadAllGroupMessages: ${me.canReadAllGroupMessages}")
76+
77+
// Feature 1: can_react_to_messages in ChatMemberRestricted and ChatPermissions
78+
// RestrictedMemberChatMember implements ChatPermissions, so canReactToMessages
79+
// appears in both types as required by the Telegram Bot API spec
80+
onChatMemberUpdated(
81+
initialFilter = chatMemberGotRestrictedFilter + chatMemberGotRestrictionsChangedFilter
82+
) { update ->
83+
val restricted = update.newChatMemberState.restrictedMemberChatMemberOrNull()
84+
?: return@onChatMemberUpdated
85+
println("Restriction update for ${update.member.firstName}:")
86+
// canReactToMessages as ChatMemberRestricted field
87+
println(" canReactToMessages (ChatMemberRestricted): ${restricted.canReactToMessages}")
88+
// same field via ChatPermissions — RestrictedMemberChatMember : ChatPermissions
89+
val permissions: ChatPermissions = restricted
90+
println(" canReactToMessages (ChatPermissions): ${permissions.canReactToMessages}")
91+
}
92+
93+
// Feature 1: can_react_to_messages in ChatMemberRestricted and ChatPermissions
94+
// RestrictedMemberChatMember implements ChatPermissions, so canReactToMessages
95+
// appears in both types as required by the Telegram Bot API spec
96+
onCommand(
97+
"retrieveRights"
98+
) { message ->
99+
val replyMessage = message.replyTo ?.fromUserChatMessageOrNull() ?: run {
100+
reply(message) { +"This command works only in groups/supergroups/channels" }
101+
return@onCommand
102+
}
103+
val chatMember = getChatMember(message.chat.id, replyMessage.user.id)
104+
val chatPermissions = chatMember.restrictedMemberChatMemberOrNull()
105+
106+
val canReactToMessages = chatPermissions ?.canReactToMessages
107+
reply(message) { +"Can react to messages: $canReactToMessages" }
108+
}
109+
110+
// Feature 2: return_bots parameter in getChatAdministrators
111+
// retrieveOtherBots = true corresponds to return_bots = true in the Telegram API
112+
onCommand("admins") { message ->
113+
val chat = message.chat.publicChatOrNull() ?: run {
114+
reply(message) { +"This command works only in groups/supergroups/channels" }
115+
return@onCommand
116+
}
117+
val admins = getChatAdministrators(chat, retrieveOtherBots = true)
118+
reply(message) {
119+
+"Administrators (retrieveOtherBots=true, includes bots):\n"
120+
admins.forEach { admin ->
121+
val kind = if (admin.user is CommonBot) "bot" else "user"
122+
+"${admin.user.firstName} [$kind]\n"
123+
}
124+
}
125+
}
126+
127+
// Feature 4: deleteMessageReaction
128+
// Deletes a specific reaction by the replied message's author on that message
129+
onCommand("deleteReaction") { message ->
130+
val replied = message.replyTo ?.fromUserChatMessageOrNull() ?: run {
131+
reply(message) { +"Reply to a message to remove that user's reaction from it" }
132+
return@onCommand
133+
}
134+
deleteUserMessageReaction(replied, replied.user.id)
135+
reply(message) { +"Deleted reaction by ${replied.user.firstName} on the replied message" }
136+
}
137+
138+
// Feature 3: deleteAllMessageReactions
139+
// Deletes all reactions that the replied message's author has left in this chat
140+
onCommand("deleteAllReactions") { message ->
141+
val replied = message.replyTo?.fromUserMessageOrNull() ?: run {
142+
reply(message) { +"Reply to a message to clear all reactions of that user in this chat" }
143+
return@onCommand
144+
}
145+
deleteAllUserMessageReactions(message.chat, replied.user.id)
146+
reply(message) { +"Deleted all reactions by ${replied.user.firstName} in this chat" }
147+
}
148+
149+
// Feature 5: messages from other bots in groups
150+
// Bots with canReadAllGroupMessages=true (privacy mode off) receive messages from other bots.
151+
// This handler logs all such messages to demonstrate the feature.
152+
onContentMessage(
153+
initialFilter = { msg ->
154+
val user = msg.fromUserMessageOrNull()?.user
155+
user is CommonBot && user.id != me.id
156+
}
157+
) { message ->
158+
val sender = message.fromUserMessageOrNull()?.user
159+
println("Message from other bot received (canReadAllGroupMessages=${me.canReadAllGroupMessages}):")
160+
println(" sender: ${sender?.firstName} (@${(sender as? CommonBot)?.username?.username})")
161+
println(" content: ${message.content}")
162+
}
163+
}.second.join()
164+
}

ChecklistsBot/src/main/kotlin/ChecklistsBot.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import dev.inmo.kslog.common.defaultMessageFormatter
44
import dev.inmo.kslog.common.setDefaultKSLog
55
import dev.inmo.micro_utils.coroutines.runCatchingLogging
66
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
7-
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
87
import dev.inmo.tgbotapi.extensions.api.bot.getMe
98
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
109
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
@@ -33,7 +32,7 @@ import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
3332
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
3433
import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
3534
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
36-
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
35+
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
3736
import dev.inmo.tgbotapi.types.message.content.ChecklistContent
3837
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
3938
import dev.inmo.tgbotapi.types.update.abstracts.Update

CustomBot/src/main/kotlin/CustomBot.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import dev.inmo.kslog.common.KSLog
22
import dev.inmo.kslog.common.LogLevel
33
import dev.inmo.kslog.common.defaultMessageFormatter
44
import dev.inmo.kslog.common.setDefaultKSLog
5-
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
5+
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
66
import dev.inmo.tgbotapi.extensions.api.bot.getMe
77
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
88
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
@@ -22,7 +22,7 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
2222
import dev.inmo.tgbotapi.types.media.AudioMediaGroupMemberTelegramMedia
2323
import dev.inmo.tgbotapi.types.media.toTelegramMediaAudio
2424
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaPhoto
25-
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
25+
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
2626
import dev.inmo.tgbotapi.types.update.abstracts.Update
2727
import kotlinx.coroutines.CoroutineScope
2828
import kotlinx.coroutines.Dispatchers
@@ -31,8 +31,8 @@ private var BehaviourContextData.update: Update?
3131
get() = get("update") as? Update
3232
set(value) = set("update", value)
3333

34-
private var BehaviourContextData.commonMessage: CommonMessage<*>?
35-
get() = get("commonMessage") as? CommonMessage<*>
34+
private var BehaviourContextData.commonMessage: ChatContentMessage<*>?
35+
get() = get("commonMessage") as? ChatContentMessage<*>
3636
set(value) = set("commonMessage", value)
3737

3838
/**
@@ -129,7 +129,7 @@ suspend fun main(vararg args: String) {
129129
println(it.chatEvent)
130130
}
131131

132-
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
132+
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
133133
println(it)
134134
}
135135
}.second.join()

DeepLinksBot/src/main/kotlin/DeepLinksBot.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
1+
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
22
import dev.inmo.tgbotapi.extensions.api.bot.getMe
33
import dev.inmo.tgbotapi.extensions.api.send.reply
44
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
@@ -36,7 +36,7 @@ suspend fun main(vararg args: String) {
3636
onDeepLink { (it, deepLink) ->
3737
reply(it, "Ok, I got deep link \"${deepLink}\" in trigger")
3838
}
39-
waitDeepLinks().subscribeSafelyWithoutExceptions(this) { (it, deepLink) ->
39+
waitDeepLinks().subscribeLoggingDropExceptions(this) { (it, deepLink) ->
4040
reply(it, "Ok, I got deep link \"${deepLink}\" in waiter")
4141
println(triggersHolder.handleableCommandsHolder.handleable)
4242
}

0 commit comments

Comments
 (0)