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