1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2024-11-25 19:48:43 +00:00

Merge pull request #88 from InsanusMokrassar/0.27.3

0.27.3
This commit is contained in:
InsanusMokrassar 2020-05-16 22:07:56 +06:00 committed by GitHub
commit c31403c1a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1188 additions and 131 deletions

11
.github/labeler.yml vendored
View File

@ -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"

View File

@ -5,14 +5,13 @@
# 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:

View File

@ -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`:

View File

@ -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).

View File

@ -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"
}
```

View File

@ -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")
}
}
}
}
}

View File

@ -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"
}
}
}
}
}
}

View File

@ -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"}

View File

@ -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

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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"

View File

@ -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"}
{"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"}

View File

@ -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(

View File

@ -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)

View File

@ -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
}

View File

@ -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<Unit>)? = null,
allowedUpdates: List<String>? = null,
updatesReceiver: UpdateReceiver<Update>
): 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<MessageUpdate>? = null,
messageMediaGroupCallback: UpdateReceiver<MessageMediaGroupUpdate>? = null,
@ -140,6 +143,7 @@ fun RequestsExecutor.startGettingOfUpdates(
}
@Suppress("unused")
@Deprecated("Replaced and renamed in TelegramBotAPI-extensions-utils")
fun RequestsExecutor.startGettingOfUpdates(
messageCallback: UpdateReceiver<MessageUpdate>? = null,
mediaGroupCallback: UpdateReceiver<MediaGroupUpdate>? = null,

View File

@ -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<Update>,
mediaGroupsDebounceMillis: Long = 1000L
): UpdateReceiver<Update> {
val updatesChannel = Channel<Update>(Channel.UNLIMITED)
val mediaGroupChannel = Channel<Pair<String, BaseMessageUpdate>>(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) }
}

View File

@ -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<String>? = 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,

View File

@ -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)
<small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
[![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.

View File

@ -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")
}
}
}

View File

@ -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"

View File

@ -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"}
{"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"}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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<Update>.lastUpdateIdentifier(): UpdateIdentifier? {
return maxBy { it.updateId } ?.lastUpdateIdentifier()
}
/**
* Will convert incoming list of updates to list with [MediaGroupUpdate]s
*/
fun List<Update>.convertWithMediaGroupUpdates(): List<Update> {
val resultUpdates = mutableListOf<Update>()
val mediaGroups = mutableMapOf<MediaGroupIdentifier, MutableList<BaseSentMessageUpdate>>()
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<BaseSentMessageUpdate>.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}")
}
}

View File

