diff --git a/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt b/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt index 2623cfc..9b9e1f6 100644 --- a/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt +++ b/BusinessConnectionsBot/src/main/kotlin/BusinessConnectionsBot.kt @@ -2,24 +2,68 @@ 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.accessibleMessageOrNull +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.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<String>) { val botToken = args.first() @@ -34,6 +78,7 @@ suspend fun main(args: Array<String>) { } val businessConnectionsChats = mutableMapOf<BusinessConnectionId, ChatId>() + val chatsBusinessConnections = mutableMapOf<ChatId, BusinessConnectionId>() val businessConnectionsChatsMutex = Mutex() telegramBotWithBehaviourAndLongPolling(botToken, CoroutineScope(Dispatchers.IO)) { @@ -43,12 +88,14 @@ suspend fun main(args: Array<String>) { 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") } @@ -71,7 +118,20 @@ suspend fun main(args: Array<String>) { if (businessContentMessage.sentByBusinessConnectionOwner) { reply(sent, "You have sent this message to the ${businessContentMessage.businessConnectionId.string} related chat") } else { - reply(sent, "User have sent this message to you in the ${businessContentMessage.businessConnectionId.string} related chat") + 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}") + } + } + ) } } } @@ -98,5 +158,318 @@ suspend fun main(args: Array<String>) { } 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<TextContent>, isPublic: Boolean) { + val businessConnectionId = chatsBusinessConnections[it.chat.id] ?: return@handleSetProfilePhoto + val replyTo = it.replyTo ?.commonMessageOrNull() ?.withContentOrNull<PhotoContent>() + if (replyTo == null) { + reply(it) { + +"Reply to photo for using of this command" + } + return@handleSetProfilePhoto + } + + val set = runCatching { + val file = downloadFileToTemp(replyTo.content) + setBusinessAccountProfilePhoto( + businessConnectionId, + InputProfilePhoto.Static( + file.multipartFile() + ), + isPublic = isPublic + ) + }.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<VisualMediaGroupPartContent>() + if (replyTo == null) { + reply(it) { + +"Reply to photo or video for using of this command" + } + return@onCommand + } + + val posted = runCatching { + val file = downloadFileToTemp(replyTo.content) + postStory( + businessConnectionId, + when (replyTo.content) { + is PhotoContent -> 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<StoryContent>() + if (replyTo == null) { + reply(it) { + +"Reply to photo or video for using of this command" + } + return@onCommand + } + + val deleted = runCatching { + deleteStory(businessConnectionId, replyTo.content.story.id) + }.getOrElse { + it.printStackTrace() + false + } + reply(it) { + if (deleted) { + +"Story has been deleted" + } else { + +"Story has not been deleted" + } + } + } }.second.join() } \ No newline at end of file diff --git a/CustomBot/src/main/kotlin/CustomBot.kt b/CustomBot/src/main/kotlin/CustomBot.kt index 6cf9334..4bf65a2 100644 --- a/CustomBot/src/main/kotlin/CustomBot.kt +++ b/CustomBot/src/main/kotlin/CustomBot.kt @@ -4,6 +4,7 @@ import dev.inmo.kslog.common.defaultMessageFormatter import dev.inmo.kslog.common.setDefaultKSLog import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.tgbotapi.extensions.api.bot.getMe +import dev.inmo.tgbotapi.extensions.api.chat.get.getChat import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextData import dev.inmo.tgbotapi.extensions.behaviour_builder.buildSubcontextInitialAction import dev.inmo.tgbotapi.extensions.behaviour_builder.telegramBotWithBehaviourAndLongPolling @@ -65,6 +66,7 @@ suspend fun main(vararg args: String) { onCommand("start") { println(data.update) println(data.commonMessage) + println(getChat(it.chat)) } onCommand( diff --git a/InlineQueriesBot/build.gradle b/InlineQueriesBot/build.gradle index 7131a21..49b7e81 100644 --- a/InlineQueriesBot/build.gradle +++ b/InlineQueriesBot/build.gradle @@ -12,14 +12,16 @@ plugins { id "org.jetbrains.kotlin.multiplatform" } -apply plugin: 'application' - -mainClassName="InlineQueriesBotKt" - apply from: "$nativePartTemplate" kotlin { - jvm() + jvm { + binaries { + executable { + mainClass.set("InlineQueriesBotKt") + } + } + } sourceSets { commonMain { @@ -27,12 +29,9 @@ kotlin { implementation kotlin('stdlib') api "dev.inmo:tgbotapi:$telegram_bot_api_version" + api "io.ktor:ktor-client-logging:$ktor_version" } } } } -dependencies { - implementation 'io.ktor:ktor-client-logging-jvm:3.1.0' -} - diff --git a/InlineQueriesBot/src/commonMain/kotlin/Bot.kt b/InlineQueriesBot/src/commonMain/kotlin/Bot.kt index 18019d8..c89df0f 100644 --- a/InlineQueriesBot/src/commonMain/kotlin/Bot.kt +++ b/InlineQueriesBot/src/commonMain/kotlin/Bot.kt @@ -1,4 +1,4 @@ -import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.send.reply @@ -59,7 +59,7 @@ suspend fun doInlineQueriesBot(token: String) { reply(message, deepLink) } - allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { + allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) { println(it) } diff --git a/KeyboardsBot/KeyboardsBotLib/src/commonMain/kotlin/KeyboardsBot.kt b/KeyboardsBot/KeyboardsBotLib/src/commonMain/kotlin/KeyboardsBot.kt index 9781465..1be39cf 100644 --- a/KeyboardsBot/KeyboardsBotLib/src/commonMain/kotlin/KeyboardsBot.kt +++ b/KeyboardsBot/KeyboardsBotLib/src/commonMain/kotlin/KeyboardsBot.kt @@ -1,4 +1,4 @@ -import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.bot.getMe @@ -89,7 +89,7 @@ suspend fun activateKeyboardsBot( onCommandWithArgs("inline") { message, args -> val numberArgs = args.mapNotNull { it.toIntOrNull() } val numberOfPages = numberArgs.getOrNull(1) ?: numberArgs.firstOrNull() ?: 10 - val page = numberArgs.firstOrNull() ?.takeIf { numberArgs.size > 1 } ?.coerceAtLeast(1) ?: 1 + val page = numberArgs.firstOrNull()?.takeIf { numberArgs.size > 1 }?.coerceAtLeast(1) ?: 1 reply( message, replyMarkup = inlineKeyboard { @@ -138,7 +138,8 @@ suspend fun activateKeyboardsBot( onBaseInlineQuery { val page = it.query.takeWhile { it.isDigit() }.toIntOrNull() ?: return@onBaseInlineQuery - val count = it.query.removePrefix(page.toString()).dropWhile { !it.isDigit() }.takeWhile { it.isDigit() }.toIntOrNull() ?: return@onBaseInlineQuery + val count = it.query.removePrefix(page.toString()).dropWhile { !it.isDigit() }.takeWhile { it.isDigit() } + .toIntOrNull() ?: return@onBaseInlineQuery answer( it, @@ -170,7 +171,7 @@ suspend fun activateKeyboardsBot( setMyCommands(BotCommand("inline", "Creates message with pagination inline keyboard")) - allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { + allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) { println(it) } }.join() diff --git a/RandomFileSenderBot/build.gradle b/RandomFileSenderBot/build.gradle index d4e2f24..8626fe1 100644 --- a/RandomFileSenderBot/build.gradle +++ b/RandomFileSenderBot/build.gradle @@ -12,12 +12,14 @@ plugins { id "org.jetbrains.kotlin.multiplatform" } -apply plugin: 'application' - -mainClassName="RandomFileSenderBotKt" - kotlin { - jvm() + jvm { + binaries { + executable { + mainClass.set("RandomFileSenderBotKt") + } + } + } sourceSets { commonMain { diff --git a/ResenderBot/ResenderBotLib/src/commonMain/kotlin/ResenderBot.kt b/ResenderBot/ResenderBotLib/src/commonMain/kotlin/ResenderBot.kt index 69fb356..094ff67 100644 --- a/ResenderBot/ResenderBotLib/src/commonMain/kotlin/ResenderBot.kt +++ b/ResenderBot/ResenderBotLib/src/commonMain/kotlin/ResenderBot.kt @@ -1,4 +1,4 @@ -import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.send.withTypingAction import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat @@ -31,15 +31,15 @@ suspend fun activateResenderBot( it.content.createResend( chat.id, messageThreadId = it.threadIdOrNull, - replyParameters = it.replyInfo ?.messageMeta ?.let { meta -> - val quote = it.withContentOrNull<TextContent>() ?.content ?.quote + replyParameters = it.replyInfo?.messageMeta?.let { meta -> + val quote = it.withContentOrNull<TextContent>()?.content?.quote ReplyParameters( meta, - entities = quote ?.textSources ?: emptyList(), - quotePosition = quote ?.position + entities = quote?.textSources ?: emptyList(), + quotePosition = quote?.position ) }, - effectId = it.possiblyWithEffectMessageOrNull() ?.effectId + effectId = it.possiblyWithEffectMessageOrNull()?.effectId ) ) { it.forEach(print) @@ -49,7 +49,7 @@ suspend fun activateResenderBot( println("Answer info: $answer") } - allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { + allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) { println(it) } print(bot.getMe()) diff --git a/StickerInfoBot/StickerInfoBotLib/src/commonMain/kotlin/StickerInfoBot.kt b/StickerInfoBot/StickerInfoBotLib/src/commonMain/kotlin/StickerInfoBot.kt index da0db26..19aefe1 100644 --- a/StickerInfoBot/StickerInfoBotLib/src/commonMain/kotlin/StickerInfoBot.kt +++ b/StickerInfoBot/StickerInfoBotLib/src/commonMain/kotlin/StickerInfoBot.kt @@ -1,5 +1,5 @@ import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandler -import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.get.getCustomEmojiStickerOrNull @@ -55,7 +55,7 @@ suspend fun activateStickerInfoBot( withTypingAction(it.chat) { it.content.textSources.mapNotNull { if (it is CustomEmojiTextSource) { - getCustomEmojiStickerOrNull(it.customEmojiId) ?.stickerSetName + getCustomEmojiStickerOrNull(it.customEmojiId)?.stickerSetName } else { null } @@ -76,7 +76,7 @@ suspend fun activateStickerInfoBot( ) } - allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { + allUpdatesFlow.subscribeLoggingDropExceptions(scope = this) { println(it) } }.join() diff --git a/WebApp/build.gradle b/WebApp/build.gradle index 8e04927..22cad8d 100644 --- a/WebApp/build.gradle +++ b/WebApp/build.gradle @@ -16,10 +16,14 @@ plugins { id "org.jetbrains.compose" version "$compose_version" } -apply plugin: 'application' - kotlin { - jvm() + jvm { + binaries { + executable { + mainClass.set("WebAppServerKt") + } + } + } js(IR) { browser() binaries.executable() @@ -53,10 +57,6 @@ kotlin { } } -application { - mainClassName = "WebAppServerKt" -} - tasks.getByName("compileKotlinJvm") .dependsOn(jsBrowserDistribution) tasks.getByName("compileKotlinJvm").configure { diff --git a/WebApp/src/jsMain/kotlin/main.kt b/WebApp/src/jsMain/kotlin/main.kt index fa1c3d0..609850c 100644 --- a/WebApp/src/jsMain/kotlin/main.kt +++ b/WebApp/src/jsMain/kotlin/main.kt @@ -1,6 +1,5 @@ import androidx.compose.runtime.* -import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions -import dev.inmo.tgbotapi.types.CustomEmojiId +import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.tgbotapi.types.userIdField import dev.inmo.tgbotapi.types.webAppQueryIdField import dev.inmo.tgbotapi.webapps.* @@ -23,10 +22,12 @@ import kotlinx.dom.appendElement import kotlinx.dom.appendText import kotlinx.serialization.json.Json import org.jetbrains.compose.web.attributes.InputType -import org.jetbrains.compose.web.css.DisplayStyle +import org.jetbrains.compose.web.attributes.placeholder +import org.jetbrains.compose.web.css.Style +import org.jetbrains.compose.web.css.StyleSheet import org.jetbrains.compose.web.css.Color as ComposeColor import org.jetbrains.compose.web.css.backgroundColor -import org.jetbrains.compose.web.css.display +import org.jetbrains.compose.web.css.color import org.jetbrains.compose.web.dom.* import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.renderComposable @@ -39,6 +40,13 @@ fun HTMLElement.log(text: String) { appendElement("p", {}) } +private object RootStyleSheet : StyleSheet() { + val rootClass by style { + color(ComposeColor("var(--tg-theme-text-color)")) + backgroundColor(ComposeColor("var(--tg-theme-bg-color)")) + } +} + @OptIn(ExperimentalUnsignedTypes::class) fun main() { console.log("Web app started") @@ -46,6 +54,14 @@ fun main() { val baseUrl = window.location.origin.removeSuffix("/") renderComposable("root") { + Style(RootStyleSheet) + DisposableEffect(null) { + scopeElement.classList.add(RootStyleSheet.rootClass) + + onDispose { + scopeElement.classList.remove(RootStyleSheet.rootClass) + } + } val scope = rememberCoroutineScope() val isSafeState = remember { mutableStateOf<Boolean?>(null) } val logsState = remember { mutableStateListOf<Any?>() } @@ -87,6 +103,7 @@ fun main() { P() Text("Chat from WebAppInitData: ${webApp.initDataUnsafe.chat}") + H3 { Text("Emoji status management") } val emojiStatusAccessState = remember { mutableStateOf(false) } webApp.onEmojiStatusAccessRequested { emojiStatusAccessState.value = it.isAllowed @@ -110,7 +127,7 @@ fun main() { userId ?.let { userId -> Button({ onClick { - scope.launchSafelyWithoutExceptions { + scope.launchLoggingDropExceptions { client.post("$baseUrl/setCustomEmoji") { parameter(userIdField, userId.long) setBody( @@ -127,10 +144,12 @@ fun main() { } } } + P() + H3 { Text("Call server method with webAppQueryIdField") } Button({ onClick { - scope.launchSafelyWithoutExceptions { + scope.launchLoggingDropExceptions { handleResult({ "Clicked" }) { client.post("${window.location.origin.removeSuffix("/")}/inline") { parameter(webAppQueryIdField, it) @@ -145,10 +164,11 @@ fun main() { } P() + H3 { Text("User info") } Text("Allow to write in private messages: ${webApp.initDataUnsafe.user ?.allowsWriteToPM ?: "User unavailable"}") P() - Text("Alerts:") + H3 { Text("Alerts") } Button({ onClick { webApp.showPopup( @@ -186,8 +206,22 @@ fun main() { }) { Text("Alert") } + Button({ + onClick { + webApp.showConfirm( + "This is confirm message" + ) { + logsState.add( + "You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm" + ) + } + } + }) { + Text("Confirm") + } P() + H3 { Text("Write access callbacks") } Button({ onClick { webApp.requestWriteAccess() @@ -206,6 +240,7 @@ fun main() { } P() + H3 { Text("Request contact") } Button({ onClick { webApp.requestContact() @@ -220,24 +255,9 @@ fun main() { }) { Text("Request contact with callback") } - P() - - Button({ - onClick { - webApp.showConfirm( - "This is confirm message" - ) { - logsState.add( - "You have pressed \"${if (it) "Ok" else "Cancel"}\" in confirm" - ) - } - } - }) { - Text("Confirm") - } P() - + H3 { Text("Closing confirmation") } val isClosingConfirmationEnabledState = remember { mutableStateOf(webApp.isClosingConfirmationEnabled) } Button({ onClick { @@ -255,7 +275,7 @@ fun main() { } P() - + H3 { Text("Colors") } val headerColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) } fun updateHeaderColor() { val (r, g, b) = Random.nextUBytes(3) @@ -280,7 +300,6 @@ fun main() { } P() - val backgroundColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) } fun updateBackgroundColor() { val (r, g, b) = Random.nextUBytes(3) @@ -305,7 +324,6 @@ fun main() { } P() - val bottomBarColor = remember { mutableStateOf<Color.Hex>(Color.Hex("#000000")) } fun updateBottomBarColor() { val (r, g, b) = Random.nextUBytes(3) @@ -329,60 +347,6 @@ fun main() { } } - P() - - val storageTrigger = remember { mutableStateOf<List<Pair<CloudStorageKey, CloudStorageValue>>>(emptyList()) } - fun updateCloudStorage() { - webApp.cloudStorage.getAll { - it.onSuccess { - storageTrigger.value = it.toList().sortedBy { it.first.key } - } - } - } - key(storageTrigger.value) { - storageTrigger.value.forEach { (key, value) -> - val keyState = remember { mutableStateOf(key.key) } - val valueState = remember { mutableStateOf(value.value) } - Input(InputType.Text) { - value(key.key) - onInput { keyState.value = it.value } - } - Input(InputType.Text) { - value(value.value) - onInput { valueState.value = it.value } - } - Button({ - onClick { - if (key.key != keyState.value) { - webApp.cloudStorage.remove(key) - } - webApp.cloudStorage.set(keyState.value, valueState.value) - updateCloudStorage() - } - }) { - Text("Save") - } - } - let { // new element adding - val keyState = remember { mutableStateOf("") } - val valueState = remember { mutableStateOf("") } - Input(InputType.Text) { - onInput { keyState.value = it.value } - } - Input(InputType.Text) { - onInput { valueState.value = it.value } - } - Button({ - onClick { - webApp.cloudStorage.set(keyState.value, valueState.value) - updateCloudStorage() - } - }) { - Text("Save") - } - } - } - remember { webApp.apply { @@ -432,9 +396,10 @@ fun main() { } } } - P() - let { // Accelerometer + P() + let { + H3 { Text("Accelerometer") } val enabledState = remember { mutableStateOf(webApp.accelerometer.isStarted) } webApp.onAccelerometerStarted { enabledState.value = true } webApp.onAccelerometerStopped { enabledState.value = false } @@ -475,7 +440,8 @@ fun main() { } P() - let { // Gyroscope + let { + H3 { Text("Gyroscope") } val enabledState = remember { mutableStateOf(webApp.gyroscope.isStarted) } webApp.onGyroscopeStarted { enabledState.value = true } webApp.onGyroscopeStopped { enabledState.value = false } @@ -514,9 +480,10 @@ fun main() { Text("z: ${zState.value}") } } - P() - let { // DeviceOrientation + P() + let { + H3 { Text("Device Orientation") } val enabledState = remember { mutableStateOf(webApp.deviceOrientation.isStarted) } webApp.onDeviceOrientationStarted { enabledState.value = true } webApp.onDeviceOrientationStopped { enabledState.value = false } @@ -555,8 +522,181 @@ fun main() { Text("gamma: ${gammaState.value}") } } + + P() + H3 { Text("Cloud storage") } + val storageTrigger = remember { mutableStateOf<List<Pair<CloudStorageKey, CloudStorageValue>>>(emptyList()) } + fun updateCloudStorage() { + webApp.cloudStorage.getAll { + it.onSuccess { + storageTrigger.value = it.toList().sortedBy { it.first.key } + } + } + } + key(storageTrigger.value) { + storageTrigger.value.forEach { (key, value) -> + val keyState = remember { mutableStateOf(key.key) } + val valueState = remember { mutableStateOf(value.value) } + Input(InputType.Text) { + value(key.key) + onInput { keyState.value = it.value } + } + Input(InputType.Text) { + value(value.value) + onInput { valueState.value = it.value } + } + Button({ + onClick { + if (key.key != keyState.value) { + webApp.cloudStorage.remove(key) + } + webApp.cloudStorage.set(keyState.value, valueState.value) + updateCloudStorage() + } + }) { + Text("Save") + } + } + let { // new element adding + val keyState = remember { mutableStateOf("") } + val valueState = remember { mutableStateOf("") } + Input(InputType.Text) { + onInput { keyState.value = it.value } + } + Input(InputType.Text) { + onInput { valueState.value = it.value } + } + Button({ + onClick { + webApp.cloudStorage.set(keyState.value, valueState.value) + updateCloudStorage() + } + }) { + Text("Save") + } + } + } + + P() + let { // DeviceStorage + H3 { Text("Device storage") } + val fieldKey = remember { mutableStateOf("") } + val fieldValue = remember { mutableStateOf("") } + val message = remember { mutableStateOf("") } + Div { + Text("Start type title of key. If value will be found in device storage, it will be shown in value input") + } + + Input(InputType.Text) { + placeholder("Key") + value(fieldKey.value) + onInput { + fieldKey.value = it.value + webApp.deviceStorage.getItem(it.value) { e, v -> + fieldValue.value = v ?: "" + if (v == null) { + message.value = "Value for key \"${it.value}\" has not been found" + } else { + message.value = "Value for key \"${it.value}\" has been found: \"$v\"" + } + } + } + } + Div { + Text("If you want to change value if typed key - just put it here") + } + Input(InputType.Text) { + placeholder("Value") + value(fieldValue.value) + onInput { + fieldValue.value = it.value + webApp.deviceStorage.setItem(fieldKey.value, it.value) { e, v -> + if (v == true) { + fieldValue.value = it.value + message.value = "Value \"${it.value}\" has been saved" + } + } + } + } + + if (message.value.isNotEmpty()) { + Div { Text(message.value) } + } + } P() + let { // DeviceStorage + H3 { Text("Secure storage") } + val fieldKey = remember { mutableStateOf("") } + val fieldValue = remember { mutableStateOf("") } + val message = remember { mutableStateOf("") } + val restorableState = remember { mutableStateOf(false) } + Div { + Text("Start type title of key. If value will be found in device storage, it will be shown in value input") + } + + Input(InputType.Text) { + placeholder("Key") + value(fieldKey.value) + onInput { + fieldKey.value = it.value + webApp.secureStorage.getItem(it.value) { e, v, restorable -> + fieldValue.value = v ?: "" + restorableState.value = restorable == true + if (v == null) { + if (restorable == true) { + message.value = "Value for key \"${it.value}\" has not been found, but can be restored" + } else { + message.value = "Value for key \"${it.value}\" has not been found. Error: $e" + } + } else { + message.value = "Value for key \"${it.value}\" has been found: \"$v\"" + } + } + } + } + if (restorableState.value) { + Button({ + onClick { + webApp.secureStorage.restoreItem(fieldKey.value) { e, v -> + fieldValue.value = v ?: "" + if (v == null) { + message.value = "Value for key \"${fieldKey.value}\" has not been restored. Error: $e" + } else { + message.value = "Value for key \"${fieldKey.value}\" has been restored: \"$v\"" + } + } + } + }) { + Text("Restore") + } + } + Div { + Text("If you want to change value if typed key - just put it here") + } + Input(InputType.Text) { + placeholder("Value") + value(fieldValue.value) + onInput { + fieldValue.value = it.value + webApp.secureStorage.setItem(fieldKey.value, it.value) { e, v -> + if (v) { + fieldValue.value = it.value + message.value = "Value \"${it.value}\" has been saved" + } else { + message.value = "Value \"${it.value}\" has not been saved. Error: $e" + } + } + } + } + + if (message.value.isNotEmpty()) { + Div { Text(message.value) } + } + } + P() + + H3 { Text("Events") } EventType.values().forEach { eventType -> when (eventType) { EventType.AccelerometerChanged -> webApp.onAccelerometerChanged { /*logsState.add("AccelerometerChanged") /* see accelerometer block */ */ } diff --git a/WebApp/src/jvmMain/kotlin/WebAppServer.kt b/WebApp/src/jvmMain/kotlin/WebAppServer.kt index d6ed35f..11b1f8a 100644 --- a/WebApp/src/jvmMain/kotlin/WebAppServer.kt +++ b/WebApp/src/jvmMain/kotlin/WebAppServer.kt @@ -1,4 +1,5 @@ import dev.inmo.kslog.common.* +import dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.ktor.server.createKtorServer import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery @@ -195,7 +196,7 @@ suspend fun main(vararg args: String) { BotCommand("reply_markup", "Use to get reply markup keyboard with web app trigger"), BotCommand("inline", "Use to get inline keyboard with web app trigger"), ) - allUpdatesFlow.subscribeSafelyWithoutExceptions(this) { + allUpdatesFlow.subscribeLoggingDropExceptions(this) { println(it) } println(getMe()) diff --git a/build.gradle b/build.gradle index ddbaec4..491d1cc 100644 --- a/build.gradle +++ b/build.gradle @@ -14,8 +14,8 @@ allprojects { nativePartTemplate = "${rootProject.projectDir.absolutePath}/native_template.gradle" } repositories { - mavenLocal() mavenCentral() + google() if (project.hasProperty("GITHUB_USER") && project.hasProperty("GITHUB_TOKEN")) { maven { url "https://maven.pkg.github.com/InsanusMokrassar/TelegramBotAPI" @@ -26,7 +26,8 @@ allprojects { } } - maven { url "https://nexus.inmo.dev/repository/maven-releases/" } + maven { url "https://proxy.nexus.inmo.dev/repository/maven-releases/" } + mavenLocal() } } diff --git a/gradle.properties b/gradle.properties index dc5e890..f2a00ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,9 +5,9 @@ org.gradle.jvmargs=-Xmx3148m kotlin.daemon.jvmargs=-Xmx3g -Xms500m -kotlin_version=2.1.10 -telegram_bot_api_version=23.2.0 -micro_utils_version=0.24.6 +kotlin_version=2.1.20 +telegram_bot_api_version=25.0.0 +micro_utils_version=0.25.3 serialization_version=1.8.0 -ktor_version=3.1.0 +ktor_version=3.1.1 compose_version=1.7.3