mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI-examples.git
synced 2026-06-06 15:47:17 +00:00
buildable new bots
This commit is contained in:
@@ -3,6 +3,7 @@ import dev.inmo.kslog.common.LogLevel
|
|||||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
import dev.inmo.micro_utils.common.Percentage
|
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.answers.answer
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountStarBalance
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountStarBalance
|
||||||
@@ -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.api.stories.postStory
|
||||||
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.*
|
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.extendedPrivateChatOrThrow
|
||||||
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
|
import dev.inmo.tgbotapi.extensions.utils.ifAccessibleMessage
|
||||||
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
|
import dev.inmo.tgbotapi.extensions.utils.ifBusinessContentMessage
|
||||||
@@ -44,13 +46,15 @@ import dev.inmo.tgbotapi.types.MessageId
|
|||||||
import dev.inmo.tgbotapi.types.RawChatId
|
import dev.inmo.tgbotapi.types.RawChatId
|
||||||
import dev.inmo.tgbotapi.types.business_connection.BusinessConnectionId
|
import dev.inmo.tgbotapi.types.business_connection.BusinessConnectionId
|
||||||
import dev.inmo.tgbotapi.types.chat.PrivateChat
|
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.PhotoContent
|
||||||
import dev.inmo.tgbotapi.types.message.content.StoryContent
|
import dev.inmo.tgbotapi.types.message.content.StoryContent
|
||||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||||
import dev.inmo.tgbotapi.types.message.content.VideoContent
|
import dev.inmo.tgbotapi.types.message.content.VideoContent
|
||||||
import dev.inmo.tgbotapi.types.message.content.VisualMediaGroupPartContent
|
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.InputStoryContent.*
|
||||||
import dev.inmo.tgbotapi.types.stories.StoryArea
|
import dev.inmo.tgbotapi.types.stories.StoryArea
|
||||||
import dev.inmo.tgbotapi.types.stories.StoryAreaPosition
|
import dev.inmo.tgbotapi.types.stories.StoryAreaPosition
|
||||||
import dev.inmo.tgbotapi.types.stories.StoryAreaType
|
import dev.inmo.tgbotapi.types.stories.StoryAreaType
|
||||||
@@ -120,6 +124,15 @@ suspend fun main(args: Array<String>) {
|
|||||||
if (businessContentMessage.sentByBusinessConnectionOwner) {
|
if (businessContentMessage.sentByBusinessConnectionOwner) {
|
||||||
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
|
reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat")
|
||||||
} else {
|
} 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(
|
reply(
|
||||||
to = sent,
|
to = sent,
|
||||||
text = "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat",
|
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>) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 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 ->
|
onCommandWithArgs("set_business_account_name", initialFilter = { it.chat is PrivateChat }) { it, args ->
|
||||||
val firstName = args[0]
|
val firstName = args[0]
|
||||||
val secondName = args.getOrNull(1)
|
val secondName = args.getOrNull(1)
|
||||||
@@ -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 businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto
|
||||||
val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>()
|
val replyTo = it.replyTo ?.chatContentMessageOrNull() ?.withContentOrNull<PhotoContent>()
|
||||||
if (replyTo == null) {
|
if (replyTo == null) {
|
||||||
reply(it) {
|
reply(it) {
|
||||||
+"Reply to photo for using of this command"
|
+"Reply to photo for using of this command"
|
||||||
@@ -411,7 +426,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
|
|
||||||
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
|
onCommand("post_story", initialFilter = { it.chat is PrivateChat }) {
|
||||||
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
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) {
|
if (replyTo == null) {
|
||||||
reply(it) {
|
reply(it) {
|
||||||
+"Reply to photo or video for using of this command"
|
+"Reply to photo or video for using of this command"
|
||||||
@@ -424,12 +439,16 @@ suspend fun main(args: Array<String>) {
|
|||||||
postStory(
|
postStory(
|
||||||
businessConnectionId,
|
businessConnectionId,
|
||||||
when (replyTo.content) {
|
when (replyTo.content) {
|
||||||
is PhotoContent -> InputStoryContent.Photo(
|
is PhotoContent -> Photo(
|
||||||
file.multipartFile()
|
file.multipartFile()
|
||||||
)
|
)
|
||||||
is VideoContent -> InputStoryContent.Video(
|
is VideoContent -> Video(
|
||||||
file.multipartFile()
|
file.multipartFile()
|
||||||
)
|
)
|
||||||
|
is LivePhotoContent -> Video(
|
||||||
|
file.multipartFile(),
|
||||||
|
isAnimation = true
|
||||||
|
)
|
||||||
},
|
},
|
||||||
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
|
activePeriod = PostStory.ACTIVE_PERIOD_6_HOURS,
|
||||||
areas = listOf(
|
areas = listOf(
|
||||||
@@ -465,7 +484,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
|
|
||||||
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
|
onCommand("delete_story", initialFilter = { it.chat is PrivateChat }) {
|
||||||
val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@onCommand
|
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) {
|
if (replyTo == null) {
|
||||||
reply(it) {
|
reply(it) {
|
||||||
+"Reply to photo or video for using of this command"
|
+"Reply to photo or video for using of this command"
|
||||||
|
|||||||
21
ChatManagementBot/build.gradle
Normal file
21
ChatManagementBot/build.gradle
Normal 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"
|
||||||
|
}
|
||||||
146
ChatManagementBot/src/main/kotlin/ChatManagementBot.kt
Normal file
146
ChatManagementBot/src/main/kotlin/ChatManagementBot.kt
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
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.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.chatContentMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
|
||||||
|
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.restrictedMemberChatMemberOrNull
|
||||||
|
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 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()
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import dev.inmo.kslog.common.defaultMessageFormatter
|
|||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
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.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
@@ -33,7 +32,7 @@ import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
|
|||||||
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
||||||
import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
|
import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
|
||||||
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
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.content.ChecklistContent
|
||||||
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
|
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
|
||||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import dev.inmo.kslog.common.KSLog
|
|||||||
import dev.inmo.kslog.common.LogLevel
|
import dev.inmo.kslog.common.LogLevel
|
||||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
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.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
import dev.inmo.tgbotapi.extensions.api.bot.getMyStarBalance
|
||||||
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
|
||||||
@@ -22,7 +22,7 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPhoto
|
|||||||
import dev.inmo.tgbotapi.types.media.AudioMediaGroupMemberTelegramMedia
|
import dev.inmo.tgbotapi.types.media.AudioMediaGroupMemberTelegramMedia
|
||||||
import dev.inmo.tgbotapi.types.media.toTelegramMediaAudio
|
import dev.inmo.tgbotapi.types.media.toTelegramMediaAudio
|
||||||
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaPhoto
|
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 dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -31,8 +31,8 @@ private var BehaviourContextData.update: Update?
|
|||||||
get() = get("update") as? Update
|
get() = get("update") as? Update
|
||||||
set(value) = set("update", value)
|
set(value) = set("update", value)
|
||||||
|
|
||||||
private var BehaviourContextData.commonMessage: CommonMessage<*>?
|
private var BehaviourContextData.commonMessage: ChatContentMessage<*>?
|
||||||
get() = get("commonMessage") as? CommonMessage<*>
|
get() = get("commonMessage") as? ChatContentMessage<*>
|
||||||
set(value) = set("commonMessage", value)
|
set(value) = set("commonMessage", value)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,7 +129,7 @@ suspend fun main(vararg args: String) {
|
|||||||
println(it.chatEvent)
|
println(it.chatEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
}.second.join()
|
}.second.join()
|
||||||
|
|||||||
@@ -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.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
|
||||||
@@ -36,7 +36,7 @@ suspend fun main(vararg args: String) {
|
|||||||
onDeepLink { (it, deepLink) ->
|
onDeepLink { (it, deepLink) ->
|
||||||
reply(it, "Ok, I got deep link \"${deepLink}\" in trigger")
|
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")
|
reply(it, "Ok, I got deep link \"${deepLink}\" in waiter")
|
||||||
println(triggersHolder.handleableCommandsHolder.handleable)
|
println(triggersHolder.handleableCommandsHolder.handleable)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import dev.inmo.kslog.common.w
|
|||||||
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
import dev.inmo.micro_utils.coroutines.runCatchingLogging
|
||||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
|
||||||
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
|
||||||
@@ -75,8 +74,29 @@ suspend fun main(vararg args: String) {
|
|||||||
send(it.chat, testText)
|
send(it.chat, testText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendMessageDraft now accepts empty text (length 0 is valid since TG Bot API 9.0)
|
||||||
|
// Useful to show a typing indicator without any text yet
|
||||||
|
onCommand("test_empty_draft") {
|
||||||
|
sendMessageDraftFlowWithTexts(
|
||||||
|
it.chat.id,
|
||||||
|
flow<String> {
|
||||||
|
emit("") // empty draft — clears / initializes typing indicator with no content
|
||||||
|
delay(1500L)
|
||||||
|
val step = 50
|
||||||
|
var currentLength = step
|
||||||
|
while (isActive && testText.length > currentLength) {
|
||||||
|
delay(500L)
|
||||||
|
emit(testText.take(currentLength))
|
||||||
|
currentLength += step
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
send(it.chat, testText)
|
||||||
|
}
|
||||||
|
|
||||||
setMyCommands(
|
setMyCommands(
|
||||||
BotCommand("test_draft_flow", "Start draft testing with flow"),
|
BotCommand("test_draft_flow", "Start draft testing with flow"),
|
||||||
|
BotCommand("test_empty_draft", "Draft starting from empty text (TG Bot API 9.0)"),
|
||||||
scope = BotCommandScope.AllGroupChats
|
scope = BotCommandScope.AllGroupChats
|
||||||
)
|
)
|
||||||
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.awaitFirst
|
import dev.inmo.micro_utils.coroutines.awaitFirst
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
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.send
|
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitAnyContentMessage
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitAnyContentMessage
|
||||||
@@ -13,7 +13,7 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameThread
|
|||||||
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
||||||
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
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 dev.inmo.tgbotapi.utils.botCommand
|
import dev.inmo.tgbotapi.utils.botCommand
|
||||||
import dev.inmo.tgbotapi.utils.firstOf
|
import dev.inmo.tgbotapi.utils.firstOf
|
||||||
@@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.first
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
sealed interface BotState : State
|
sealed interface BotState : State
|
||||||
data class ExpectContentOrStopState(override val context: IdChatIdentifier, val sourceMessage: CommonMessage<TextContent>) : BotState
|
data class ExpectContentOrStopState(override val context: IdChatIdentifier, val sourceMessage: ChatContentMessage<TextContent>) : BotState
|
||||||
data class StopState(override val context: IdChatIdentifier) : BotState
|
data class StopState(override val context: IdChatIdentifier) : BotState
|
||||||
|
|
||||||
suspend fun main(args: Array<String>) {
|
suspend fun main(args: Array<String>) {
|
||||||
@@ -97,7 +97,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
startChain(ExpectContentOrStopState(it.chat.id, it.withContentOrNull() ?: return@onContentMessage))
|
startChain(ExpectContentOrStopState(it.chat.id, it.withContentOrNull() ?: return@onContentMessage))
|
||||||
}
|
}
|
||||||
|
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
}.second.join()
|
}.second.join()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
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.files.downloadFileToTemp
|
||||||
import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo
|
import dev.inmo.tgbotapi.extensions.api.get.getFileAdditionalInfo
|
||||||
@@ -10,6 +10,7 @@ import dev.inmo.tgbotapi.requests.abstracts.asMultipartFile
|
|||||||
import dev.inmo.tgbotapi.types.actions.*
|
import dev.inmo.tgbotapi.types.actions.*
|
||||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
||||||
import dev.inmo.tgbotapi.types.media.TelegramMediaDocument
|
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.TelegramMediaPhoto
|
||||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||||
import dev.inmo.tgbotapi.types.message.content.*
|
import dev.inmo.tgbotapi.types.message.content.*
|
||||||
@@ -46,6 +47,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
val action = when (content) {
|
val action = when (content) {
|
||||||
is PhotoContent -> UploadPhotoAction
|
is PhotoContent -> UploadPhotoAction
|
||||||
is AnimationContent,
|
is AnimationContent,
|
||||||
|
is LivePhotoContent,
|
||||||
is VideoContent -> UploadVideoAction
|
is VideoContent -> UploadVideoAction
|
||||||
is StickerContent -> ChooseStickerAction
|
is StickerContent -> ChooseStickerAction
|
||||||
is MediaGroupContent<*> -> UploadPhotoAction
|
is MediaGroupContent<*> -> UploadPhotoAction
|
||||||
@@ -74,7 +76,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
)
|
)
|
||||||
is MediaGroupContent<*> -> replyWithMediaGroup(
|
is MediaGroupContent<*> -> replyWithMediaGroup(
|
||||||
it,
|
it,
|
||||||
content.group.map {
|
content.group.mapNotNull {
|
||||||
when (val innerContent = it.content) {
|
when (val innerContent = it.content) {
|
||||||
is AudioContent -> TelegramMediaAudio(
|
is AudioContent -> TelegramMediaAudio(
|
||||||
downloadFileToTemp(innerContent.media).asMultipartFile()
|
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||||
@@ -88,6 +90,10 @@ suspend fun main(args: Array<String>) {
|
|||||||
is VideoContent -> TelegramMediaVideo(
|
is VideoContent -> TelegramMediaVideo(
|
||||||
downloadFileToTemp(innerContent.media).asMultipartFile()
|
downloadFileToTemp(innerContent.media).asMultipartFile()
|
||||||
)
|
)
|
||||||
|
is LivePhotoContent -> TelegramMediaLivePhoto(
|
||||||
|
downloadFileToTemp(innerContent.media).asMultipartFile(),
|
||||||
|
innerContent.media.photo ?.fileId ?: return@mapNotNull null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -107,10 +113,16 @@ suspend fun main(args: Array<String>) {
|
|||||||
it,
|
it,
|
||||||
outFile.asMultipartFile()
|
outFile.asMultipartFile()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is LivePhotoContent -> replyWithLivePhoto(
|
||||||
|
it,
|
||||||
|
outFile.asMultipartFile(),
|
||||||
|
content.media.photo ?.fileId ?: error("Unable to resend live photo files without their photos")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import dev.inmo.kslog.common.KSLog
|
|||||||
import dev.inmo.kslog.common.LogLevel
|
import dev.inmo.kslog.common.LogLevel
|
||||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
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.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
||||||
import dev.inmo.tgbotapi.extensions.api.gifts.getChatGiftsFlow
|
import dev.inmo.tgbotapi.extensions.api.gifts.getChatGiftsFlow
|
||||||
@@ -105,7 +104,7 @@ suspend fun main(vararg args: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
// allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
// println(it)
|
// println(it)
|
||||||
// }
|
// }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import dev.inmo.kslog.common.KSLog
|
|||||||
import dev.inmo.kslog.common.LogLevel
|
import dev.inmo.kslog.common.LogLevel
|
||||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
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.getMe
|
||||||
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.onGiveawayCompleted
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onGiveawayCompleted
|
||||||
@@ -50,7 +50,7 @@ suspend fun main(vararg args: String) {
|
|||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// allUpdatesFlow.subscribeSafelyWithoutExceptions(this) {
|
// allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
// println(it)
|
// println(it)
|
||||||
// }
|
// }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
|
|||||||
@@ -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.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
|
||||||
@@ -77,6 +77,6 @@ suspend fun main(vararg args: String) {
|
|||||||
MarkdownV2
|
MarkdownV2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.tgbotapi.extensions.api.EditLiveLocationInfo
|
import dev.inmo.tgbotapi.extensions.api.EditLiveLocationInfo
|
||||||
import dev.inmo.tgbotapi.extensions.api.edit.location.live.stopLiveLocation
|
import dev.inmo.tgbotapi.extensions.api.edit.location.live.stopLiveLocation
|
||||||
import dev.inmo.tgbotapi.extensions.api.handleLiveLocation
|
import dev.inmo.tgbotapi.extensions.api.handleLiveLocation
|
||||||
@@ -61,7 +61,7 @@ suspend fun main(vararg args: String) {
|
|||||||
stopLiveLocation(it, replyMarkup = null)
|
stopLiveLocation(it, replyMarkup = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
LivePhotosBot/build.gradle
Normal file
21
LivePhotosBot/build.gradle
Normal 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"
|
||||||
|
}
|
||||||
166
LivePhotosBot/src/main/kotlin/LivePhotosBot.kt
Normal file
166
LivePhotosBot/src/main/kotlin/LivePhotosBot.kt
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import dev.inmo.kslog.common.KSLog
|
||||||
|
import dev.inmo.kslog.common.LogLevel
|
||||||
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.edit.media.editMessageMedia
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.media.sendLivePhoto
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.media.sendMediaGroup
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.media.sendPaidMedia
|
||||||
|
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||||
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling
|
||||||
|
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.onPaidMediaInfoContent
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||||
|
import dev.inmo.tgbotapi.types.message.content.LivePhotoContent
|
||||||
|
import dev.inmo.tgbotapi.types.message.payments.PaidMedia
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaLivePhoto
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramPaidMediaLivePhoto
|
||||||
|
import dev.inmo.tgbotapi.types.media.toTelegramPaidMediaLivePhoto
|
||||||
|
import dev.inmo.tgbotapi.utils.RiskFeature
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This bot demonstrates Live Photos support introduced in Telegram Bot API.
|
||||||
|
*
|
||||||
|
* Key concepts demonstrated:
|
||||||
|
* - [dev.inmo.tgbotapi.types.files.LivePhotoFile] — the LivePhoto class: a photo with an attached short video
|
||||||
|
* - [TelegramMediaLivePhoto] — InputMediaLivePhoto: used in sendMediaGroup and editMessageMedia
|
||||||
|
* - [LivePhotoContent] — the content type carried in Message.live_photo / ExternalReplyInfo.live_photo
|
||||||
|
* - [sendLivePhoto] — method to send a live photo
|
||||||
|
* - [PaidMedia.LivePhoto] — PaidMediaLivePhoto: a live photo inside paid media content
|
||||||
|
* - [TelegramPaidMediaLivePhoto] — InputPaidMediaLivePhoto: used in sendPaidMedia
|
||||||
|
* - sendMediaGroup and editMessageMedia with live photos
|
||||||
|
*/
|
||||||
|
@OptIn(RiskFeature::class)
|
||||||
|
suspend fun main(vararg args: String) {
|
||||||
|
val botToken = args.first()
|
||||||
|
val isDebug = args.any { it == "debug" }
|
||||||
|
val isTestServer = args.any { it == "testServer" }
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
setDefaultKSLog(
|
||||||
|
KSLog { level: LogLevel, tag: String?, message: Any, throwable: Throwable? ->
|
||||||
|
println(defaultMessageFormatter(level, tag, message, throwable))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
telegramBotWithBehaviourAndLongPolling(
|
||||||
|
botToken,
|
||||||
|
CoroutineScope(Dispatchers.IO),
|
||||||
|
testServer = isTestServer
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Demonstrates: LivePhoto class (LivePhotoFile), live_photo field in Message, sendLivePhoto,
|
||||||
|
// InputMediaLivePhoto (TelegramMediaLivePhoto), InputPaidMediaLivePhoto (TelegramPaidMediaLivePhoto),
|
||||||
|
// editMessageMedia with live photo
|
||||||
|
onLivePhoto { message ->
|
||||||
|
// message.content is LivePhotoContent — this is the live_photo field of Message
|
||||||
|
val content: LivePhotoContent = message.content
|
||||||
|
|
||||||
|
// content.media is LivePhotoFile — the LivePhoto class (photo + short video in one file)
|
||||||
|
val livePhotoFile = content.media
|
||||||
|
println("=== Live photo received ===")
|
||||||
|
println(" fileId: ${livePhotoFile.fileId}")
|
||||||
|
println(" fileUniqueId: ${livePhotoFile.fileUniqueId}")
|
||||||
|
println(" width: ${livePhotoFile.width}")
|
||||||
|
println(" height: ${livePhotoFile.height}")
|
||||||
|
println(" duration: ${livePhotoFile.duration}s")
|
||||||
|
println(" photo (thumb): ${livePhotoFile.photo?.fileId}")
|
||||||
|
println(" mimeType: ${livePhotoFile.mimeType}")
|
||||||
|
println(" fileSize: ${livePhotoFile.fileSize}")
|
||||||
|
println(" caption: ${content.text}")
|
||||||
|
|
||||||
|
// sendLivePhoto: resend the received live photo back using LivePhotoFile overload
|
||||||
|
val sent = sendLivePhoto(
|
||||||
|
chatId = message.chat.id,
|
||||||
|
livePhoto = livePhotoFile,
|
||||||
|
text = "Resent via sendLivePhoto"
|
||||||
|
)
|
||||||
|
println(" sent message id: ${sent.messageId}")
|
||||||
|
|
||||||
|
// InputPaidMediaLivePhoto (TelegramPaidMediaLivePhoto): send the live photo as paid media (1 star)
|
||||||
|
sendPaidMedia(
|
||||||
|
chatId = message.chat.id,
|
||||||
|
starCount = 1,
|
||||||
|
media = listOf(
|
||||||
|
// TelegramPaidMediaLivePhoto is InputPaidMediaLivePhoto
|
||||||
|
TelegramPaidMediaLivePhoto(
|
||||||
|
file = livePhotoFile.fileId,
|
||||||
|
photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId
|
||||||
|
)
|
||||||
|
),
|
||||||
|
text = "Paid live photo (1 star)"
|
||||||
|
)
|
||||||
|
|
||||||
|
// editMessageMedia with InputMediaLivePhoto (TelegramMediaLivePhoto):
|
||||||
|
// edit the previously sent message to replace it with itself via TelegramMediaLivePhoto
|
||||||
|
val sentAsMedia = sent.withContentOrNull<LivePhotoContent>()
|
||||||
|
if (sentAsMedia != null) {
|
||||||
|
editMessageMedia(
|
||||||
|
message = sentAsMedia,
|
||||||
|
// TelegramMediaLivePhoto is InputMediaLivePhoto
|
||||||
|
media = TelegramMediaLivePhoto(
|
||||||
|
file = livePhotoFile.fileId,
|
||||||
|
photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId,
|
||||||
|
text = "Edited via editMessageMedia with TelegramMediaLivePhoto"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demonstrates: sendMediaGroup with live photos, InputMediaLivePhoto (TelegramMediaLivePhoto)
|
||||||
|
onLivePhotoGallery { mediaGroupContent ->
|
||||||
|
println("=== Live photo gallery received (${mediaGroupContent.group.size} items) ===")
|
||||||
|
mediaGroupContent.group.forEach { groupMember ->
|
||||||
|
val livePhotoFile = groupMember.content.media
|
||||||
|
println(" - fileId: ${livePhotoFile.fileId}, ${livePhotoFile.width}x${livePhotoFile.height}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMediaGroup with TelegramMediaLivePhoto (InputMediaLivePhoto)
|
||||||
|
sendMediaGroup(
|
||||||
|
chatId = mediaGroupContent.group.first().sourceMessage.chat.id,
|
||||||
|
media = mediaGroupContent.group.map { groupMember ->
|
||||||
|
val livePhotoFile = groupMember.content.media
|
||||||
|
// TelegramMediaLivePhoto is InputMediaLivePhoto — used here in sendMediaGroup
|
||||||
|
TelegramMediaLivePhoto(
|
||||||
|
file = livePhotoFile.fileId,
|
||||||
|
photo = livePhotoFile.photo?.fileId ?: livePhotoFile.fileId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demonstrates: PaidMediaLivePhoto (PaidMedia.LivePhoto) in received paid media content
|
||||||
|
onPaidMediaInfoContent { message ->
|
||||||
|
val paidMedia = message.content.paidMediaInfo.media
|
||||||
|
val livePhotos = paidMedia.filterIsInstance<PaidMedia.LivePhoto>()
|
||||||
|
if (livePhotos.isNotEmpty()) {
|
||||||
|
println("=== Paid media with live photos received ===")
|
||||||
|
livePhotos.forEach { paidLivePhoto ->
|
||||||
|
// paidLivePhoto is PaidMedia.LivePhoto — PaidMediaLivePhoto class
|
||||||
|
val livePhotoFile = paidLivePhoto.livePhoto
|
||||||
|
println(" - fileId: ${livePhotoFile.fileId}, ${livePhotoFile.width}x${livePhotoFile.height}")
|
||||||
|
println(" duration: ${livePhotoFile.duration}s")
|
||||||
|
}
|
||||||
|
reply(message, "Received ${livePhotos.size} paid live photo(s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demonstrates: live_photo field in edited messages (EditedMessage with LivePhotoContent)
|
||||||
|
onEditedLivePhoto { message ->
|
||||||
|
println("=== Edited live photo received ===")
|
||||||
|
println(" fileId: ${message.content.media.fileId}")
|
||||||
|
println(" caption: ${message.content.text}")
|
||||||
|
}
|
||||||
|
|
||||||
|
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) {
|
||||||
|
println(it)
|
||||||
|
}
|
||||||
|
}.second.join()
|
||||||
|
}
|
||||||
@@ -5,15 +5,20 @@ import dev.inmo.kslog.common.setDefaultKSLog
|
|||||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
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.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.getManagedBotToken
|
||||||
import dev.inmo.tgbotapi.extensions.api.managed_bots.replaceManagedBotToken
|
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.reply
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
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.BehaviourContextData
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction
|
||||||
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.onCommand
|
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.onManagedBotCreated
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onManagedBotUpdated
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onManagedBotUpdated
|
||||||
import dev.inmo.tgbotapi.extensions.utils.chatEventMessageOrNull
|
import dev.inmo.tgbotapi.extensions.utils.chatEventMessageOrNull
|
||||||
@@ -22,10 +27,12 @@ import dev.inmo.tgbotapi.extensions.utils.managedBotCreatedOrNull
|
|||||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatReplyKeyboard
|
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.replyKeyboard
|
||||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.requestManagedBotButton
|
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.Username
|
||||||
import dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestManagedBot
|
import dev.inmo.tgbotapi.types.buttons.KeyboardButtonRequestManagedBot
|
||||||
import dev.inmo.tgbotapi.types.buttons.PreparedKeyboardButtonId
|
import dev.inmo.tgbotapi.types.buttons.PreparedKeyboardButtonId
|
||||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||||
import dev.inmo.tgbotapi.types.request.RequestId
|
import dev.inmo.tgbotapi.types.request.RequestId
|
||||||
import dev.inmo.tgbotapi.types.toChatId
|
import dev.inmo.tgbotapi.types.toChatId
|
||||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
@@ -36,8 +43,8 @@ private var BehaviourContextData.update: Update?
|
|||||||
get() = get("update") as? Update
|
get() = get("update") as? Update
|
||||||
set(value) = set("update", value)
|
set(value) = set("update", value)
|
||||||
|
|
||||||
private var BehaviourContextData.commonMessage: CommonMessage<*>?
|
private var BehaviourContextData.commonMessage: ChatContentMessage<*>?
|
||||||
get() = get("commonMessage") as? CommonMessage<*>
|
get() = get("commonMessage") as? ChatContentMessage<*>
|
||||||
set(value) = set("commonMessage", value)
|
set(value) = set("commonMessage", value)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,18 +121,16 @@ suspend fun main(vararg args: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onManagedBotCreated {
|
onManagedBotCreated {
|
||||||
reply(it, "Managed bot created successfully: ${it.chatEvent.bot}")
|
val botChatId = it.chatEvent.bot.id.toChatId()
|
||||||
val token = getManagedBotToken(
|
reply(it, "Managed bot created successfully: ${it.chatEvent.bot}\nBot ID: ${botChatId.chatId.long}")
|
||||||
it.chatEvent.bot.id.toChatId()
|
val token = getManagedBotToken(botChatId)
|
||||||
)
|
|
||||||
reply(it, "Token: $token")
|
reply(it, "Token: $token")
|
||||||
}
|
}
|
||||||
|
|
||||||
onManagedBotUpdated {
|
onManagedBotUpdated {
|
||||||
send(it.user, "Managed bot has been updated: ${it.bot}")
|
val botChatId = it.bot.id.toChatId()
|
||||||
val token = getManagedBotToken(
|
send(it.user, "Managed bot has been updated: ${it.bot}\nBot ID: ${botChatId.chatId.long}")
|
||||||
it.bot.id.toChatId()
|
val token = getManagedBotToken(botChatId)
|
||||||
)
|
|
||||||
send(it.user, "Token: $token")
|
send(it.user, "Token: $token")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +141,71 @@ suspend fun main(vararg args: String) {
|
|||||||
reply(it, "Token in replace update: ${replaceManagedBotToken(managedBotCreated.bot.id.toChatId())}")
|
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) {
|
allUpdatesFlow.subscribeLoggingDropExceptions(this) {
|
||||||
println(it)
|
println(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import dev.inmo.kslog.common.KSLog
|
|||||||
import dev.inmo.kslog.common.LogLevel
|
import dev.inmo.kslog.common.LogLevel
|
||||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
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.setMyCommands
|
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.sendQuizPoll
|
||||||
import dev.inmo.tgbotapi.extensions.api.send.polls.sendRegularPoll
|
import dev.inmo.tgbotapi.extensions.api.send.polls.sendRegularPoll
|
||||||
@@ -16,14 +16,18 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOp
|
|||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOptionDeleted
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollOptionDeleted
|
||||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollUpdates
|
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onPollUpdates
|
||||||
import dev.inmo.tgbotapi.extensions.utils.accessibleMessageOrNull
|
import dev.inmo.tgbotapi.extensions.utils.accessibleMessageOrNull
|
||||||
|
import dev.inmo.tgbotapi.extensions.utils.chatContentMessageOrNull
|
||||||
import dev.inmo.tgbotapi.extensions.utils.customEmojiTextSourceOrNull
|
import dev.inmo.tgbotapi.extensions.utils.customEmojiTextSourceOrNull
|
||||||
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithArgsSources
|
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithArgsSources
|
||||||
import dev.inmo.tgbotapi.types.BotCommand
|
import dev.inmo.tgbotapi.types.BotCommand
|
||||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||||
import dev.inmo.tgbotapi.types.PollId
|
import dev.inmo.tgbotapi.types.PollId
|
||||||
import dev.inmo.tgbotapi.types.ReplyParameters
|
import dev.inmo.tgbotapi.types.ReplyParameters
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaLocation
|
||||||
|
import dev.inmo.tgbotapi.types.media.TelegramMediaVenue
|
||||||
import dev.inmo.tgbotapi.types.polls.InputPollOption
|
import dev.inmo.tgbotapi.types.polls.InputPollOption
|
||||||
import dev.inmo.tgbotapi.types.polls.PollAnswer
|
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.buildEntities
|
||||||
import dev.inmo.tgbotapi.utils.customEmoji
|
import dev.inmo.tgbotapi.utils.customEmoji
|
||||||
import dev.inmo.tgbotapi.utils.regular
|
import dev.inmo.tgbotapi.utils.regular
|
||||||
@@ -35,11 +39,22 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This bot will answer with anonymous or public poll and send message on
|
* This bot demonstrates poll features including the new API additions:
|
||||||
* any update.
|
*
|
||||||
*
|
* * `/anonymous` — anonymous regular poll
|
||||||
* * Use `/anonymous` to take anonymous regular poll
|
* * `/public` — public regular poll with option adding
|
||||||
* * Use `/public` to take public regular poll
|
* * `/quiz` — quiz poll with random correct answer
|
||||||
|
* * `/media_poll` — poll with [TelegramMediaLocation] as poll media (InputMediaLocation),
|
||||||
|
* and [TelegramMediaVenue] as option media (InputMediaVenue / InputPollOptionMedia)
|
||||||
|
* * `/quiz_media` — quiz poll with [TelegramMediaLocation] as `media` and [TelegramMediaVenue]
|
||||||
|
* as `explanationMedia` (new [QuizPoll.explanationMedia] field)
|
||||||
|
* * `/members_only` — poll with `membersOnly = true` (new [dev.inmo.tgbotapi.types.polls.Poll.membersOnly] field)
|
||||||
|
* * `/country_codes` — poll with `countryCodes` (new [dev.inmo.tgbotapi.types.polls.Poll.countryCodes] field)
|
||||||
|
* * `/single_option` — poll with just 1 option (minimum options count decreased from 2 to 1)
|
||||||
|
*
|
||||||
|
* [onPollUpdates] prints [dev.inmo.tgbotapi.types.polls.Poll.media], [dev.inmo.tgbotapi.types.polls.Poll.membersOnly],
|
||||||
|
* [dev.inmo.tgbotapi.types.polls.Poll.countryCodes], [QuizPoll.explanationMedia], and
|
||||||
|
* [dev.inmo.tgbotapi.types.polls.PollOption.media] for each option.
|
||||||
*/
|
*/
|
||||||
suspend fun main(vararg args: String) {
|
suspend fun main(vararg args: String) {
|
||||||
val botToken = args.first()
|
val botToken = args.first()
|
||||||
@@ -173,6 +188,120 @@ suspend fun main(vararg args: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TelegramMediaLocation implements InputPollMedia and InputPollOptionMedia (InputMediaLocation)
|
||||||
|
// TelegramMediaVenue implements InputPollMedia and InputPollOptionMedia (InputMediaVenue)
|
||||||
|
// Both can be used as poll question media or as option media
|
||||||
|
onCommand("media_poll") {
|
||||||
|
val sentPoll = sendRegularPoll(
|
||||||
|
it.chat.id,
|
||||||
|
buildEntities { regular("Which venue would you visit?") },
|
||||||
|
listOf(
|
||||||
|
// 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") },
|
||||||
|
),
|
||||||
|
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 = false,
|
||||||
|
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 = false,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onPollAnswer {
|
onPollAnswer {
|
||||||
val chatId = pollToChat[it.pollId] ?: return@onPollAnswer
|
val chatId = pollToChat[it.pollId] ?: return@onPollAnswer
|
||||||
|
|
||||||
@@ -185,14 +314,29 @@ suspend fun main(vararg args: String) {
|
|||||||
onPollUpdates {
|
onPollUpdates {
|
||||||
val chatId = pollToChat[it.id] ?: return@onPollUpdates
|
val chatId = pollToChat[it.id] ?: return@onPollUpdates
|
||||||
|
|
||||||
when(it.isAnonymous) {
|
// Poll.media — PollMedia attached to the poll question (new field)
|
||||||
false -> send(chatId, "[onPollUpdates] Public poll updated: ${it.options.joinToString()}")
|
// Poll.membersOnly — whether poll is restricted to channel members (new field)
|
||||||
true -> send(chatId, "[onPollUpdates] Anonymous poll updated: ${it.options.joinToString()}")
|
// 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 {
|
onPollOptionAdded {
|
||||||
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.let { pollMessage ->
|
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { pollMessage ->
|
||||||
reply(pollMessage) {
|
reply(pollMessage) {
|
||||||
+"Poll option added: \n"
|
+"Poll option added: \n"
|
||||||
+it.chatEvent.optionTextSources
|
+it.chatEvent.optionTextSources
|
||||||
@@ -200,7 +344,7 @@ suspend fun main(vararg args: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onPollOptionDeleted {
|
onPollOptionDeleted {
|
||||||
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.let { pollMessage ->
|
it.chatEvent.pollMessage ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { pollMessage ->
|
||||||
reply(pollMessage) {
|
reply(pollMessage) {
|
||||||
+"Poll option deleted: \n"
|
+"Poll option deleted: \n"
|
||||||
+it.chatEvent.optionTextSources
|
+it.chatEvent.optionTextSources
|
||||||
@@ -210,7 +354,7 @@ suspend fun main(vararg args: String) {
|
|||||||
|
|
||||||
onContentMessage {
|
onContentMessage {
|
||||||
val replyPollOptionId = it.replyInfo ?.pollOptionId ?: return@onContentMessage
|
val replyPollOptionId = it.replyInfo ?.pollOptionId ?: return@onContentMessage
|
||||||
it.replyTo ?.accessibleMessageOrNull() ?.let { replied ->
|
it.replyTo ?.accessibleMessageOrNull() ?.chatContentMessageOrNull() ?.let { replied ->
|
||||||
reply(replied, pollOptionId = replyPollOptionId) {
|
reply(replied, pollOptionId = replyPollOptionId) {
|
||||||
+"Reply to poll option"
|
+"Reply to poll option"
|
||||||
}
|
}
|
||||||
@@ -221,8 +365,13 @@ suspend fun main(vararg args: String) {
|
|||||||
BotCommand("anonymous", "Create anonymous regular poll"),
|
BotCommand("anonymous", "Create anonymous regular poll"),
|
||||||
BotCommand("public", "Create non anonymous regular poll"),
|
BotCommand("public", "Create non anonymous regular poll"),
|
||||||
BotCommand("quiz", "Create quiz poll with random right answer"),
|
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)"),
|
||||||
)
|
)
|
||||||
|
|
||||||
allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { println(it) }
|
allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) { println(it) }
|
||||||
}.second.join()
|
}.second.join()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
|
|||||||
import dev.inmo.tgbotapi.types.chat.member.ChatCommonAdministratorRights
|
import dev.inmo.tgbotapi.types.chat.member.ChatCommonAdministratorRights
|
||||||
import dev.inmo.tgbotapi.types.commands.BotCommandScope
|
import dev.inmo.tgbotapi.types.commands.BotCommandScope
|
||||||
import dev.inmo.tgbotapi.types.message.abstracts.AccessibleMessage
|
import dev.inmo.tgbotapi.types.message.abstracts.AccessibleMessage
|
||||||
|
import dev.inmo.tgbotapi.types.message.abstracts.ChatMessage
|
||||||
import dev.inmo.tgbotapi.types.request.RequestId
|
import dev.inmo.tgbotapi.types.request.RequestId
|
||||||
import dev.inmo.tgbotapi.utils.*
|
import dev.inmo.tgbotapi.utils.*
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
@@ -208,7 +209,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
) {
|
) {
|
||||||
val replyMessage = it.replyTo
|
val replyMessage = it.replyTo
|
||||||
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
||||||
if (replyMessage is AccessibleMessage) {
|
if (replyMessage is ChatMessage) {
|
||||||
reply(
|
reply(
|
||||||
replyMessage,
|
replyMessage,
|
||||||
"Manage keyboard:",
|
"Manage keyboard:",
|
||||||
@@ -229,7 +230,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
val replyMessage = it.replyTo
|
val replyMessage = it.replyTo
|
||||||
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
val userInReply = replyMessage?.fromUserMessageOrNull()?.user?.id ?: return@onCommand
|
||||||
|
|
||||||
if (replyMessage is AccessibleMessage) {
|
if (replyMessage is ChatMessage) {
|
||||||
reply(
|
reply(
|
||||||
replyMessage,
|
replyMessage,
|
||||||
"Manage keyboard:",
|
"Manage keyboard:",
|
||||||
@@ -247,7 +248,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
initialFilter = { it.user.id == allowedAdmin }
|
initialFilter = { it.user.id == allowedAdmin }
|
||||||
) {
|
) {
|
||||||
val messageReply =
|
val messageReply =
|
||||||
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
it.message.chatContentMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||||
val userId = messageReply.user.id
|
val userId = messageReply.user.id
|
||||||
val permissions =
|
val permissions =
|
||||||
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
||||||
@@ -334,7 +335,7 @@ suspend fun main(args: Array<String>) {
|
|||||||
initialFilter = { it.user.id == allowedAdmin }
|
initialFilter = { it.user.id == allowedAdmin }
|
||||||
) {
|
) {
|
||||||
val messageReply =
|
val messageReply =
|
||||||
it.message.commonMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
it.message.chatContentMessageOrNull()?.replyTo?.fromUserMessageOrNull() ?: return@onMessageDataCallbackQuery
|
||||||
val userId = messageReply.user.id
|
val userId = messageReply.user.id
|
||||||
val permissions =
|
val permissions =
|
||||||
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
getUserChatPermissions(it.message.chat.id.toChatId(), userId) ?: return@onMessageDataCallbackQuery
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import dev.inmo.tgbotapi.extensions.utils.previewChannelDirectMessagesChatOrNull
|
|||||||
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
import dev.inmo.tgbotapi.extensions.utils.suggestedChannelDirectMessagesContentMessageOrNull
|
||||||
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
import dev.inmo.tgbotapi.types.message.SuggestedPostParameters
|
||||||
import dev.inmo.tgbotapi.types.message.abstracts.ChannelPaidPost
|
import dev.inmo.tgbotapi.types.message.abstracts.ChannelPaidPost
|
||||||
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 dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||||
import dev.inmo.tgbotapi.utils.firstOf
|
import dev.inmo.tgbotapi.utils.firstOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import dev.inmo.kslog.common.LogLevel
|
|||||||
import dev.inmo.kslog.common.defaultMessageFormatter
|
import dev.inmo.kslog.common.defaultMessageFormatter
|
||||||
import dev.inmo.kslog.common.setDefaultKSLog
|
import dev.inmo.kslog.common.setDefaultKSLog
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions
|
||||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
|
||||||
import dev.inmo.tgbotapi.abstracts.FromUser
|
import dev.inmo.tgbotapi.abstracts.FromUser
|
||||||
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
import dev.inmo.tgbotapi.extensions.api.bot.getMe
|
||||||
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
import dev.inmo.tgbotapi.extensions.api.business.getBusinessAccountGiftsFlow
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ kotlin.daemon.jvmargs=-Xmx3g -Xms500m
|
|||||||
|
|
||||||
|
|
||||||
kotlin_version=2.3.20
|
kotlin_version=2.3.20
|
||||||
telegram_bot_api_version=34.0.0-t6
|
telegram_bot_api_version=34.0.0-t7
|
||||||
micro_utils_version=0.29.1
|
micro_utils_version=0.29.1
|
||||||
serialization_version=1.10.0
|
serialization_version=1.10.0
|
||||||
ktor_version=3.4.1
|
ktor_version=3.4.1
|
||||||
|
|||||||
@@ -73,3 +73,7 @@ include ":TagsBot"
|
|||||||
include ":ManagedBotsBot"
|
include ":ManagedBotsBot"
|
||||||
|
|
||||||
include ":GuestQueryBot"
|
include ":GuestQueryBot"
|
||||||
|
|
||||||
|
include ":LivePhotosBot"
|
||||||
|
|
||||||
|
include ":ChatManagementBot"
|
||||||
|
|||||||
Reference in New Issue
Block a user