@ -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<Unit>)? = null,
allowedUpdates: List<String>? = null,
updatesReceiver: UpdateReceiver<Update>
): 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<Unit>? = 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<Unit>? = null,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
): Job = startGettingOfUpdatesByLongPolling(
timeoutSeconds,
scope,
exceptionsHandler,
updatesFilter.allowedUpdates,
updatesFilter.asUpdateReceiver
)
fun RequestsExecutor.startGettingOfUpdatesByLongPolling(
messageCallback: UpdateReceiver<MessageUpdate>? = null,
messageMediaGroupCallback: UpdateReceiver<MessageMediaGroupUpdate>? = null,
editedMessageCallback: UpdateReceiver<EditMessageUpdate>? = null,
editedMessageMediaGroupCallback: UpdateReceiver<EditMessageMediaGroupUpdate>? = null,
channelPostCallback: UpdateReceiver<ChannelPostUpdate>? = null,
channelPostMediaGroupCallback: UpdateReceiver<ChannelPostMediaGroupUpdate>? = null,
editedChannelPostCallback: UpdateReceiver<EditChannelPostUpdate>? = null,
editedChannelPostMediaGroupCallback: UpdateReceiver<EditChannelPostMediaGroupUpdate>? = null,
chosenInlineResultCallback: UpdateReceiver<ChosenInlineResultUpdate>? = null,
inlineQueryCallback: UpdateReceiver<InlineQueryUpdate>? = null,
callbackQueryCallback: UpdateReceiver<CallbackQueryUpdate>? = null,
shippingQueryCallback: UpdateReceiver<ShippingQueryUpdate>? = null,
preCheckoutQueryCallback: UpdateReceiver<PreCheckoutQueryUpdate>? = null,
pollCallback: UpdateReceiver<PollUpdate>? = null,
pollAnswerCallback: UpdateReceiver<PollAnswerUpdate>? = null,
timeoutSeconds: Seconds = 30,
exceptionsHandler: ExceptionHandler<Unit>? = 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<MessageUpdate>? = null,
mediaGroupCallback: UpdateReceiver<MediaGroupUpdate>? = null,
editedMessageCallback: UpdateReceiver<EditMessageUpdate>? = null,
channelPostCallback: UpdateReceiver<ChannelPostUpdate>? = null,
editedChannelPostCallback: UpdateReceiver<EditChannelPostUpdate>? = null,
chosenInlineResultCallback: UpdateReceiver<ChosenInlineResultUpdate>? = null,
inlineQueryCallback: UpdateReceiver<InlineQueryUpdate>? = null,
callbackQueryCallback: UpdateReceiver<CallbackQueryUpdate>? = null,
shippingQueryCallback: UpdateReceiver<ShippingQueryUpdate>? = null,
preCheckoutQueryCallback: UpdateReceiver<PreCheckoutQueryUpdate>? = null,
pollCallback: UpdateReceiver<PollUpdate>? = null,
pollAnswerCallback: UpdateReceiver<PollAnswerUpdate>? = null,
timeoutSeconds: Seconds = 30,
exceptionsHandler: ExceptionHandler<Unit>? = 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
)

View File

@ -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<Update>,
debounceTimeMillis: Long = 1000L
): UpdateReceiver<Update> {
val updatesChannel = Channel<Update>(Channel.UNLIMITED)
val mediaGroupChannel = Channel<Pair<String, BaseMessageUpdate>>(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<Update>
) = updateHandlerWithMediaGroupsAdaptation(output, 1000L)

View File

@ -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<Unit>? = null,
block: UpdateReceiver<Update>
) {
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<Unit>,
listenHost: String = "0.0.0.0",
listenRoute: String? = null,
privateKeyConfig: WebhookPrivateKeyConfig? = null,
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
block: UpdateReceiver<Update>
): 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<Boolean>,
exceptionsHandler: ExceptionHandler<Unit> = {},
listenHost: String = "0.0.0.0",
listenRoute: String? = null,
privateKeyConfig: WebhookPrivateKeyConfig? = null,
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
block: UpdateReceiver<Update>
): 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<Unit> = {},
listenHost: String = "0.0.0.0",
listenRoute: String = "/",
privateKeyConfig: WebhookPrivateKeyConfig? = null,
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
block: UpdateReceiver<Update>
): Job = internalSetWebhookInfoAndStartListenWebhooks(
listenPort,
engineFactory,
setWebhookRequest as Request<Boolean>,
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<SetWebhook, Map<String, MultipartFile>, Boolean>,
exceptionsHandler: ExceptionHandler<Unit> = {},
listenHost: String = "0.0.0.0",
listenRoute: String? = null,
privateKeyConfig: WebhookPrivateKeyConfig? = null,
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
block: UpdateReceiver<Update>
): Job = internalSetWebhookInfoAndStartListenWebhooks(
listenPort,
engineFactory,
setWebhookRequest as Request<Boolean>,
exceptionsHandler,
listenHost,
listenRoute,
privateKeyConfig,
scope,
block
)

View File

@ -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

View File

@ -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"]
}
}
}
}

View File

@ -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 <T : Any> execute(request: Request<T>): T
}

View File

@ -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

View File

