diff --git a/.github/labeler.yml b/.github/labeler.yml index 41aaf94cad..45ea34644b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,6 +1,7 @@ -core: ./TelegramBotAPI/**/* -api_extensions: ./TelegramBotAPI-extensions-api/**/* -utils_extensions: ./TelegramBotAPI-extensions-utils/**/* +api: "TelegramBotAPI-extensions-api/**" +utils: "TelegramBotAPI-extensions-utils/**" +core: "TelegramBotAPI/**" # currently not work -code: ./**/*.kt -gradle: ./**/*.gradle +code: "**/*.kt" +gradle: "**/*.gradle" +markdown: "**/*.md" diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index e90b599b9a..be1ee465e3 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -5,15 +5,14 @@ # file with configuration. For more information, see: # https://github.com/actions/labeler/blob/master/README.md -name: Labeler -on: [pull_request] +name: "Pull Request Labeler" +on: + - pull_request jobs: - label: - + triage: runs-on: ubuntu-latest - steps: - - uses: actions/labeler@v2 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + - uses: actions/labeler@v2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index decf6501a0..eabd6a44c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,33 @@ * `closePollExactAfter` * `closePollAfter` +### 0.27.3 + +* `TelegramBotAPI`: + * `UpdateDeserializationStrategy` is publicly available now + * All `setWebhook` extensions was marked as deprecated, renamed and replaced into `TelegramBotAPI-extensions-utils` + * Typealias `ExceptionHandler` was added - it will be used for `handleSafely` + * `SetWebhook` factories signatures was changed (backward compatibility was not broken) + * `executeUnsafe` now working differently + * Now it is possible to pass exceptions handler into `executeUnsafe` + * `BasketballDiceAnimationType` was added + * `UnknownDiceAnimationType` now is deprecated due to renaming - currently it is typealias for `CustomDiceAnimationType` + * `CustomDiceAnimationType` now is `data` class instead of common class + * `FlowsUpdatesFilter` will use size 64 by default for internal broadcast channels +* `TelegramBotAPI-extensions-api`: + * Long Polling extensions now are deprecated in this project. It was replaced into `TelegramBotAPI-extensions-utils` + * Several `telegramBot` functions was renamed into `telegramBotWithCustomClientConfig` + * Add one more `setWebhookInfo` realisation +* `TelegramBotAPI-extensions-utils`: + * Extension `toTelegramUpdate` was added + * Long Polling extensions were added + * Updates utils were added + * New extensions `startListenWebhooks`, `setWebhookInfoAndStartListenWebhooks` and `includeWebhookHandlingInRoute` was added + * New extension `CoroutineScope#updateHandlerWithMediaGroupsAdaptation` was added + * New extension `flowsUpdatesFilter` was added +* `TelegramBotAPI-all`: + * Project was created + ### 0.27.2 * `Common`: diff --git a/README.md b/README.md index 0efe495c2e..b522762079 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ the list of this complex currently next projects: `RequestsExecutor`), which allows to use the core library in more pleasant way * [TelegramBotAPI Util Extensions](TelegramBotAPI-extensions-utils/README.md) - contains extensions for more comfortable work with commands, updates and other different things +* [TelegramBotAPI All](TelegramBotAPI-all/README.md) - concentration of all previously mentioned libraries Most part of some specific solves or unuseful moments are describing by official [Telegram Bot API](https://core.telegram.org/bots/api). @@ -62,15 +63,64 @@ kotlin { ## Ok, where should I start? -In most cases, the most simple way will be to implement -[TelegramBotAPI Extensions](TelegramBotAPI-extensions-api/README.md) and -[TelegramBotAPI Util Extensions](TelegramBotAPI-extensions-utils/README.md) for the reason that they contains more -simple tools. If you want to dive deeper in the core of library or develop something for it - welcome to +In most cases, the most simple way will be to implement [TelegramBotAPI All](TelegramBotAPI-all/README.md) - it contains +all necessary tools for comfort usage of this library. If you want to exclude some libraries, you can implement just +[TelegramBotAPI API Extensions](TelegramBotAPI-extensions-api/README.md), +[TelegramBotAPI Util Extensions](TelegramBotAPI-extensions-utils/README.md) or even [TelegramBotAPI](TelegramBotAPI/README.md). +If you want to dive deeper in the core of library or develop something for it - welcome to learn more from +[TelegramBotAPI](TelegramBotAPI/README.md) and our [Telegram Chat](https://teleg.one/InMoTelegramBotAPIChat). + Anyway, all libraries are very typical inside of them. Examples: * In `TelegramBotAPI` common request look like `requestsExecutor.execute(SomeRequest())` * `TelegramBotAPI-extensions-api` typical syntax look like `requestsExecutor.someRequest()` (in most cases it would be better to use `bot` name instead of `requestsExecutor`) * `TelegramBotAPI-extensions-utils` will look like `filter.filterBaseMessageUpdates(chatId).filterExactCommands(Regex("^.*$"))...` + +## Build instruction + +If you want to build this project or to contribute, there are several recommendations: + +### Build + +In case if you want to just build project, run next command: + +```bash +./gradlew clean build +``` + +On windows: + +``` +gradlew.bat clean build +``` + +### Publishing for work with your version locally + +In case, if you want to work in your other projects using your modification (or some state) of this library, +you can use next code: + +```bash +./gradlew clean build publishToMavenLocal +``` + +On windows: + +``` +gradlew.bat clean build publishToMavenLocal +``` + +But you must remember, that in this case your local maven repo must be the first one from +your project retrieving libraries: + +```groovy +repositories { + mavenLocal() // that must be the first one + jcenter() + mavenCentral() +} +``` + +Besides, for your own version you can change variable `library_version` in the file [gradle.properties](./gradle.properties). diff --git a/TelegramBotAPI-all/README.md b/TelegramBotAPI-all/README.md new file mode 100644 index 0000000000..4fc918fb4e --- /dev/null +++ b/TelegramBotAPI-all/README.md @@ -0,0 +1,16 @@ +# TelegramBotAPI-all + +Concentration of all TelegramBotAPI libraries: + +* [TelegramBotAPI](../TelegramBotAPI/README.md) +* [TelegramBotAPI Extensions](../TelegramBotAPI-extensions-api/README.md) +* [TelegramBotAPI Util Extensions](../TelegramBotAPI-extensions-utils/README.md) + +## Implementation + +```groovy +dependencies { + // ... + implementation "com.github.insanusmokrassar:TelegramBotAPI-all:tgBotAPIVersion" +} +``` diff --git a/TelegramBotAPI-all/build.gradle b/TelegramBotAPI-all/build.gradle new file mode 100644 index 0000000000..939d13307f --- /dev/null +++ b/TelegramBotAPI-all/build.gradle @@ -0,0 +1,52 @@ +buildscript { + repositories { + mavenLocal() + jcenter() + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version" + } +} + +plugins { + id "org.jetbrains.kotlin.multiplatform" version "$kotlin_version" + id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version" +} + +project.version = "$library_version" +project.group = "$library_group" + +apply from: "publish.gradle" + +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url "https://kotlin.bintray.com/kotlinx" } +} + +kotlin { + jvm() + js() + + sourceSets { + commonMain { + dependencies { + implementation kotlin('stdlib') + if ((project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true") { + api "${project.group}:TelegramBotAPI:$library_version" + api "${project.group}:TelegramBotAPI-extensions-api:$library_version" + api "${project.group}:TelegramBotAPI-extensions-utils:$library_version" + } else { + api project(":TelegramBotAPI") + api project(":TelegramBotAPI-extensions-api") + api project(":TelegramBotAPI-extensions-utils") + } + } + } + } +} diff --git a/TelegramBotAPI-all/maven.publish.gradle b/TelegramBotAPI-all/maven.publish.gradle new file mode 100644 index 0000000000..b81cdc6f73 --- /dev/null +++ b/TelegramBotAPI-all/maven.publish.gradle @@ -0,0 +1,57 @@ +apply plugin: 'maven-publish' + +task javadocsJar(type: Jar) { + classifier = 'javadoc' +} + +afterEvaluate { + project.publishing.publications.all { + // rename artifacts + groupId "${project.group}" + if (it.name.contains('kotlinMultiplatform')) { + artifactId = "${project.name}" + } else { + artifactId = "${project.name}-$name" + } + } +} + +publishing { + publications.all { + artifact javadocsJar + + pom.withXml { + asNode().children().last() + { + resolveStrategy = Closure.DELEGATE_FIRST + + description "This project just include all subproject of TelegramBotAPI" + name "Telegram Bot API All" + url "https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-all" + + scm { + developerConnection "scm:git:[fetch=]https://github.com/insanusmokrassar/TelegramBotAPI.git[push=]https://github.com/insanusmokrassar/TelegramBotAPI.git" + url "https://github.com/insanusmokrassar/TelegramBotAPI.git" + } + + developers { + + developer { + id "InsanusMokrassar" + name "Ovsiannikov Aleksei" + email "ovsyannikov.alexey95@gmail.com" + } + + } + + licenses { + + license { + name "Apache Software License 2.0" + url "https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE" + } + + } + } + } + } +} \ No newline at end of file diff --git a/TelegramBotAPI-all/mpp_publish_template.json b/TelegramBotAPI-all/mpp_publish_template.json new file mode 100644 index 0000000000..363a274ae2 --- /dev/null +++ b/TelegramBotAPI-all/mpp_publish_template.json @@ -0,0 +1 @@ +{"bintrayConfig":{"repo":"StandardRepository","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API All","description":"This project just include all subproject of TelegramBotAPI","url":"https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-all","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"} \ No newline at end of file diff --git a/TelegramBotAPI-all/publish.gradle b/TelegramBotAPI-all/publish.gradle new file mode 100644 index 0000000000..2fd72b3b1a --- /dev/null +++ b/TelegramBotAPI-all/publish.gradle @@ -0,0 +1,55 @@ +apply plugin: 'com.jfrog.bintray' + +apply from: "maven.publish.gradle" + +bintray { + user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER') + key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY') + filesSpec { + from "${buildDir}/publications/" + eachFile { + String directorySubname = it.getFile().parentFile.name + if (it.getName() == "module.json") { + if (directorySubname == "kotlinMultiplatform") { + it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module") + } else { + it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module") + } + } else { + if (directorySubname == "kotlinMultiplatform" && it.getName() == "pom-default.xml") { + it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.pom") + } else { + it.exclude() + } + } + } + into "${project.group}".replace(".", "/") + } + pkg { + repo = "StandardRepository" + name = "${project.name}" + vcsUrl = "https://github.com/InsanusMokrassar/TelegramBotAPI" + licenses = ["Apache-2.0"] + version { + name = "${project.version}" + released = new Date() + vcsTag = "${project.version}" + gpg { + sign = true + passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase') + } + } + } +} + +bintrayUpload.doFirst { + publications = publishing.publications.collect { + if (it.name.contains('kotlinMultiplatform')) { + null + } else { + it.name + } + } - null +} + +bintrayUpload.dependsOn publishToMavenLocal \ No newline at end of file diff --git a/TelegramBotAPI-extensions-api/README.md b/TelegramBotAPI-extensions-api/README.md index eeac67ba38..d49094f4d5 100644 --- a/TelegramBotAPI-extensions-api/README.md +++ b/TelegramBotAPI-extensions-api/README.md @@ -78,6 +78,10 @@ In all examples supposed that you have created bot. ## Updates +**Currently, these paragraphs almost outdated due to the fact that extensions for listening of updates and webhooks were +replaced into `TelegramBotAPI-extensions-utils`. But, most part of information below is correct with small fixes and +adding of `TelegramBotAPI-extensions-utils` dependency.** + Usually, it is more comfortable to use filter object to get separated types of updates: ```kotlin diff --git a/TelegramBotAPI-extensions-api/build.gradle b/TelegramBotAPI-extensions-api/build.gradle index 0cbe7c0136..217310d289 100644 --- a/TelegramBotAPI-extensions-api/build.gradle +++ b/TelegramBotAPI-extensions-api/build.gradle @@ -40,7 +40,7 @@ kotlin { if ((project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true") { api "${project.group}:TelegramBotAPI:$library_version" } else { - implementation project(":TelegramBotAPI") + api project(":TelegramBotAPI") } } } diff --git a/TelegramBotAPI-extensions-api/maven.publish.gradle b/TelegramBotAPI-extensions-api/maven.publish.gradle index 8fc16d04e8..ec352c0e8c 100644 --- a/TelegramBotAPI-extensions-api/maven.publish.gradle +++ b/TelegramBotAPI-extensions-api/maven.publish.gradle @@ -26,7 +26,7 @@ publishing { description "API extensions which provide work with RequestsExecutor of TelegramBotAPI almost like it is described in original Telegram Bot API reference" name "Telegram Bot API Extensions for API" - url "https://insanusmokrassar.github.io/TelegramBotAPI" + url "https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-extensions-api" scm { developerConnection "scm:git:[fetch=]https://github.com/insanusmokrassar/TelegramBotAPI.git[push=]https://github.com/insanusmokrassar/TelegramBotAPI.git" diff --git a/TelegramBotAPI-extensions-api/mpp_publish_template.json b/TelegramBotAPI-extensions-api/mpp_publish_template.json index 7558d0e4c0..3ae276449a 100644 --- a/TelegramBotAPI-extensions-api/mpp_publish_template.json +++ b/TelegramBotAPI-extensions-api/mpp_publish_template.json @@ -1 +1 @@ -{"bintrayConfig":{"repo":"StandardRepository","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API Extensions for API","description":"API extensions which provide work with RequestsExecutor of TelegramBotAPI almost like it is described in original Telegram Bot API reference","url":"https://insanusmokrassar.github.io/TelegramBotAPI","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"} \ No newline at end of file +{"bintrayConfig":{"repo":"StandardRepository","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API Extensions for API","description":"API extensions which provide work with RequestsExecutor of TelegramBotAPI almost like it is described in original Telegram Bot API reference","url":"https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-extensions-api","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"} \ No newline at end of file diff --git a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotBuilder.kt b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotBuilder.kt index 414f2a1d8d..fc8a7f8856 100644 --- a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotBuilder.kt +++ b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotBuilder.kt @@ -4,7 +4,8 @@ import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor import com.github.insanusmokrassar.TelegramBotAPI.utils.TelegramAPIUrlsKeeper import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.* +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.ProxyConfig /** * @param proxy Standard ktor [ProxyConfig] @@ -32,7 +33,7 @@ data class BotBuilder internal constructor( } /** - * @return Created by [telegramBot] function [RequestsExecutor]. This executor will be preconfigured using [token] and + * @return Created by [telegramBotWithCustomClientConfig] function [RequestsExecutor]. This executor will be preconfigured using [token] and * [block] */ fun telegramBot( diff --git a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotExtensions.kt b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotExtensions.kt index 44514ddeae..ef586bcae8 100644 --- a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotExtensions.kt +++ b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/BotExtensions.kt @@ -22,7 +22,7 @@ fun telegramBot( * Allows to create bot using bot [urlsKeeper] and specify [HttpClientEngine] by passing [clientEngine] param and optionally * configure [HttpClient] using [clientConfig] */ -fun telegramBot( +fun telegramBotWithCustomClientConfig( urlsKeeper: TelegramAPIUrlsKeeper, clientEngine: HttpClientEngine, clientConfig: HttpClientConfig<*>.() -> Unit = {} @@ -34,7 +34,7 @@ fun telegramBot( /** * Allows to create bot using bot [urlsKeeper] and optionally configure [HttpClient] using [clientConfig] */ -fun telegramBot( +fun telegramBotWithCustomClientConfig( urlsKeeper: TelegramAPIUrlsKeeper, clientConfig: HttpClientConfig<*>.() -> Unit = {} ): RequestsExecutor = telegramBot( @@ -47,7 +47,7 @@ fun telegramBot( */ fun telegramBot( token: String -): RequestsExecutor = telegramBot(TelegramAPIUrlsKeeper(token)) +): RequestsExecutor = telegramBotWithCustomClientConfig(TelegramAPIUrlsKeeper(token)) /** * Allows to create bot using bot [token] and already prepared [client] @@ -60,10 +60,10 @@ fun telegramBot( /** * Allows to create bot using bot [token] and configure [HttpClient] using [clientConfig] */ -fun telegramBot( +fun telegramBotWithCustomClientConfig( token: String, clientConfig: HttpClientConfig<*>.() -> Unit -): RequestsExecutor = telegramBot(TelegramAPIUrlsKeeper(token), clientConfig) +): RequestsExecutor = telegramBotWithCustomClientConfig(TelegramAPIUrlsKeeper(token), clientConfig) /** * Allows to create bot using bot [token] and specify [HttpClientEngine] by passing [clientEngine] param and optionally @@ -73,4 +73,4 @@ fun telegramBot( token: String, clientEngine: HttpClientEngine, clientConfig: HttpClientConfig<*>.() -> Unit = {} -): RequestsExecutor = telegramBot(TelegramAPIUrlsKeeper(token), clientEngine, clientConfig) +): RequestsExecutor = telegramBotWithCustomClientConfig(TelegramAPIUrlsKeeper(token), clientEngine, clientConfig) diff --git a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/JsonUtils.kt b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/JsonUtils.kt new file mode 100644 index 0000000000..a88439d920 --- /dev/null +++ b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/JsonUtils.kt @@ -0,0 +1,11 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.api + +import kotlinx.serialization.json.Json + +@Suppress("EXPERIMENTAL_API_USAGE") +internal val nonstrictJsonFormat = Json { + isLenient = true + ignoreUnknownKeys = true + serializeSpecialFloatingPointValues = true + useArrayPolymorphism = true +} diff --git a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/updates/UpdatesPolling.kt b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/updates/UpdatesPolling.kt index 6a078b810a..4c05dc0a95 100644 --- a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/updates/UpdatesPolling.kt +++ b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/updates/UpdatesPolling.kt @@ -10,14 +10,14 @@ import com.github.insanusmokrassar.TelegramBotAPI.types.update.* import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.* import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.* -import com.github.insanusmokrassar.TelegramBotAPI.utils.PreviewFeature -import com.github.insanusmokrassar.TelegramBotAPI.utils.handleSafely +import com.github.insanusmokrassar.TelegramBotAPI.utils.* import kotlinx.coroutines.* +@Deprecated("Replaced and renamed in TelegramBotAPI-extensions-utils") fun RequestsExecutor.startGettingOfUpdates( timeoutSeconds: Seconds = 30, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), - exceptionsHandler: (suspend (Exception) -> Unit)? = null, + exceptionsHandler: (ExceptionHandler)? = null, allowedUpdates: List? = null, updatesReceiver: UpdateReceiver ): Job = scope.launch { @@ -71,6 +71,7 @@ fun RequestsExecutor.startGettingOfUpdates( @FlowPreview @PreviewFeature @Suppress("unused") +@Deprecated("Replaced and renamed in TelegramBotAPI-extensions-utils") fun RequestsExecutor.startGettingFlowsUpdates( timeoutSeconds: Seconds = 30, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), @@ -82,6 +83,7 @@ fun RequestsExecutor.startGettingFlowsUpdates( startGettingOfUpdates(timeoutSeconds, scope, exceptionsHandler, allowedUpdates, asUpdateReceiver) } +@Deprecated("Replaced and renamed in TelegramBotAPI-extensions-utils") fun RequestsExecutor.startGettingOfUpdates( updatesFilter: UpdatesFilter, timeoutSeconds: Seconds = 30, @@ -95,6 +97,7 @@ fun RequestsExecutor.startGettingOfUpdates( updatesFilter.asUpdateReceiver ) +@Deprecated("Replaced and renamed in TelegramBotAPI-extensions-utils") fun RequestsExecutor.startGettingOfUpdates( messageCallback: UpdateReceiver? = null, messageMediaGroupCallback: UpdateReceiver? = null, @@ -140,6 +143,7 @@ fun RequestsExecutor.startGettingOfUpdates( } @Suppress("unused") +@Deprecated("Replaced and renamed in TelegramBotAPI-extensions-utils") fun RequestsExecutor.startGettingOfUpdates( messageCallback: UpdateReceiver? = null, mediaGroupCallback: UpdateReceiver? = null, diff --git a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/utils/UpdatesHandling.kt b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/utils/UpdatesHandling.kt new file mode 100644 index 0000000000..98d3e67f4f --- /dev/null +++ b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/utils/UpdatesHandling.kt @@ -0,0 +1,49 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.api.utils + +import com.github.insanusmokrassar.TelegramBotAPI.extensions.api.InternalUtils.convertWithMediaGroupUpdates +import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.UpdateReceiver +import com.github.insanusmokrassar.TelegramBotAPI.utils.extensions.accumulateByKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch + +/** + * Create [UpdateReceiver] object which will correctly accumulate updates and send into output updates which INCLUDE + * [com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.MediaGroupUpdate]s. + * + * @see UpdateReceiver + */ +fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation( + output: UpdateReceiver, + mediaGroupsDebounceMillis: Long = 1000L +): UpdateReceiver { + val updatesChannel = Channel(Channel.UNLIMITED) + val mediaGroupChannel = Channel>(Channel.UNLIMITED) + val mediaGroupAccumulatedChannel = mediaGroupChannel.accumulateByKey( + mediaGroupsDebounceMillis, + scope = this + ) + + launch { + launch { + for (update in updatesChannel) { + when (val data = update.data) { + is MediaGroupMessage -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate) + else -> output(update) + } + } + } + launch { + for ((_, mediaGroup) in mediaGroupAccumulatedChannel) { + mediaGroup.convertWithMediaGroupUpdates().forEach { + output(it) + } + } + } + } + + return { updatesChannel.send(it) } +} diff --git a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/webhook/SetWebhook.kt b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/webhook/SetWebhookInfo.kt similarity index 65% rename from TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/webhook/SetWebhook.kt rename to TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/webhook/SetWebhookInfo.kt index 7d2e45d493..5878a01717 100644 --- a/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/webhook/SetWebhook.kt +++ b/TelegramBotAPI-extensions-api/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/api/webhook/SetWebhookInfo.kt @@ -5,6 +5,22 @@ import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.FileId import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.MultipartFile import com.github.insanusmokrassar.TelegramBotAPI.requests.webhook.SetWebhook +/** + * Use this method to send information about webhook (like [url]) + */ +suspend fun RequestsExecutor.setWebhookInfo( + url: String, + maxAllowedConnections: Int? = null, + allowedUpdates: List? = null +) = execute( + SetWebhook( + url, maxAllowedConnections, allowedUpdates + ) +) + +/** + * Use this method to send information about webhook (like [url] and [certificate]) + */ suspend fun RequestsExecutor.setWebhookInfo( url: String, certificate: FileId, @@ -16,6 +32,9 @@ suspend fun RequestsExecutor.setWebhookInfo( ) ) +/** + * Use this method to send information about webhook (like [url] and [certificate]) + */ suspend fun RequestsExecutor.setWebhookInfo( url: String, certificate: MultipartFile, diff --git a/TelegramBotAPI-extensions-utils/README.md b/TelegramBotAPI-extensions-utils/README.md index b4b1e5bb66..e2078e7f8b 100644 --- a/TelegramBotAPI-extensions-utils/README.md +++ b/TelegramBotAPI-extensions-utils/README.md @@ -1,5 +1,23 @@ # TelegramBotAPI Util Extensions +- [TelegramBotAPI Util Extensions](#telegrambotapi-util--extensions) + * [What is it?](#what-is-it-) + * [How to implement library?](#how-to-implement-library-) + + [Maven](#maven) + + [Gradle](#gradle) + * [How to use?](#how-to-use-) + + [Updates](#updates) + - [Long polling](#long-polling) + - [WebHooks (currently JVM-only)](#webhooks--currently-jvm-only-) + + [Filters](#filters) + - [Sent messages](#sent-messages) + * [Common messages](#common-messages) + * [Chat actions](#chat-actions) + + [Shortcuts](#shortcuts) + - [ScheduledCloseInfo](#scheduledcloseinfo) + +Table of contents generated with markdown-toc + [![Download](https://api.bintray.com/packages/insanusmokrassar/StandardRepository/TelegramBotAPI-extensions-utils/images/download.svg) ](https://bintray.com/insanusmokrassar/StandardRepository/TelegramBotAPI-extensions-utils/_latestVersion) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.insanusmokrassar/TelegramBotAPI-extensions-utils/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.insanusmokrassar/TelegramBotAPI-extensions-utils) @@ -60,13 +78,90 @@ val filter = FlowsUpdatesFilter(64) Alternative way to use the things below: ```kotlin -val filter = bot.startGettingUpdates( +val filter = bot.startGettingFlowsUpdatesByLongPolling( scope = CoroutineScope(Dispatchers.Default) ) { // place code from examples here with replacing of `filter` by `this` } ``` +### Updates + +As mentioned in [Telegram Bot API reference](https://core.telegram.org/bots/api#getting-updates), there are two ways for +updates retrieving: + +* Webhooks +* Long Polling + +Both of them you could use in your project using [TelegramBotAPI](../TelegramBotAPI/README.md), but here there are +several useful extensions for both of them. + +Anyway, in both of ways it will be useful to know that it is possible to create `UpdateReceiver` object using function +`flowsUpdatesFilter`: + +```kotlin +val internalChannelsSizes = 128 +flowsUpdatesFilter(internalChannelsSizes/* default is 64 */) { + /* ... */ +} +``` + +#### Long polling + +The most simple way is Long Polling and one of the usages was mentioned above: + +```kotlin +val filter = bot.startGettingFlowsUpdatesByLongPolling( + scope = CoroutineScope(Dispatchers.Default) +) { + // place code from examples here with replacing of `filter` by `this` +} +``` + +Extension `startGettingFlowsUpdatesByLongPolling` was used in this example, but there are a lot of variations of +`startGettingOfUpdatesByLongPolling` and others for getting the same result. Usually, it is supposed that you already +have created `filter` object (or something like this) and will pass it into extension: + +```kotlin +val filter = FlowsUpdatesFilter(64) +bot.startGettingOfUpdatesByLongPolling( + filter +) +``` + +But also there are extensions which allow to pass lambdas directly: + +```kotlin +bot.startGettingOfUpdatesByLongPolling( + { + println("Received message update: $it") + } +) +``` + +Anyway, it is strictly recommended to pass your `CoroutineScope` object to this method at least for more comfortable +management of updates. + +#### WebHooks (currently JVM-only) + +For webhooks there are less number of functions and extensions than for Long Polling (but it is still fully automated): + +```kotlin +startListenWebhooks( + 8081, + CIO // require to implement this engine dependency +) { + // here will be all updates one by one in $it +} +``` + +Besides, there are two additional opportunities: + +* Extension `Route#includeWebhookHandlingInRoute`, which allow you to include webhook processing inside your ktor +application without creating of new one server (as it is happening in `startListenWebhooks`) +* Extension `RequestsExecutor#setWebhookInfoAndStartListenWebhooks`. It is allow to set up full server (in fact, with +`startListenWebhooks`), but also send `SetWebhook` request before and check that it was successful + ### Filters There are several filters for flows. @@ -136,11 +231,11 @@ filter.messageFlow.asChatEventsFlow().onlySupergroupEvents().onEach { ) ``` -## Shortcuts +### Shortcuts With shortcuts you are able to use simple factories for several things. -### ScheduledCloseInfo +#### ScheduledCloseInfo In case if you are creating some poll, you able to use next shortcuts. diff --git a/TelegramBotAPI-extensions-utils/build.gradle b/TelegramBotAPI-extensions-utils/build.gradle index 0cbe7c0136..217310d289 100644 --- a/TelegramBotAPI-extensions-utils/build.gradle +++ b/TelegramBotAPI-extensions-utils/build.gradle @@ -40,7 +40,7 @@ kotlin { if ((project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true") { api "${project.group}:TelegramBotAPI:$library_version" } else { - implementation project(":TelegramBotAPI") + api project(":TelegramBotAPI") } } } diff --git a/TelegramBotAPI-extensions-utils/maven.publish.gradle b/TelegramBotAPI-extensions-utils/maven.publish.gradle index 1f2fd3ed2a..61f4b51a17 100644 --- a/TelegramBotAPI-extensions-utils/maven.publish.gradle +++ b/TelegramBotAPI-extensions-utils/maven.publish.gradle @@ -26,7 +26,7 @@ publishing { description "Util extensions for more useful work with updates and other things" name "Telegram Bot API Utility Extensions" - url "https://insanusmokrassar.github.io/TelegramBotAPI" + url "https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-extensions-utils" scm { developerConnection "scm:git:[fetch=]https://github.com/insanusmokrassar/TelegramBotAPI.git[push=]https://github.com/insanusmokrassar/TelegramBotAPI.git" diff --git a/TelegramBotAPI-extensions-utils/mpp_publish_template.json b/TelegramBotAPI-extensions-utils/mpp_publish_template.json index d5c147454d..c51f9e348d 100644 --- a/TelegramBotAPI-extensions-utils/mpp_publish_template.json +++ b/TelegramBotAPI-extensions-utils/mpp_publish_template.json @@ -1 +1 @@ -{"bintrayConfig":{"repo":"StandardRepository","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API Utility Extensions","description":"Util extensions for more useful work with updates and other things","url":"https://insanusmokrassar.github.io/TelegramBotAPI","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"} \ No newline at end of file +{"bintrayConfig":{"repo":"StandardRepository","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API Utility Extensions","description":"Util extensions for more useful work with updates and other things","url":"https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-extensions-utils","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"} \ No newline at end of file diff --git a/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/JsonFormat.kt b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/JsonFormat.kt new file mode 100644 index 0000000000..63977802c2 --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/JsonFormat.kt @@ -0,0 +1,11 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils + +import kotlinx.serialization.json.Json + +@Suppress("EXPERIMENTAL_API_USAGE") +internal val nonstrictJsonFormat = Json { + isLenient = true + ignoreUnknownKeys = true + serializeSpecialFloatingPointValues = true + useArrayPolymorphism = true +} diff --git a/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/FlowsUpdatesFactory.kt b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/FlowsUpdatesFactory.kt new file mode 100644 index 0000000000..08d3cb7b77 --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/FlowsUpdatesFactory.kt @@ -0,0 +1,17 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates + +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.FlowsUpdatesFilter + +/** + * Non-suspendable function for easy-to-use creating of [FlowsUpdatesFilter] and applying the block to it + * + * @see flowsUpdatesFilter + */ +inline fun flowsUpdatesFilter( + internalChannelsSizes: Int = 64, + block: FlowsUpdatesFilter.() -> Unit +): FlowsUpdatesFilter { + val filter = FlowsUpdatesFilter(internalChannelsSizes) + filter.block() + return filter +} diff --git a/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/UpdateDeserialization.kt b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/UpdateDeserialization.kt new file mode 100644 index 0000000000..017a61400d --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/UpdateDeserialization.kt @@ -0,0 +1,31 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates + +import com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.nonstrictJsonFormat +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.UpdateDeserializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +/** + * @return Deserialize [source] as [com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update] + */ +fun Json.toTelegramUpdate(source: String) = parse(UpdateDeserializationStrategy, source) +/** + * @return Deserialize [source] as [com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update] + */ +fun Json.toTelegramUpdate(source: JsonElement) = fromJson(UpdateDeserializationStrategy, source) + +/** + * @return Deserialize [this] as [com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update]. In fact, + * it is must be JSON + * + * @see Json.toTelegramUpdate + */ +fun String.toTelegramUpdate() = nonstrictJsonFormat.toTelegramUpdate(this) +/** + * @return Deserialize [this] as [com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update] + * + * @see Json.toTelegramUpdate + */ +fun JsonElement.toTelegramUpdate() = nonstrictJsonFormat.toTelegramUpdate(this) + + diff --git a/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/UpdatesUtils.kt b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/UpdatesUtils.kt new file mode 100644 index 0000000000..6aa7bc6ba9 --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/UpdatesUtils.kt @@ -0,0 +1,93 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates + +import com.github.insanusmokrassar.TelegramBotAPI.types.MediaGroupIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.types.UpdateIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage +import com.github.insanusmokrassar.TelegramBotAPI.types.update.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.* + +/** + * @return If [this] is [SentMediaGroupUpdate] - [Update.updateId] of [last] element, or its own [Update.updateId] + */ +fun Update.lastUpdateIdentifier(): UpdateIdentifier { + return if (this is SentMediaGroupUpdate) { + origins.last().updateId + } else { + updateId + } +} + +/** + * @return The biggest [UpdateIdentifier] OR null + * + * @see [Update.lastUpdateIdentifier] + */ +fun List.lastUpdateIdentifier(): UpdateIdentifier? { + return maxBy { it.updateId } ?.lastUpdateIdentifier() +} + +/** + * Will convert incoming list of updates to list with [MediaGroupUpdate]s + */ +fun List.convertWithMediaGroupUpdates(): List { + val resultUpdates = mutableListOf() + val mediaGroups = mutableMapOf>() + for (update in this) { + val data = (update.data as? MediaGroupMessage) + if (data == null) { + resultUpdates.add(update) + continue + } + when (update) { + is BaseEditMessageUpdate -> resultUpdates.add( + update.toEditMediaGroupUpdate() + ) + is BaseSentMessageUpdate -> { + mediaGroups.getOrPut(data.mediaGroupId) { + mutableListOf() + }.add(update) + } + else -> resultUpdates.add(update) + } + } + mediaGroups.values.map { + it.toSentMediaGroupUpdate() ?.let { mediaGroupUpdate -> + resultUpdates.add(mediaGroupUpdate) + } + } + resultUpdates.sortBy { it.updateId } + return resultUpdates +} + +/** + * @receiver List of [BaseSentMessageUpdate] where [BaseSentMessageUpdate.data] is [MediaGroupMessage] and all messages + * have the same [MediaGroupMessage.mediaGroupId] + * @return [MessageMediaGroupUpdate] in case if [first] object of [this] is [MessageUpdate]. When [first] object is + * [ChannelPostUpdate] instance - will return [ChannelPostMediaGroupUpdate]. Otherwise will be returned null + */ +fun List.toSentMediaGroupUpdate(): SentMediaGroupUpdate? = (this as? SentMediaGroupUpdate) ?: let { + if (isEmpty()) { + return@let null + } + val resultList = sortedBy { it.updateId } + when (first()) { + is MessageUpdate -> MessageMediaGroupUpdate(resultList) + is ChannelPostUpdate -> ChannelPostMediaGroupUpdate(resultList) + else -> null + } +} + +/** + * @return [EditMessageMediaGroupUpdate] in case if [this] is [EditMessageUpdate]. When [this] object is + * [EditChannelPostUpdate] instance - will return [EditChannelPostMediaGroupUpdate] + * + * @throws IllegalStateException + */ +fun BaseEditMessageUpdate.toEditMediaGroupUpdate(): EditMediaGroupUpdate = (this as? EditMediaGroupUpdate) ?: let { + when (this) { + is EditMessageUpdate -> EditMessageMediaGroupUpdate(this) + is EditChannelPostUpdate -> EditChannelPostMediaGroupUpdate(this) + else -> error("Unsupported type of ${BaseEditMessageUpdate::class.simpleName}") + } +} diff --git a/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/LongPolling.kt b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/LongPolling.kt new file mode 100644 index 0000000000..10d9003d61 --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/LongPolling.kt @@ -0,0 +1,180 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.retrieving + +import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor +import com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.RequestException +import com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.convertWithMediaGroupUpdates +import com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.lastUpdateIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.requests.GetUpdates +import com.github.insanusmokrassar.TelegramBotAPI.types.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.* +import com.github.insanusmokrassar.TelegramBotAPI.utils.* +import kotlinx.coroutines.* + +fun RequestsExecutor.startGettingOfUpdatesByLongPolling( + timeoutSeconds: Seconds = 30, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + exceptionsHandler: (ExceptionHandler)? = null, + allowedUpdates: List? = null, + updatesReceiver: UpdateReceiver +): Job = scope.launch { + var lastUpdateIdentifier: UpdateIdentifier? = null + + while (isActive) { + handleSafely( + { e -> + exceptionsHandler ?.invoke(e) + if (e is RequestException) { + delay(1000L) + } + } + ) { + val updates = execute( + GetUpdates( + offset = lastUpdateIdentifier?.plus(1), + timeout = timeoutSeconds, + allowed_updates = allowedUpdates + ) + ).let { originalUpdates -> + val converted = originalUpdates.convertWithMediaGroupUpdates() + /** + * Dirty hack for cases when the media group was retrieved not fully: + * + * We are throw out the last media group and will reretrieve it again in the next get updates + * and it will guarantee that it is full + */ + if (originalUpdates.size == getUpdatesLimit.last && converted.last() is SentMediaGroupUpdate) { + converted - converted.last() + } else { + converted + } + } + + handleSafely { + for (update in updates) { + updatesReceiver(update) + + lastUpdateIdentifier = update.lastUpdateIdentifier() + } + } + } + } +} + +/** + * This method will create a new one [FlowsUpdatesFilter]. This method could be unsafe due to the fact that it will start + * getting updates IMMEDIATELY. That means that your bot will be able to skip some of them until you will call + * [kotlinx.coroutines.flow.Flow.collect] on one of [FlowsUpdatesFilter] flows. To avoid it, you can pass + * [flowUpdatesPreset] lambda - it will be called BEFORE starting updates getting + */ +@FlowPreview +@PreviewFeature +@Suppress("unused") +fun RequestsExecutor.startGettingFlowsUpdatesByLongPolling( + timeoutSeconds: Seconds = 30, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + exceptionsHandler: ExceptionHandler? = null, + flowsUpdatesFilterUpdatesKeeperCount: Int = 64, + flowUpdatesPreset: FlowsUpdatesFilter.() -> Unit = {} +): FlowsUpdatesFilter = FlowsUpdatesFilter(flowsUpdatesFilterUpdatesKeeperCount).apply { + flowUpdatesPreset() + startGettingOfUpdatesByLongPolling(timeoutSeconds, scope, exceptionsHandler, allowedUpdates, asUpdateReceiver) +} + +fun RequestsExecutor.startGettingOfUpdatesByLongPolling( + updatesFilter: UpdatesFilter, + timeoutSeconds: Seconds = 30, + exceptionsHandler: ExceptionHandler? = null, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default) +): Job = startGettingOfUpdatesByLongPolling( + timeoutSeconds, + scope, + exceptionsHandler, + updatesFilter.allowedUpdates, + updatesFilter.asUpdateReceiver +) + +fun RequestsExecutor.startGettingOfUpdatesByLongPolling( + messageCallback: UpdateReceiver? = null, + messageMediaGroupCallback: UpdateReceiver? = null, + editedMessageCallback: UpdateReceiver? = null, + editedMessageMediaGroupCallback: UpdateReceiver? = null, + channelPostCallback: UpdateReceiver? = null, + channelPostMediaGroupCallback: UpdateReceiver? = null, + editedChannelPostCallback: UpdateReceiver? = null, + editedChannelPostMediaGroupCallback: UpdateReceiver? = null, + chosenInlineResultCallback: UpdateReceiver? = null, + inlineQueryCallback: UpdateReceiver? = null, + callbackQueryCallback: UpdateReceiver? = null, + shippingQueryCallback: UpdateReceiver? = null, + preCheckoutQueryCallback: UpdateReceiver? = null, + pollCallback: UpdateReceiver? = null, + pollAnswerCallback: UpdateReceiver? = null, + timeoutSeconds: Seconds = 30, + exceptionsHandler: ExceptionHandler? = null, + scope: CoroutineScope = GlobalScope +): Job { + return startGettingOfUpdatesByLongPolling( + SimpleUpdatesFilter( + messageCallback, + messageMediaGroupCallback, + editedMessageCallback, + editedMessageMediaGroupCallback, + channelPostCallback, + channelPostMediaGroupCallback, + editedChannelPostCallback, + editedChannelPostMediaGroupCallback, + chosenInlineResultCallback, + inlineQueryCallback, + callbackQueryCallback, + shippingQueryCallback, + preCheckoutQueryCallback, + pollCallback, + pollAnswerCallback + ), + timeoutSeconds, + exceptionsHandler, + scope + ) +} + +@Suppress("unused") +fun RequestsExecutor.startGettingOfUpdatesByLongPolling( + messageCallback: UpdateReceiver? = null, + mediaGroupCallback: UpdateReceiver? = null, + editedMessageCallback: UpdateReceiver? = null, + channelPostCallback: UpdateReceiver? = null, + editedChannelPostCallback: UpdateReceiver? = null, + chosenInlineResultCallback: UpdateReceiver? = null, + inlineQueryCallback: UpdateReceiver? = null, + callbackQueryCallback: UpdateReceiver? = null, + shippingQueryCallback: UpdateReceiver? = null, + preCheckoutQueryCallback: UpdateReceiver? = null, + pollCallback: UpdateReceiver? = null, + pollAnswerCallback: UpdateReceiver? = null, + timeoutSeconds: Seconds = 30, + exceptionsHandler: ExceptionHandler? = null, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default) +): Job = startGettingOfUpdatesByLongPolling( + messageCallback = messageCallback, + messageMediaGroupCallback = mediaGroupCallback, + editedMessageCallback = editedMessageCallback, + editedMessageMediaGroupCallback = mediaGroupCallback, + channelPostCallback = channelPostCallback, + channelPostMediaGroupCallback = mediaGroupCallback, + editedChannelPostCallback = editedChannelPostCallback, + editedChannelPostMediaGroupCallback = mediaGroupCallback, + chosenInlineResultCallback = chosenInlineResultCallback, + inlineQueryCallback = inlineQueryCallback, + callbackQueryCallback = callbackQueryCallback, + shippingQueryCallback = shippingQueryCallback, + preCheckoutQueryCallback = preCheckoutQueryCallback, + pollCallback = pollCallback, + pollAnswerCallback = pollAnswerCallback, + timeoutSeconds = timeoutSeconds, + exceptionsHandler = exceptionsHandler, + scope = scope +) + diff --git a/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt new file mode 100644 index 0000000000..508c7f334e --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt @@ -0,0 +1,60 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.retrieving + +import com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.convertWithMediaGroupUpdates +import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.UpdateReceiver +import com.github.insanusmokrassar.TelegramBotAPI.utils.extensions.accumulateByKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch + + +/** + * Create [UpdateReceiver] object which will correctly accumulate updates and send into output updates which INCLUDE + * [com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.MediaGroupUpdate]s. + * + * @see UpdateReceiver + */ +fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation( + output: UpdateReceiver, + debounceTimeMillis: Long = 1000L +): UpdateReceiver { + val updatesChannel = Channel(Channel.UNLIMITED) + val mediaGroupChannel = Channel>(Channel.UNLIMITED) + val mediaGroupAccumulatedChannel = mediaGroupChannel.accumulateByKey( + debounceTimeMillis, + scope = this + ) + + launch { + launch { + for (update in updatesChannel) { + when (val data = update.data) { + is MediaGroupMessage -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate) + else -> output(update) + } + } + } + launch { + for ((_, mediaGroup) in mediaGroupAccumulatedChannel) { + mediaGroup.convertWithMediaGroupUpdates().forEach { + output(it) + } + } + } + } + + return { updatesChannel.send(it) } +} + +/** + * Create [UpdateReceiver] object which will correctly accumulate updates and send into output updates which INCLUDE + * [com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.MediaGroupUpdate]s. + * + * @see UpdateReceiver + */ +fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation( + output: UpdateReceiver +) = updateHandlerWithMediaGroupsAdaptation(output, 1000L) \ No newline at end of file diff --git a/TelegramBotAPI-extensions-utils/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/Webhook.kt b/TelegramBotAPI-extensions-utils/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/Webhook.kt new file mode 100644 index 0000000000..ea95854ba7 --- /dev/null +++ b/TelegramBotAPI-extensions-utils/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/extensions/utils/updates/retrieving/Webhook.kt @@ -0,0 +1,201 @@ +package com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.retrieving + +import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor +import com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.nonstrictJsonFormat +import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.MultipartFile +import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request +import com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base.MultipartRequestImpl +import com.github.insanusmokrassar.TelegramBotAPI.requests.webhook.SetWebhook +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.UpdateDeserializationStrategy +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.UpdateReceiver +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.UpdatesFilter +import com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.webhook.WebhookPrivateKeyConfig +import com.github.insanusmokrassar.TelegramBotAPI.utils.ExceptionHandler +import com.github.insanusmokrassar.TelegramBotAPI.utils.handleSafely +import io.ktor.application.call +import io.ktor.request.receiveText +import io.ktor.response.respond +import io.ktor.routing.* +import io.ktor.server.engine.* +import kotlinx.coroutines.* +import java.util.concurrent.Executors + + +/** + * Allows to include webhook in custom route everywhere in your server + * + * @param [scope] Will be used for mapping of media groups + * @param [exceptionsHandler] Pass this parameter to set custom exception handler for getting updates + * @param [block] Some receiver block like [com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.FlowsUpdatesFilter] + * + * @see com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.FlowsUpdatesFilter + * @see UpdatesFilter + * @see UpdatesFilter.asUpdateReceiver + */ +fun Route.includeWebhookHandlingInRoute( + scope: CoroutineScope, + exceptionsHandler: ExceptionHandler? = null, + block: UpdateReceiver +) { + val transformer = scope.updateHandlerWithMediaGroupsAdaptation(block) + post { + handleSafely( + exceptionsHandler ?: {} + ) { + val asJson = + nonstrictJsonFormat.parseJson(call.receiveText()) + val update = nonstrictJsonFormat.fromJson( + UpdateDeserializationStrategy, + asJson + ) + transformer(update) + } + call.respond("Ok") + } +} + +/** + * Setting up ktor server, set webhook info via [SetWebhook] request. + * + * @param listenPort port which will be listen by bot + * @param listenRoute address to listen by bot. If null - will be set up in root of host + * @param scope Scope which will be used for + * @param privateKeyConfig If configured - server will be created with [sslConnector]. [connector] will be used otherwise + * + * @see com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.FlowsUpdatesFilter + * @see UpdatesFilter + * @see UpdatesFilter.asUpdateReceiver + */ +fun startListenWebhooks( + listenPort: Int, + engineFactory: ApplicationEngineFactory<*, *>, + exceptionsHandler: ExceptionHandler, + listenHost: String = "0.0.0.0", + listenRoute: String? = null, + privateKeyConfig: WebhookPrivateKeyConfig? = null, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + block: UpdateReceiver +): ApplicationEngine { + lateinit var engine: ApplicationEngine + val env = applicationEngineEnvironment { + + module { + routing { + listenRoute ?.also { + createRouteFromPath(it).includeWebhookHandlingInRoute(scope, exceptionsHandler, block) + } ?: includeWebhookHandlingInRoute(scope, exceptionsHandler, block) + } + } + privateKeyConfig ?.let { + sslConnector( + privateKeyConfig.keyStore, + privateKeyConfig.aliasName, + privateKeyConfig::keyStorePassword, + privateKeyConfig::aliasPassword + ) { + host = listenHost + port = listenPort + } + } ?: connector { + host = listenHost + port = listenPort + } + + } + engine = embeddedServer(engineFactory, env) + engine.start(false) + + return engine +} + +private suspend fun RequestsExecutor.internalSetWebhookInfoAndStartListenWebhooks( + listenPort: Int, + engineFactory: ApplicationEngineFactory<*, *>, + setWebhookRequest: Request, + exceptionsHandler: ExceptionHandler = {}, + listenHost: String = "0.0.0.0", + listenRoute: String? = null, + privateKeyConfig: WebhookPrivateKeyConfig? = null, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + block: UpdateReceiver +): Job { + return try { + execute(setWebhookRequest) + val engine = startListenWebhooks(listenPort, engineFactory, exceptionsHandler, listenHost, listenRoute, privateKeyConfig, scope, block) + scope.launch { + engine.environment.parentCoroutineContext[Job] ?.join() + engine.stop(1000, 5000) + } + } catch (e: Exception) { + throw e + } +} + +/** + * Setting up ktor server, set webhook info via [SetWebhook] request. + * + * @param listenPort port which will be listen by bot + * @param listenRoute address to listen by bot + * @param scope Scope which will be used for + * + * @see com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.FlowsUpdatesFilter + * @see UpdatesFilter + * @see UpdatesFilter.asUpdateReceiver + */ +@Suppress("unused") +suspend fun RequestsExecutor.setWebhookInfoAndStartListenWebhooks( + listenPort: Int, + engineFactory: ApplicationEngineFactory<*, *>, + setWebhookRequest: SetWebhook, + exceptionsHandler: ExceptionHandler = {}, + listenHost: String = "0.0.0.0", + listenRoute: String = "/", + privateKeyConfig: WebhookPrivateKeyConfig? = null, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + block: UpdateReceiver +): Job = internalSetWebhookInfoAndStartListenWebhooks( + listenPort, + engineFactory, + setWebhookRequest as Request, + exceptionsHandler, + listenHost, + listenRoute, + privateKeyConfig, + scope, + block +) + +/** + * Setting up ktor server, set webhook info via [SetWebhook] request. + * + * @param listenPort port which will be listen by bot + * @param listenRoute address to listen by bot + * @param scope Scope which will be used for + * + * @see com.github.insanusmokrassar.TelegramBotAPI.updateshandlers.FlowsUpdatesFilter + * @see UpdatesFilter + * @see UpdatesFilter.asUpdateReceiver + */ +@Suppress("unused") +suspend fun RequestsExecutor.setWebhookInfoAndStartListenWebhooks( + listenPort: Int, + engineFactory: ApplicationEngineFactory<*, *>, + setWebhookRequest: MultipartRequestImpl, Boolean>, + exceptionsHandler: ExceptionHandler = {}, + listenHost: String = "0.0.0.0", + listenRoute: String? = null, + privateKeyConfig: WebhookPrivateKeyConfig? = null, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + block: UpdateReceiver +): Job = internalSetWebhookInfoAndStartListenWebhooks( + listenPort, + engineFactory, + setWebhookRequest as Request, + exceptionsHandler, + listenHost, + listenRoute, + privateKeyConfig, + scope, + block +) diff --git a/TelegramBotAPI/README.md b/TelegramBotAPI/README.md index 87db0f0558..67536e6141 100644 --- a/TelegramBotAPI/README.md +++ b/TelegramBotAPI/README.md @@ -149,53 +149,3 @@ Here was used `okhttp` realisation of client, but there are several others engin available on ktor.io site for [client](https://ktor.io/clients/http-client/engines.html) and [server](https://ktor.io/quickstart/artifacts.html) engines. -## Getting updates - -In this library currently realised two ways to get updates from telegram: - -* Polling - in this case bot will request updates from time to time (you can set up delay between requests) -* Webhook via reverse proxy or something like this - -### Updates filters - -Currently webhook method contains `UpdatesFilter` as necessary argument for getting updates. -`UpdatesFilter` will sort updates and throw their into different callbacks. Currently supporting -separate getting updates for media groups - they are accumulating with debounce in one second -(for being sure that all objects of media group was received). - -Updates polling also support `UpdatesFilter` but it is not required to use it and you can get updates directly -in `UpdateReceiver`, which you will provide to `startGettingOfUpdates` method - -### Webhook set up - -If you wish to use webhook method, you will need: - -* White IP - your IP address or host, which available for calling. [TelegramBotAPI](https://core.telegram.org/bots/api#setwebhook) -recommend to use some unique address for each bot which you are using -* SSL certificate. Usually you can obtain the certificate using your domain provider, [Let'sEncrypt](https://letsencrypt.org/) or [create it](https://core.telegram.org/bots/self-signed) -* Nginx or something like this - -Template for Nginx server config you can find in [this gist](https://gist.github.com/InsanusMokrassar/fcc6e09cebd07e46e8f0fdec234750c4#file-nginxssl-conf). - -For webhook you can provide `File` with public part of certificate, `URL` where bot will be available and inner `PORT` which -will be used to start receiving of updates. Actually, you can skip passing of `File` when you have something like -nginx for proxy forwarding. - -In case of using `nginx` with reverse-proxy config, setting up of Webhook will look like: - -```kotlin -requestsExecutor.setWebhook( - WEBHOOK_URL, - INTERNAL_PORT, - filter, - ENGINE_FACTORY -) -``` - -Here: - -* `WEBHOOK_URL` - the url which will be used by Telegram system to send updates -* `INTERNAL_PORT` - the port which will be used in bot for listening of updates -* `filter` - instance of [UpdatesFilter](https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/updateshandlers/UpdatesFilter.kt), -which will be used to filter incoming updates -* `ENGINE_FACTORY` - used factory name, for example, `CIO` in case of usage `io.ktor:ktor-server-cio` as server engine diff --git a/TelegramBotAPI/build.gradle b/TelegramBotAPI/build.gradle index d42e420763..583e4cf72d 100644 --- a/TelegramBotAPI/build.gradle +++ b/TelegramBotAPI/build.gradle @@ -64,6 +64,8 @@ kotlin { api "io.ktor:ktor-server-host-common:$ktor_version" api "io.ktor:ktor-client-cio:$ktor_version" + + api "javax.activation:activation:$javax_activation_version" } } jvmTest { @@ -81,13 +83,4 @@ kotlin { } } } - - - targets.all { - compilations.all { - kotlinOptions { - freeCompilerArgs += ["-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", "-Xopt-in=kotlin.RequiresOptIn"] - } - } - } } diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/RequestsExecutor.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/RequestsExecutor.kt index 285793254b..13c531a527 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/RequestsExecutor.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/RequestsExecutor.kt @@ -3,9 +3,20 @@ package com.github.insanusmokrassar.TelegramBotAPI.bot import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request import io.ktor.utils.io.core.Closeable +/** + * Interface for making requests to Telegram Bot API. Currently, there is only one built-in implementation - + * [com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorRequestsExecutor] + * + * @see Request + * @see com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorRequestsExecutor + */ interface RequestsExecutor : Closeable { /** - * @throws com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.RequestException + * Unsafe execution of incoming [request]. Can throw almost any exception. So, it is better to use + * something like [com.github.insanusmokrassar.TelegramBotAPI.utils.extensions.executeAsync] or + * [com.github.insanusmokrassar.TelegramBotAPI.utils.extensions.executeUnsafe] + * + * @throws Exception */ suspend fun execute(request: Request): T } \ No newline at end of file diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt index bb682b4063..792664a3e4 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt @@ -11,6 +11,15 @@ private val updatesListSerializer = ListSerializer( UpdateSerializerWithoutSerialization ) +/** + * Request updates from Telegram Bot API system. It is important, that the result updates WILL NOT include + * [com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.MediaGroupUpdate] objects due to the fact, + * that it is internal abstraction and in fact any [com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage] + * is just a common [com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.Message] + * + * @see com.github.insanusmokrassar.TelegramBotAPI.extensions.utils.updates.retrieving.updateHandlerWithMediaGroupsAdaptation + * @see com.github.insanusmokrassar.TelegramBotAPI.utils.convertWithMediaGroupUpdates + */ @Serializable data class GetUpdates( val offset: UpdateIdentifier? = null,// set `last update id + 1` to receive next part of updates diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt index 43c53b89aa..e662818d14 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt @@ -7,33 +7,56 @@ import com.github.insanusmokrassar.TelegramBotAPI.types.* import kotlinx.serialization.* import kotlinx.serialization.builtins.serializer +private fun correctWebhookUrl(sourceUrl: String) = if (sourceUrl.contains("://")) { + sourceUrl +} else { + "https://$sourceUrl" +} + +fun SetWebhook( + url: String, + certificate: MultipartFile, + maxAllowedConnections: Int? = null, + allowedUpdates: List? = null +): MultipartRequestImpl, Boolean> = MultipartRequestImpl( + SetWebhook( + correctWebhookUrl(url), + null, + maxAllowedConnections, + allowedUpdates + ), + mapOf(certificateField to certificate) +) + +fun SetWebhook( + url: String, + certificate: FileId, + maxAllowedConnections: Int? = null, + allowedUpdates: List? = null +): SetWebhook = SetWebhook( + correctWebhookUrl(url), + certificate.fileId, + maxAllowedConnections, + allowedUpdates +) + +@Suppress("USELESS_CAST") fun SetWebhook( url: String, certificate: InputFile, maxAllowedConnections: Int? = null, allowedUpdates: List? = null -) : Request { - val data = SetWebhook( - url, - (certificate as? FileId) ?.fileId, - maxAllowedConnections, - allowedUpdates - ) - return when (certificate) { - is FileId -> data - is MultipartFile -> MultipartRequestImpl( - data, - mapOf(certificateField to certificate) - ) - } +): Request = when (certificate) { + is MultipartFile -> SetWebhook(correctWebhookUrl(url), certificate as MultipartFile, maxAllowedConnections, allowedUpdates) + is FileId -> SetWebhook(correctWebhookUrl(url), certificate as FileId, maxAllowedConnections, allowedUpdates) } fun SetWebhook( url: String, maxAllowedConnections: Int? = null, allowedUpdates: List? = null -) : Request = SetWebhook( - url, +) = SetWebhook( + correctWebhookUrl(url), null, maxAllowedConnections, allowedUpdates diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt index 7930f11d22..037e6e6815 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt @@ -7,7 +7,7 @@ data class WebhookInfo( @SerialName(urlField) val url: String, @SerialName(pendingUpdateCountField) - val awaitDeliery: Int, + val awaitDelivery: Int, @SerialName(maxAllowedConnectionsField) val maxConnections: Int = 40, // default count according to documentation @SerialName(hasCustomCertificateField) diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/dice/DiceAnimationType.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/dice/DiceAnimationType.kt index 28b4836d15..0c51776d74 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/dice/DiceAnimationType.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/dice/DiceAnimationType.kt @@ -15,9 +15,15 @@ object DartsDiceAnimationType : DiceAnimationType() { override val emoji: String = "\uD83C\uDFAF" } @Serializable(DiceAnimationTypeSerializer::class) -class UnknownDiceAnimationType( +object BasketballDiceAnimationType : DiceAnimationType() { + override val emoji: String = "\uD83C\uDFC0" +} +@Serializable(DiceAnimationTypeSerializer::class) +data class CustomDiceAnimationType( override val emoji: String ) : DiceAnimationType() +@Deprecated("Renamed", ReplaceWith("CustomDiceAnimationType")) +typealias UnknownDiceAnimationType = CustomDiceAnimationType @Serializer(DiceAnimationType::class) internal object DiceAnimationTypeSerializer : KSerializer { @@ -26,7 +32,8 @@ internal object DiceAnimationTypeSerializer : KSerializer { return when (val type = decoder.decodeString()) { CubeDiceAnimationType.emoji -> CubeDiceAnimationType DartsDiceAnimationType.emoji -> DartsDiceAnimationType - else -> UnknownDiceAnimationType(type) + BasketballDiceAnimationType.emoji -> BasketballDiceAnimationType + else -> CustomDiceAnimationType(type) } } diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdates/MediaGroupUpdate.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdates/MediaGroupUpdate.kt index 5724d2d9b6..6a9936cfcf 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdates/MediaGroupUpdate.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdates/MediaGroupUpdate.kt @@ -3,6 +3,13 @@ package com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdate import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.* +/** + * By default there is no instances of objects which could be deserialized from raw updates. If you want to get objects + * with this type, you should use something like [com.github.insanusmokrassar.TelegramBotAPI.extensions.api.SetWebhookKt.includeWebhookInRoute] + * + * @see com.github.insanusmokrassar.TelegramBotAPI.extensions.api.SetWebhookKt.includeWebhookInRoute + * @see com.github.insanusmokrassar.TelegramBotAPI.extensions.api.updates.UpdatesPollingKt.startGettingOfUpdates + */ interface MediaGroupUpdate : Update interface SentMediaGroupUpdate: MediaGroupUpdate { diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/abstracts/Update.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/abstracts/Update.kt index fa2129f854..874d3860fc 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/abstracts/Update.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/abstracts/Update.kt @@ -26,7 +26,14 @@ internal object UpdateSerializerWithoutSerialization : KSerializer { override fun serialize(encoder: Encoder, value: Update) = throw UnsupportedOperationException() } -internal object UpdateDeserializationStrategy : DeserializationStrategy { +/** + * Use this object to deserialize objects with type [Update]. Currently it is restricted to use this + * [DeserializationStrategy] only with JSON + * + * @see StringFormat.parse + * @see kotlinx.serialization.json.Json.parse + */ +object UpdateDeserializationStrategy : DeserializationStrategy { override val descriptor: SerialDescriptor = JsonElementSerializer.descriptor override fun patch(decoder: Decoder, old: Update): Update = throw UpdateNotSupportedException("Update") diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/updateshandlers/FlowsUpdatesFilter.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/updateshandlers/FlowsUpdatesFilter.kt index ffde4ccb35..052e1a8f03 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/updateshandlers/FlowsUpdatesFilter.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/updateshandlers/FlowsUpdatesFilter.kt @@ -3,17 +3,16 @@ package com.github.insanusmokrassar.TelegramBotAPI.updateshandlers import com.github.insanusmokrassar.TelegramBotAPI.types.update.* import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdates.* import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow +@Suppress("EXPERIMENTAL_API_USAGE") private fun BroadcastChannel.createUpdateReceiver(): UpdateReceiver = ::send -@FlowPreview +@Suppress("EXPERIMENTAL_API_USAGE", "unused") class FlowsUpdatesFilter( - broadcastChannelsSize: Int = Channel.CONFLATED + broadcastChannelsSize: Int = 64 ): UpdatesFilter { private val messageChannel: BroadcastChannel = BroadcastChannel(broadcastChannelsSize) private val messageMediaGroupChannel: BroadcastChannel = BroadcastChannel(broadcastChannelsSize) diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/HandleSafely.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/HandleSafely.kt index 6e8deeaae8..955cfe7bb7 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/HandleSafely.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/HandleSafely.kt @@ -3,6 +3,8 @@ package com.github.insanusmokrassar.TelegramBotAPI.utils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.supervisorScope + +typealias ExceptionHandler = suspend (Exception) -> T /** * It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions * @@ -10,7 +12,7 @@ import kotlinx.coroutines.supervisorScope * exception will be available for catching */ suspend inline fun handleSafely( - noinline onException: suspend (Exception) -> T = { throw it }, + noinline onException: ExceptionHandler = { throw it }, noinline block: suspend CoroutineScope.() -> T ): T { return try { diff --git a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt index ebb3f462ae..6e4a4f50ef 100644 --- a/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt +++ b/TelegramBotAPI/src/commonMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt @@ -4,6 +4,7 @@ import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor import com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.RequestException import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request import com.github.insanusmokrassar.TelegramBotAPI.types.Response +import com.github.insanusmokrassar.TelegramBotAPI.utils.handleSafely import kotlinx.coroutines.* @@ -33,16 +34,23 @@ fun RequestsExecutor.executeAsync( suspend fun RequestsExecutor.executeUnsafe( request: Request, retries: Int = 0, - retriesDelay: Long = 1000L + retriesDelay: Long = 1000L, + onAllFailed: (suspend (exceptions: Array) -> Unit)? = null ): T? { var leftRetries = retries + val exceptions = onAllFailed ?.let { mutableListOf() } do { - try { - return execute(request) - } catch (e: RequestException) { - leftRetries-- - delay(retriesDelay) - } + handleSafely( + { + leftRetries-- + delay(retriesDelay) + exceptions ?.add(it) + null + } + ) { + execute(request) + } ?.let { return it } } while(leftRetries >= 0) + onAllFailed ?.invoke(exceptions ?.toTypedArray() ?: emptyArray()) return null } diff --git a/TelegramBotAPI/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt b/TelegramBotAPI/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt index 64aaf9caf8..9f3865c94d 100644 --- a/TelegramBotAPI/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt +++ b/TelegramBotAPI/src/jvmMain/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt @@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit * which will be used by telegram to send encrypted messages * @param scope Scope which will be used for */ +@Deprecated("Replaced into project TelegramBotAPI-extensions-utils") suspend fun RequestsExecutor.setWebhook( url: String, port: Int, @@ -41,7 +42,7 @@ suspend fun RequestsExecutor.setWebhook( scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), allowedUpdates: List? = null, maxAllowedConnections: Int? = null, - exceptionsHandler: (suspend (Exception) -> Unit)? = null, + exceptionsHandler: (ExceptionHandler)? = null, block: UpdateReceiver ): Job { val executeDeferred = certificate ?.let { @@ -137,6 +138,7 @@ suspend fun RequestsExecutor.setWebhook( } } +@Deprecated("Replaced into project TelegramBotAPI-extensions-utils") suspend fun RequestsExecutor.setWebhook( url: String, port: Int, @@ -164,6 +166,7 @@ suspend fun RequestsExecutor.setWebhook( block ) +@Deprecated("Replaced into project TelegramBotAPI-extensions-utils") suspend fun RequestsExecutor.setWebhook( url: String, port: Int, @@ -188,6 +191,7 @@ suspend fun RequestsExecutor.setWebhook( block ) +@Deprecated("Replaced into project TelegramBotAPI-extensions-utils") suspend fun RequestsExecutor.setWebhook( url: String, port: Int, diff --git a/gradle.properties b/gradle.properties index 80cf0b5aea..6a07572a4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,9 @@ klock_version=1.11.1 uuid_version=0.1.0 ktor_version=1.3.2 +javax_activation_version=1.1.1 + library_group=com.github.insanusmokrassar -library_version=0.27.2 +library_version=0.27.3 gradle_bintray_plugin_version=1.8.4 diff --git a/settings.gradle b/settings.gradle index 80b7407c7b..b75fc6176b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include ":TelegramBotAPI" include ":TelegramBotAPI-extensions-api" include ":TelegramBotAPI-extensions-utils" +include ":TelegramBotAPI-all" include ":docs"