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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.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
Expand All @@ -27,7 +28,8 @@ 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.commonMessageOrNull
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
Expand All @@ -44,13 +46,15 @@ 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.CommonMessage
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
Expand Down Expand Up @@ -120,6 +124,15 @@ suspend fun main(args: Array<String>) {
if (businessContentMessage.sentByBusinessConnectionOwner) {
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
} else {
// Since TG Bot API 9.0: business bots can reply to other bots in business context
// when bot-to-bot communication is enabled for both bots
if (businessContentMessage.from is PreviewBot) {
reply(
to = sent,
text = "Replying to bot ${businessContentMessage.from.firstName} in business context (bot-to-bot reply)",
)
return@ifBusinessContentMessage
}
reply(
to = sent,
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
Expand Down Expand Up @@ -203,6 +216,8 @@ suspend fun main(args: Array<String>) {
}
)
}
// Since TG Bot API 9.0: the following account management commands no longer require
// the connected user to have a Telegram Premium subscription.
onCommandWithArgs("set_business_account_name", initialFilter = { it.chat is PrivateChat }) { it, args ->
val firstName = args[0]
val secondName = args.getOrNull(1)
Expand Down Expand Up @@ -349,9 +364,9 @@ suspend fun main(args: Array<String>) {
}
}
}
suspend fun handleSetProfilePhoto(it: CommonMessage<TextContent>, isPublic: Boolean) {
suspend fun handleSetProfilePhoto(it: ChatContentMessage<TextContent>, isPublic: Boolean) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>()
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<PhotoContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo for using of this command"
Expand Down Expand Up @@ -411,7 +426,7 @@ suspend fun main(args: Array<String>) {

onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<VisualMediaGroupPartContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo or video for using of this command"
Expand All @@ -424,12 +439,16 @@ suspend fun main(args: Array<String>) {
postStory(
businessConnectionId,
when (replyTo.content) {
is PhotoContent -> InputStoryContent.Photo(
is PhotoContent -> Photo(
file.multipartFile()
)
is VideoContent -> InputStoryContent.Video(
is VideoContent -> Video(
file.multipartFile()
)
is LivePhotoContent -> Video(
file.multipartFile(),
isAnimation = true
)
},
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
areas = listOf(
Expand Down Expand Up @@ -465,7 +484,7 @@ suspend fun main(args: Array<String>) {

onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<StoryContent>()
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<StoryContent>()
if (replyTo == null) {
reply(it) {
+"Reply to photo or video for using of this command"
Expand Down
21 changes: 21 additions & 0 deletions ChatManagementBot/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
buildscript {
repositories {
mavenCentral()
}

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

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

mainClassName="ChatManagementBotKt"


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

implementation "dev.inmo:tgbotapi:$telegram_bot_api_version"
}
164 changes: 164 additions & 0 deletions ChatManagementBot/src/main/kotlin/ChatManagementBot.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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

/**
* This bot demonstrates Chat Management API features added in Bot API 9.x:
*
* 1. `can_react_to_messages` field in `ChatMemberRestricted` — printed when a member's
* restrictions are changed (requires the bot to be an admin in the group).
* `RestrictedMemberChatMember` also implements `ChatPermissions`, so the same field
* covers both `ChatMemberRestricted` and `ChatPermissions` from the spec.
*
* 2. `return_bots` in `getChatAdministrators` — `/admins` command lists all admins
* including other bots (retrieveOtherBots = true).
*
* 3. `deleteAllMessageReactions` — `/deleteallreactions` in reply to a message removes
* all reactions that the replied message's author has left across the entire chat.
*
* 4. `deleteMessageReaction` — `/deletereaction` in reply to a message removes the
* reaction the replied message's author placed on that specific message.
*
* 5. Seeing messages from other bots in groups — demonstrated via `canReadAllGroupMessages`
* from `getMe()`. When true (privacy mode off), the bot receives messages from other bots.
* All such messages are logged.
*
* Usage: pass the bot token as the first argument. Optional: `debug`, `testServer`.
*/
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})")

// Feature 5: canReadAllGroupMessages (can_read_all_group_messages) from getMe()
// When true, the bot receives messages from other bots in groups (privacy mode off)
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 all 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
// Bots with canReadAllGroupMessages=true (privacy mode off) receive messages from other bots.
// This handler logs all such messages to demonstrate the feature.
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()
}
3 changes: 1 addition & 2 deletions ChecklistsBot/src/main/kotlin/ChecklistsBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
Expand Down Expand Up @@ -33,7 +32,7 @@ import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
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
Expand Down
10 changes: 5 additions & 5 deletions CustomBot/src/main/kotlin/CustomBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.LogLevel
import dev.inmo.kslog.common.defaultMessageFormatter
import dev.inmo.kslog.common.setDefaultKSLog
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
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
Expand All @@ -22,7 +22,7 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
import dev.inmo.tgbotapi.types.media.AudioMediaGroupMemberTelegramMedia
import dev.inmo.tgbotapi.types.media.toTelegramMediaAudio
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaPhoto
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
import dev.inmo.tgbotapi.types.update.abstracts.Update
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -31,8 +31,8 @@ private var BehaviourContextData.update: Update?
get() = get("update") as? Update
set(value) = set("update", value)

private var BehaviourContextData.commonMessage: CommonMessage<*>?
get() = get("commonMessage") as? CommonMessage<*>
private var BehaviourContextData.commonMessage: ChatContentMessage<*>?
get() = get("commonMessage") as? ChatContentMessage<*>
set(value) = set("commonMessage", value)

/**
Expand Down Expand Up @@ -129,7 +129,7 @@ suspend fun main(vararg args: String) {
println(it.chatEvent)
}

allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
println(it)
}
}.second.join()
Expand Down
4 changes: 2 additions & 2 deletions DeepLinksBot/src/main/kotlin/DeepLinksBot.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
Expand Down Expand Up @@ -36,7 +36,7 @@ suspend fun main(vararg args: String) {
onDeepLink { (it, deepLink) ->
reply(it, "Ok, I got deep link \"${deepLink}\" in trigger")
}
waitDeepLinks().subscribeSafelyWithoutExceptions(this) { (it, deepLink) ->
waitDeepLinks().subscribeLoggingDropExceptions(this) { (it, deepLink) ->
reply(it, "Ok, I got deep link \"${deepLink}\" in waiter")
println(triggersHolder.handleableCommandsHolder.handleable)
}
Expand Down
Loading
Loading