@ -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<String>? = null
): MultipartRequestImpl<SetWebhook, Map<String, MultipartFile>, Boolean> = MultipartRequestImpl(
SetWebhook(
correctWebhookUrl(url),
null,
maxAllowedConnections,
allowedUpdates
),
mapOf(certificateField to certificate)
)
fun SetWebhook(
url: String,
certificate: FileId,
maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null
): SetWebhook = SetWebhook(
correctWebhookUrl(url),
certificate.fileId,
maxAllowedConnections,
allowedUpdates
)
@Suppress("USELESS_CAST")
fun SetWebhook(
url: String,
certificate: InputFile,
maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null
) : Request<Boolean> {
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<Boolean> = 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<String>? = null
) : Request<Boolean> = SetWebhook(
url,
) = SetWebhook(
correctWebhookUrl(url),
null,
maxAllowedConnections,
allowedUpdates

View File

@ -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)

View File

@ -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<DiceAnimationType> {
@ -26,7 +32,8 @@ internal object DiceAnimationTypeSerializer : KSerializer<DiceAnimationType> {
return when (val type = decoder.decodeString()) {
CubeDiceAnimationType.emoji -> CubeDiceAnimationType
DartsDiceAnimationType.emoji -> DartsDiceAnimationType
else -> UnknownDiceAnimationType(type)
BasketballDiceAnimationType.emoji -> BasketballDiceAnimationType
else -> CustomDiceAnimationType(type)
}
}

View File

@ -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 {

View File

@ -26,7 +26,14 @@ internal object UpdateSerializerWithoutSerialization : KSerializer<Update> {
override fun serialize(encoder: Encoder, value: Update) = throw UnsupportedOperationException()
}
internal object UpdateDeserializationStrategy : DeserializationStrategy<Update> {
/**
* 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<Update> {
override val descriptor: SerialDescriptor = JsonElementSerializer.descriptor
override fun patch(decoder: Decoder, old: Update): Update = throw UpdateNotSupportedException("Update")

View File

@ -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 <T> BroadcastChannel<T>.createUpdateReceiver(): UpdateReceiver<T> = ::send
@FlowPreview
@Suppress("EXPERIMENTAL_API_USAGE", "unused")
class FlowsUpdatesFilter(
broadcastChannelsSize: Int = Channel.CONFLATED
broadcastChannelsSize: Int = 64
): UpdatesFilter {
private val messageChannel: BroadcastChannel<MessageUpdate> = BroadcastChannel(broadcastChannelsSize)
private val messageMediaGroupChannel: BroadcastChannel<MessageMediaGroupUpdate> = BroadcastChannel(broadcastChannelsSize)

View File

@ -3,6 +3,8 @@ package com.github.insanusmokrassar.TelegramBotAPI.utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.supervisorScope
typealias ExceptionHandler<T> = 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 <T> handleSafely(
noinline onException: suspend (Exception) -> T = { throw it },
noinline onException: ExceptionHandler<T> = { throw it },
noinline block: suspend CoroutineScope.() -> T
): T {
return try {

View File

@ -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 <T: Any> RequestsExecutor.executeAsync(
suspend fun <T: Any> RequestsExecutor.executeUnsafe(
request: Request<T>,
retries: Int = 0,
retriesDelay: Long = 1000L
retriesDelay: Long = 1000L,
onAllFailed: (suspend (exceptions: Array<Exception>) -> Unit)? = null
): T? {
var leftRetries = retries
val exceptions = onAllFailed ?.let { mutableListOf<Exception>() }
do {
try {
return execute(request)
} catch (e: RequestException) {
handleSafely(
{
leftRetries--
delay(retriesDelay)
exceptions ?.add(it)
null
}
) {
execute(request)
} ?.let { return it }
} while(leftRetries >= 0)
onAllFailed ?.invoke(exceptions ?.toTypedArray() ?: emptyArray())
return null
}

View File

@ -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<String>? = null,
maxAllowedConnections: Int? = null,
exceptionsHandler: (suspend (Exception) -> Unit)? = null,
exceptionsHandler: (ExceptionHandler<Unit>)? = null,
block: UpdateReceiver<Update>
): 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,

View File

@ -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

View File

@ -1,4 +1,5 @@
include ":TelegramBotAPI"
include ":TelegramBotAPI-extensions-api"
include ":TelegramBotAPI-extensions-utils"
include ":TelegramBotAPI-all"
include ":docs"