diff --git a/CHANGELOG.md b/CHANGELOG.md index 2181001752..e027e10444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # TelegramBotAPI changelog +## 0.12.0 Webhooks + +* Added `DataRequest` interface which replace `Data` interface +* `MultipartRequestImpl` now use `DataRequest` +* All requests which implements `Data` now implement `DataRequest` +* Added class `SetWebhook` and its factory +* Added class `UpdatesFilter` which can help to filter updates by categories +* Added function `accumulateByKey` which work as debounce for keys and send list of received values +* Added webhooks functions and workaround for `Reverse Proxy` mode +* Added new type of updates `MediaGroupUpdate`, which can be received only from filters +* `UpdatesFilter` now use new type of updates for mediagroups +* Add `GetWebhookInfo` request and `WebhookInfo` type +* Replace updates types into separated place in types +* Now default `RequestException` will contain plain answer from telegram +* Added `UnauthorizedException` +* `RequestException` now is sealed +* Rename `ReplyMessageNotFound` to `ReplyMessageNotFoundException` +* Added `List#mediaGroupId` extension +* Added utility `T#asReference(): WeakReference(T)` extension +* Added `UpdatesPoller` class which can be instantiated for manage updates polling +* Separated execute extensions (now they are in file `Executes`) and poller creating extensions +* `BaseMessageUpdate#toMediaGroupUpdate()` will also check condition when update-receiver already is `MediaGroupUpdate` + ## 0.11.0 * Kotlin `1.3.11` -> `1.3.21` diff --git a/README.md b/README.md index 100c8cece8..ac44d17ff3 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ It is one more project which wish to be useful and full Telegram Bots API bridge for Kotlin. Most part of some specific solves or unuseful moments are describing by official [Telegram Bot API](https://core.telegram.org/bots/api). +## Compatibility + +This version compatible with [July 2018 update of TelegramBotAPI](https://core.telegram.org/bots/api#july-26-2018). That means that +most part of API has been implemented (according to last [August 2018 update of TelegramBotAPI](https://core.telegram.org/bots/api#august-27-2018)) +except the Passport API which will be included as soon as possible. + ## How to work with library? By default in any documentation will be meaning that you have variable in scope with names @@ -27,3 +33,34 @@ executor.execute(GetMe()) As a result you will receive `User` object. This object used as is now (as in API documentation), but it is possible that this class will be renamed to `RawUser` and you will be able to get real realisation of this object like `Bot` (in cases when `isBot` == `true`) or `User` (otherwise) + +## 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 you must not use it and 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 must provide `File` with public part of certificate, `URL` where bot placed and inner `PORT` which +will be used to start receiving of updates. diff --git a/build.gradle b/build.gradle index 78c107f9c6..0a44792233 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -project.version = "0.11.0" +project.version = "0.12.0" project.group = "com.github.insanusmokrassar" buildscript { @@ -34,9 +34,13 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlin_serialisation_runtime_version" implementation "joda-time:joda-time:$joda_time_version" + implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-okhttp:$ktor_version" + implementation "io.ktor:ktor-server-core:$ktor_version" + implementation "io.ktor:ktor-server-netty:$ktor_version" + // Use JUnit test framework testImplementation 'junit:junit:4.12' } diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/Ktor/KtorRequestsExecutor.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/Ktor/KtorRequestsExecutor.kt index 15ae5c6537..285bf033e9 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/Ktor/KtorRequestsExecutor.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/Ktor/KtorRequestsExecutor.kt @@ -77,7 +77,8 @@ class KtorRequestsExecutor( } ?: call.let { throw newRequestException( responseObject, - "Can't get result object" + content, + "Can't get result object from $content" ) } } diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/ReplyMessageNotFound.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/ReplyMessageNotFound.kt deleted file mode 100644 index 0208179047..0000000000 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/ReplyMessageNotFound.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions - -import com.github.insanusmokrassar.TelegramBotAPI.types.Response - -open class ReplyMessageNotFound(response: Response<*>, message: String?, cause: Throwable?) : - RequestException(response, message, cause) \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/RequestException.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/RequestException.kt index 1783631604..777ccec5d7 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/RequestException.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/exceptions/RequestException.kt @@ -5,18 +5,36 @@ import java.io.IOException fun newRequestException( response: Response<*>, + plainAnswer: String, message: String? = null, cause: Throwable? = null ) = when (response.description) { - "Bad Request: reply message not found" -> ReplyMessageNotFound(response, message, cause) - else -> RequestException(response, message, cause) + "Bad Request: reply message not found" -> ReplyMessageNotFoundException(response, plainAnswer, message, cause) + "Unauthorized" -> UnauthorizedException(response, plainAnswer, message, cause) + else -> CommonRequestException(response, plainAnswer, message, cause) } -open class RequestException internal constructor( +sealed class RequestException constructor( val response: Response<*>, + val plainAnswer: String, message: String? = null, cause: Throwable? = null ) : IOException( message, cause -) \ No newline at end of file +) + +class CommonRequestException(response: Response<*>, plainAnswer: String, message: String?, cause: Throwable?) : + RequestException(response, plainAnswer, message, cause) + +class UnauthorizedException(response: Response<*>, plainAnswer: String, message: String?, cause: Throwable?) : + RequestException(response, plainAnswer, message, cause) + +class ReplyMessageNotFoundException(response: Response<*>, plainAnswer: String, message: String?, cause: Throwable?) : + RequestException(response, plainAnswer, message, cause) + +@Deprecated( + "Replaced by ReplyMessageNotFoundException", + ReplaceWith("ReplyMessageNotFoundException", "com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.ReplyMessageNotFoundException") +) +typealias ReplyMessageNotFound = ReplyMessageNotFoundException \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt index a4800f45c1..a491568b30 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetUpdates.kt @@ -2,27 +2,30 @@ package com.github.insanusmokrassar.TelegramBotAPI.requests import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest import com.github.insanusmokrassar.TelegramBotAPI.types.UpdateIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.types.ALL_UPDATES_LIST import com.github.insanusmokrassar.TelegramBotAPI.types.update.RawUpdate import kotlinx.serialization.* import kotlinx.serialization.internal.ArrayListSerializer -const val UPDATE_MESSAGE = "message" -const val UPDATE_EDITED_MESSAGE = "edited_message" -const val UPDATE_CHANNEL_POST = "channel_post" -const val UPDATE_EDITED_CHANNEL_POST = "edited_channel_post" -const val UPDATE_CHOSEN_INLINE_RESULT = "chosen_inline_result" -const val UPDATE_INLINE_QUERY = "inline_query" -const val UPDATE_CALLBACK_QUERY = "callback_query" -const val UPDATE_SHIPPING_QUERY = "shipping_query" -const val UPDATE_PRE_CHECKOUT_QUERY = "pre_checkout_query" -/* +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_MESSAGE", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_MESSAGE")) +const val UPDATE_MESSAGE = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_MESSAGE +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_EDITED_MESSAGE", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_MESSAGE")) +const val UPDATE_EDITED_MESSAGE = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_MESSAGE +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CHANNEL_POST", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHANNEL_POST")) +const val UPDATE_CHANNEL_POST = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHANNEL_POST +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_EDITED_CHANNEL_POST", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_CHANNEL_POST")) +const val UPDATE_EDITED_CHANNEL_POST = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_CHANNEL_POST +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CHOSEN_INLINE_RESULT", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHOSEN_INLINE_RESULT")) +const val UPDATE_CHOSEN_INLINE_RESULT = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHOSEN_INLINE_RESULT +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_INLINE_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_INLINE_QUERY")) +const val UPDATE_INLINE_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_INLINE_QUERY +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CALLBACK_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CALLBACK_QUERY")) +const val UPDATE_CALLBACK_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CALLBACK_QUERY +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_SHIPPING_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_SHIPPING_QUERY")) +const val UPDATE_SHIPPING_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_SHIPPING_QUERY +@Deprecated("Replaced to other package", ReplaceWith("UPDATE_PRE_CHECKOUT_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_PRE_CHECKOUT_QUERY")) +const val UPDATE_PRE_CHECKOUT_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_PRE_CHECKOUT_QUERY - @Optional private val inline_query: RawInlineQuery? = null, - @Optional private val chosen_inline_result: Unit? = null, - @Optional private val callback_query: RawCallbackQuery? = null, - @Optional private val shipping_query: Unit? = null, - @Optional private val pre_checkout_query: Unit? = null - */ @Serializable data class GetUpdates( @Optional @@ -32,17 +35,7 @@ data class GetUpdates( @Optional val timeout: Int? = null, @Optional - val allowed_updates: List? = listOf( - UPDATE_MESSAGE, - UPDATE_EDITED_MESSAGE, - UPDATE_CHANNEL_POST, - UPDATE_EDITED_CHANNEL_POST, - UPDATE_CHOSEN_INLINE_RESULT, - UPDATE_INLINE_QUERY, - UPDATE_CALLBACK_QUERY, - UPDATE_SHIPPING_QUERY, - UPDATE_PRE_CHECKOUT_QUERY - ) + val allowed_updates: List? = ALL_UPDATES_LIST ): SimpleRequest> { override fun method(): String = "getUpdates" diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/abstracts/Request.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/abstracts/Request.kt index 7ab7129b23..3776c1c69d 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/abstracts/Request.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/abstracts/Request.kt @@ -5,12 +5,13 @@ import com.github.insanusmokrassar.TelegramBotAPI.utils.toJsonWithoutNulls import kotlinx.serialization.* import kotlinx.serialization.json.JsonObject +@Serializable(RequestSerializer::class) interface Request { fun method(): String fun resultSerializer(): KSerializer - @ImplicitReflectionSerializer - fun json(): JsonObject = toJsonWithoutNulls() + fun json(): JsonObject = toJsonWithoutNulls(RequestSerializer) } +object RequestSerializer : KSerializer> by ContextSerializer(Request::class) fun StringFormat.extractResult( from: String, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAnimation.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAnimation.kt index 175a63a7f6..7974b242a9 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAnimation.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAnimation.kt @@ -87,7 +87,7 @@ data class SendAnimationData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAudio.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAudio.kt index 9599b3a532..2e086813dd 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAudio.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendAudio.kt @@ -88,7 +88,7 @@ data class SendAudioData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendDocument.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendDocument.kt index 55e2c67357..dca1fc11c9 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendDocument.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendDocument.kt @@ -72,7 +72,7 @@ data class SendDocumentData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendMediaGroup.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendMediaGroup.kt index f13de99995..a945196b74 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendMediaGroup.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendMediaGroup.kt @@ -66,7 +66,7 @@ data class SendMediaGroupData internal constructor( @SerialName(replyToMessageIdField) @Optional override val replyToMessageId: MessageIdentifier? = null -) : Data>, +) : DataRequest>, SendMessageRequest> { @SerialName(mediaField) diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendPhoto.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendPhoto.kt index 2d5abe34c7..92336917c0 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendPhoto.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendPhoto.kt @@ -58,7 +58,7 @@ data class SendPhotoData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideo.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideo.kt index 884c22c12a..3097bf07c9 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideo.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideo.kt @@ -92,7 +92,7 @@ data class SendVideoData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideoNote.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideoNote.kt index 61d876b225..c3199c0c2f 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideoNote.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVideoNote.kt @@ -82,7 +82,7 @@ data class SendVideoNoteData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVoice.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVoice.kt index 175135b727..16ee8a3a20 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVoice.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/SendVoice.kt @@ -77,7 +77,7 @@ data class SendVoiceData internal constructor( @SerialName(replyMarkupField) @Optional override val replyMarkup: KeyboardMarkup? = null -) : Data, +) : DataRequest, SendMessageRequest, ReplyingMarkupSendMessageRequest, TextableSendMessageRequest, diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/Data.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/Data.kt index e2bd40e3d6..d8ff64cd37 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/Data.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/Data.kt @@ -1,5 +1,10 @@ package com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base -import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest - -interface Data : SimpleRequest \ No newline at end of file +@Deprecated( + "Renamed to DataRequest", + ReplaceWith( + "DataRequest", + "com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base.DataRequest" + ) +) +typealias Data = DataRequest diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/DataRequest.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/DataRequest.kt new file mode 100644 index 0000000000..d9b3f3213b --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/DataRequest.kt @@ -0,0 +1,11 @@ +package com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base + +import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest +import kotlinx.serialization.ContextSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable + +@Serializable(DataRequestSerializer::class) +interface DataRequest : SimpleRequest + +object DataRequestSerializer : KSerializer> by ContextSerializer(DataRequest::class) \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/MultipartRequestImpl.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/MultipartRequestImpl.kt index bcb68d8306..bc5488a504 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/MultipartRequestImpl.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/send/media/base/MultipartRequestImpl.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.json.JsonObject /** * Will be used as SimpleRequest if */ -class MultipartRequestImpl, F: Files, R: Any>( +class MultipartRequestImpl, F: Files, R: Any>( val data: D, val files: F ) : MultipartRequest { diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/GetWebhookInfo.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/GetWebhookInfo.kt new file mode 100644 index 0000000000..8b4decd01b --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/GetWebhookInfo.kt @@ -0,0 +1,12 @@ +package com.github.insanusmokrassar.TelegramBotAPI.requests.webhook + +import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest +import com.github.insanusmokrassar.TelegramBotAPI.types.WebhookInfo +import kotlinx.serialization.* + +@Serializable +class GetWebhookInfo : SimpleRequest { + override fun method(): String = "getWebhookInfo" + + override fun resultSerializer(): KSerializer = WebhookInfo.serializer() +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt new file mode 100644 index 0000000000..7649006554 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/webhook/SetWebhook.kt @@ -0,0 +1,65 @@ +package com.github.insanusmokrassar.TelegramBotAPI.requests.webhook + +import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.* +import com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base.* +import com.github.insanusmokrassar.TelegramBotAPI.types.* +import kotlinx.serialization.* +import kotlinx.serialization.internal.BooleanSerializer + +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) + ) + } +} + +fun SetWebhook( + url: String, + maxAllowedConnections: Int? = null, + allowedUpdates: List? = null +) : Request = SetWebhook( + url, + null, + maxAllowedConnections, + allowedUpdates +) + +@Serializable +data class SetWebhook internal constructor( + @SerialName(urlField) + val url: String, + @SerialName(certificateField) + @Optional + val certificateFile: String? = null, + @SerialName(maxAllowedConnectionsField) + @Optional + val maxAllowedConnections: Int? = null, + @SerialName(allowedUpdatesField) + @Optional + val allowedUpdates: List? = null +) : DataRequest { + override fun method(): String = "setWebhook" + override fun resultSerializer(): KSerializer = BooleanSerializer + + init { + maxAllowedConnections ?.let { + if (it !in allowedConnectionsLength) { + throw IllegalArgumentException("Allowed connection for webhook must be in $allowedConnectionsLength range (but passed $it)") + } + } + } +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt index 49fc39a4df..8aa06288d6 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/Common.kt @@ -23,6 +23,7 @@ val userProfilePhotosRequestLimit = 0 .. 100 val chatTitleLength = 1 until 255 val chatDescriptionLength = 0 until 256 val inlineResultQueryIdLingth = 1 until 64 +val allowedConnectionsLength = 1 .. 100 val invoiceTitleLimit = 1 until 32 val invoiceDescriptionLimit = 1 until 256 @@ -68,6 +69,12 @@ const val isPersonalField = "is_personal" const val nextOffsetField = "next_offset" const val switchPmTextField = "switch_pm_text" const val switchPmParameterField = "switch_pm_parameter" +const val maxAllowedConnectionsField = "max_connections" +const val allowedUpdatesField = "allowed_updates" +const val hasCustomCertificateField = "has_custom_certificate" +const val pendingUpdateCountField = "pending_update_count" +const val lastErrorDateField = "last_error_date" +const val lastErrorMessageField = "last_error_message" const val photoUrlField = "photo_url" @@ -172,6 +179,7 @@ const val pricesField = "prices" const val payloadField = "payload" const val vcardField = "vcard" const val resultsField = "results" +const val certificateField = "certificate" const val pointField = "point" const val xShiftField = "x_shift" diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/UpdateTypes.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/UpdateTypes.kt new file mode 100644 index 0000000000..e5d477db3f --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/UpdateTypes.kt @@ -0,0 +1,23 @@ +package com.github.insanusmokrassar.TelegramBotAPI.types + +const val UPDATE_MESSAGE = "message" +const val UPDATE_EDITED_MESSAGE = "edited_message" +const val UPDATE_CHANNEL_POST = "channel_post" +const val UPDATE_EDITED_CHANNEL_POST = "edited_channel_post" +const val UPDATE_CHOSEN_INLINE_RESULT = "chosen_inline_result" +const val UPDATE_INLINE_QUERY = "inline_query" +const val UPDATE_CALLBACK_QUERY = "callback_query" +const val UPDATE_SHIPPING_QUERY = "shipping_query" +const val UPDATE_PRE_CHECKOUT_QUERY = "pre_checkout_query" + +val ALL_UPDATES_LIST = listOf( + UPDATE_MESSAGE, + UPDATE_EDITED_MESSAGE, + UPDATE_CHANNEL_POST, + UPDATE_EDITED_CHANNEL_POST, + UPDATE_CHOSEN_INLINE_RESULT, + UPDATE_INLINE_QUERY, + UPDATE_CALLBACK_QUERY, + UPDATE_SHIPPING_QUERY, + UPDATE_PRE_CHECKOUT_QUERY +) diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt new file mode 100644 index 0000000000..84fa832908 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/WebhookInfo.kt @@ -0,0 +1,32 @@ +package com.github.insanusmokrassar.TelegramBotAPI.types + +import kotlinx.serialization.* + +@Serializable +data class WebhookInfo( + @SerialName(urlField) + val url: String, + @SerialName(pendingUpdateCountField) + val awaitDeliery: Int, + @SerialName(maxAllowedConnectionsField) + @Optional + val maxConnections: Int = 40, // default count according to documentation + @SerialName(hasCustomCertificateField) + @Optional + val customCertificate: Boolean = false, + @SerialName(allowedUpdatesField) + @Optional + val allowedUpdates: List = ALL_UPDATES_LIST, + @SerialName(lastErrorDateField) + @Optional + val lastErrorDate: TelegramDate? = null, + @SerialName(lastErrorMessageField) + @Optional + val lastErrorMessage: String? = null +) { + @Transient + val isNotUseWebhook: Boolean = url.isEmpty() + + @Transient + val hasError: Boolean = lastErrorMessage != null +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdate.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdate.kt new file mode 100644 index 0000000000..cd77aec304 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/update/MediaGroupUpdate.kt @@ -0,0 +1,10 @@ +package com.github.insanusmokrassar.TelegramBotAPI.types.update + +import com.github.insanusmokrassar.TelegramBotAPI.types.UpdateIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate + +data class MediaGroupUpdate( + override val updateId: UpdateIdentifier, + override val data: MediaGroupMessage +) : BaseMessageUpdate diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/BaseMessageUpdateToMediaGroupUpdate.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/BaseMessageUpdateToMediaGroupUpdate.kt new file mode 100644 index 0000000000..595e9cc7ad --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/BaseMessageUpdateToMediaGroupUpdate.kt @@ -0,0 +1,9 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils + +import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage +import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdate +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate + +fun BaseMessageUpdate.toMediaGroupUpdate(): MediaGroupUpdate? = (this as? MediaGroupUpdate) ?: ((data as? MediaGroupMessage) ?.let { + MediaGroupUpdate(updateId, it) +}) diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/JSON.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/JSON.kt index 45bc692189..b2bcc701e5 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/JSON.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/JSON.kt @@ -3,6 +3,7 @@ package com.github.insanusmokrassar.TelegramBotAPI.utils import kotlinx.serialization.* import kotlinx.serialization.json.* +@Deprecated("This method can throw exceptions") @ImplicitReflectionSerializer inline fun T.toJsonWithoutNulls(): JsonObject = Json.nonstrict.toJson( this diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/MediaGroupList.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/MediaGroupList.kt index 08866b8335..e43da3f2ef 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/MediaGroupList.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/MediaGroupList.kt @@ -1,8 +1,10 @@ package com.github.insanusmokrassar.TelegramBotAPI.utils +import com.github.insanusmokrassar.TelegramBotAPI.types.MediaGroupIdentifier import com.github.insanusmokrassar.TelegramBotAPI.types.chat.Chat import com.github.insanusmokrassar.TelegramBotAPI.types.message.ForwardedMessage import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdate import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate val List.forwarded: ForwardedMessage? @@ -17,3 +19,6 @@ val List.replyTo: Message? val List.chat: Chat? get() = first().data.chat + +val List.mediaGroupId: MediaGroupIdentifier? + get() = (first().data as? MediaGroupMessage) ?.mediaGroupId diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/AsReference.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/AsReference.kt new file mode 100644 index 0000000000..b8cb0b5eae --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/AsReference.kt @@ -0,0 +1,5 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import java.lang.ref.WeakReference + +fun T.asReference() = WeakReference(this) \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt new file mode 100644 index 0000000000..eb8b2f2679 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Executes.kt @@ -0,0 +1,48 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +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 kotlinx.coroutines.* + + +fun RequestsExecutor.executeAsync( + request: Request, + onFail: (suspend (Response<*>) -> Unit)? = null, + scope: CoroutineScope = GlobalScope, + onSuccess: (suspend (T) -> Unit)? = null +): Job { + return scope.launch { + try { + val result = execute(request) + onSuccess ?.invoke(result) + } catch (e: RequestException) { + onFail ?.invoke(e.response) + } + } +} + +fun RequestsExecutor.executeAsync( + request: Request, + scope: CoroutineScope = GlobalScope +): Deferred { + return scope.async { execute(request) } +} + +suspend fun RequestsExecutor.executeUnsafe( + request: Request, + retries: Int = 0, + retriesDelay: Long = 1000L +): T? { + var leftRetries = retries + do { + try { + return execute(request) + } catch (e: RequestException) { + leftRetries-- + delay(retriesDelay) + } + } while(leftRetries >= 0) + return null +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/ReceiveChannel.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/ReceiveChannel.kt new file mode 100644 index 0000000000..0cbc0421a9 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/ReceiveChannel.kt @@ -0,0 +1,101 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel + +private sealed class DebounceAction { + abstract val value: T +} + +private data class AddValue(override val value: T) : DebounceAction() +private data class RemoveJob(override val value: T, val job: Job) : DebounceAction() + +fun ReceiveChannel.debounceByValue( + delayMillis: Long, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + resultBroadcastChannelCapacity: Int = 32 +): ReceiveChannel { + val outChannel = Channel(resultBroadcastChannelCapacity) + val values = HashMap() + + val channel = Channel>(Channel.UNLIMITED) + scope.launch { + for (action in channel) { + when (action) { + is AddValue -> { + val msg = action.value + values[msg] ?.cancel() + lateinit var job: Job + job = launch { + delay(delayMillis) + + outChannel.send(msg) + channel.send(RemoveJob(msg, job)) + } + values[msg] = job + } + is RemoveJob -> if (values[action.value] == action.job) { + values.remove(action.value) + } + } + + } + } + + scope.launch { + for (msg in this@debounceByValue) { + channel.send(AddValue(msg)) + } + } + + return outChannel +} + +typealias AccumulatedValues = Pair> + +fun ReceiveChannel>.accumulateByKey( + delayMillis: Long, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + resultBroadcastChannelCapacity: Int = 32 +): ReceiveChannel> { + val outChannel = Channel>(resultBroadcastChannelCapacity) + val values = HashMap>() + val jobs = HashMap() + + val channel = Channel>>(Channel.UNLIMITED) + scope.launch { + for (action in channel) { + val (key, value) = action.value + when (action) { + is AddValue -> { + jobs[key] ?.cancel() + (values[key] ?: mutableListOf().also { values[key] = it }).add(value) + lateinit var job: Job + job = launch { + delay(delayMillis) + + values[key] ?.let { + outChannel.send(key to it) + channel.send(RemoveJob(key to value, job)) + } + } + jobs[key] = job + } + is RemoveJob -> if (values[key] == action.job) { + values.remove(key) + jobs.remove(key) + } + } + + } + } + + scope.launch { + for (msg in this@accumulateByKey) { + channel.send(AddValue(msg)) + } + } + + return outChannel +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/RequestsExecutor.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/RequestsExecutor.kt deleted file mode 100644 index d44cdb5871..0000000000 --- a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/RequestsExecutor.kt +++ /dev/null @@ -1,248 +0,0 @@ -package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions - -import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor -import com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.RequestException -import com.github.insanusmokrassar.TelegramBotAPI.requests.* -import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request -import com.github.insanusmokrassar.TelegramBotAPI.types.Response -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.abstracts.BaseMessageUpdate -import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update -import kotlinx.coroutines.* - -typealias UpdateReceiver = suspend (T) -> Unit - -fun RequestsExecutor.startGettingOfUpdates( - requestsDelayMillis: Long = 1000, - scope: CoroutineScope = GlobalScope, - allowedUpdates: List? = null, - block: UpdateReceiver -): Job { - return scope.launch { - var lastHandledUpdate: UpdateIdentifier = 0L - while (isActive) { - delay(requestsDelayMillis) - try { - val updates = execute( - GetUpdates( - lastHandledUpdate + 1, - allowed_updates = allowedUpdates - ) - ) - - val adaptedUpdates = mutableListOf() - var mediaGroup: MutableList? = null - - fun pushMediaGroup() { - mediaGroup ?.also { - adaptedUpdates.add(it) - mediaGroup = null - } - } - - updates.map { - it.asUpdate - }.forEach { update -> - val data = update.data - if (data is MediaGroupMessage) { - mediaGroup ?.let { - val message = it.first().data as MediaGroupMessage - if (message.mediaGroupId == data.mediaGroupId) { - it.add(update) - } else { - null - } - } ?: data.also { - pushMediaGroup() - mediaGroup = mutableListOf() - mediaGroup ?.add(update) - } - } else { - pushMediaGroup() - adaptedUpdates.add(update) - } - } - - mediaGroup ?.also { - adaptedUpdates.add(it) - mediaGroup = null - } - - for (update in adaptedUpdates) { - - try { - block(update) - lastHandledUpdate = when (update) { - is Update -> update.updateId - is List<*> -> (update.last() as? Update) ?.updateId ?: throw IllegalStateException( - "Found non-updates oriented list" - ) - else -> throw IllegalStateException( - "Unknown type of data" - ) - } - } catch (e: Exception) { - // TODO:: add exception handling - e.printStackTrace() - break - } - } - } catch (e: Exception) { - // TODO:: add exception handling - e.printStackTrace() - } - } - } -} - -fun RequestsExecutor.startGettingOfUpdates( - 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, - requestsDelayMillis: Long = 1000, - scope: CoroutineScope = GlobalScope -): Job { - return startGettingOfUpdates( - requestsDelayMillis, - scope, - listOfNotNull( - (messageCallback ?: messageMediaGroupCallback) ?.let { UPDATE_MESSAGE }, - (editedMessageCallback ?: editedMessageMediaGroupCallback) ?.let { UPDATE_EDITED_MESSAGE }, - (channelPostCallback ?: channelPostMediaGroupCallback) ?.let { UPDATE_CHANNEL_POST }, - (editedChannelPostCallback ?: editedChannelPostMediaGroupCallback) ?.let { UPDATE_EDITED_CHANNEL_POST }, - chosenInlineResultCallback ?.let { UPDATE_CHOSEN_INLINE_RESULT }, - inlineQueryCallback ?.let { UPDATE_INLINE_QUERY }, - callbackQueryCallback ?.let { UPDATE_CALLBACK_QUERY }, - shippingQueryCallback ?.let { UPDATE_SHIPPING_QUERY }, - preCheckoutQueryCallback ?.let { UPDATE_PRE_CHECKOUT_QUERY } - ) - ) { update -> - when (update) { - is MessageUpdate -> messageCallback ?.invoke(update) - is List<*> -> when (update.firstOrNull()) { - is MessageUpdate -> update.mapNotNull { it as? MessageUpdate }.let { mappedList -> - messageMediaGroupCallback ?.also { receiver -> - receiver(mappedList) - } ?: messageCallback ?.also { receiver -> - mappedList.forEach { receiver(it) } - } - } - is EditMessageUpdate -> update.mapNotNull { it as? EditMessageUpdate }.let { mappedList -> - editedMessageMediaGroupCallback ?.also { receiver -> - receiver(mappedList) - } ?: editedMessageCallback ?.also { receiver -> - mappedList.forEach { receiver(it) } - } - } - is ChannelPostUpdate -> update.mapNotNull { it as? ChannelPostUpdate }.let { mappedList -> - channelPostMediaGroupCallback ?.also { receiver -> - receiver(mappedList) - } ?: channelPostCallback ?.also { receiver -> - mappedList.forEach { receiver(it) } - } - } - is EditChannelPostUpdate -> update.mapNotNull { it as? EditChannelPostUpdate }.let { mappedList -> - editedChannelPostMediaGroupCallback ?.also { receiver -> - receiver(mappedList) - } ?: editedChannelPostCallback ?.also { receiver -> - mappedList.forEach { receiver(it) } - } - } - } - is EditMessageUpdate -> editedMessageCallback ?.invoke(update) - is ChannelPostUpdate -> channelPostCallback ?.invoke(update) - is EditChannelPostUpdate -> editedChannelPostCallback ?.invoke(update) - is ChosenInlineResultUpdate -> chosenInlineResultCallback ?.invoke(update) - is InlineQueryUpdate -> inlineQueryCallback ?.invoke(update) - is CallbackQueryUpdate -> callbackQueryCallback ?.invoke(update) - is ShippingQueryUpdate -> shippingQueryCallback ?.invoke(update) - is PreCheckoutQueryUpdate -> preCheckoutQueryCallback ?.invoke(update) - } - } -} - -fun RequestsExecutor.startGettingOfUpdates( - 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, - requestsDelayMillis: Long = 1000, - scope: CoroutineScope = GlobalScope -): Job = startGettingOfUpdates( - 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, - requestsDelayMillis = requestsDelayMillis, - scope = scope -) - -fun RequestsExecutor.executeAsync( - request: Request, - onFail: (suspend (Response<*>) -> Unit)? = null, - scope: CoroutineScope = GlobalScope, - onSuccess: (suspend (T) -> Unit)? = null -): Job { - return scope.launch { - try { - val result = execute(request) - onSuccess ?.invoke(result) - } catch (e: RequestException) { - onFail ?.invoke(e.response) - } - } -} - -fun RequestsExecutor.executeAsync( - request: Request, - scope: CoroutineScope = GlobalScope -): Deferred { - return scope.async { execute(request) } -} - -suspend fun RequestsExecutor.executeUnsafe( - request: Request, - retries: Int = 0, - retriesDelay: Long = 1000L -): T? { - var leftRetries = retries - while(true) { - try { - return execute(request) - } catch (e: RequestException) { - if (leftRetries > 0) { - leftRetries-- - delay(retriesDelay) - } else { - return null - } - } - } -} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesFilter.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesFilter.kt new file mode 100644 index 0000000000..d008a3b971 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesFilter.kt @@ -0,0 +1,106 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import com.github.insanusmokrassar.TelegramBotAPI.types.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate +import com.github.insanusmokrassar.TelegramBotAPI.utils.toMediaGroupUpdate + +data class UpdatesFilter( + private val messageCallback: UpdateReceiver? = null, + private val messageMediaGroupCallback: UpdateReceiver>? = null, + private val editedMessageCallback: UpdateReceiver? = null, + private val editedMessageMediaGroupCallback: UpdateReceiver>? = null, + private val channelPostCallback: UpdateReceiver? = null, + private val channelPostMediaGroupCallback: UpdateReceiver>? = null, + private val editedChannelPostCallback: UpdateReceiver? = null, + private val editedChannelPostMediaGroupCallback: UpdateReceiver>? = null, + private val chosenInlineResultCallback: UpdateReceiver? = null, + private val inlineQueryCallback: UpdateReceiver? = null, + private val callbackQueryCallback: UpdateReceiver? = null, + private val shippingQueryCallback: UpdateReceiver? = null, + private val preCheckoutQueryCallback: UpdateReceiver? = null +) { + val asUpdateReceiver: UpdateReceiver = this::invoke + val allowedUpdates = listOfNotNull( + (messageCallback ?: messageMediaGroupCallback) ?.let { UPDATE_MESSAGE }, + (editedMessageCallback ?: editedMessageMediaGroupCallback) ?.let { UPDATE_EDITED_MESSAGE }, + (channelPostCallback ?: channelPostMediaGroupCallback) ?.let { UPDATE_CHANNEL_POST }, + (editedChannelPostCallback ?: editedChannelPostMediaGroupCallback) ?.let { UPDATE_EDITED_CHANNEL_POST }, + chosenInlineResultCallback ?.let { UPDATE_CHOSEN_INLINE_RESULT }, + inlineQueryCallback ?.let { UPDATE_INLINE_QUERY }, + callbackQueryCallback ?.let { UPDATE_CALLBACK_QUERY }, + shippingQueryCallback ?.let { UPDATE_SHIPPING_QUERY }, + preCheckoutQueryCallback ?.let { UPDATE_PRE_CHECKOUT_QUERY } + ) + + suspend fun invoke(update: Any) { + when (update) { + is MessageUpdate -> messageCallback ?.invoke(update) + is List<*> -> when (update.firstOrNull()) { + is MessageUpdate -> update.mapNotNull { it as? MessageUpdate }.let { mappedList -> + messageMediaGroupCallback ?.also { receiver -> + receiver(mappedList.mapNotNull { it.toMediaGroupUpdate() }) + } ?: messageCallback ?.also { receiver -> + mappedList.forEach { receiver(it) } + } + } + is EditMessageUpdate -> update.mapNotNull { it as? EditMessageUpdate }.let { mappedList -> + editedMessageMediaGroupCallback ?.also { receiver -> + receiver(mappedList.mapNotNull { it.toMediaGroupUpdate() }) + } ?: editedMessageCallback ?.also { receiver -> + mappedList.forEach { receiver(it) } + } + } + is ChannelPostUpdate -> update.mapNotNull { it as? ChannelPostUpdate }.let { mappedList -> + channelPostMediaGroupCallback ?.also { receiver -> + receiver(mappedList.mapNotNull { it.toMediaGroupUpdate() }) + } ?: channelPostCallback ?.also { receiver -> + mappedList.forEach { receiver(it) } + } + } + is EditChannelPostUpdate -> update.mapNotNull { it as? EditChannelPostUpdate }.let { mappedList -> + editedChannelPostMediaGroupCallback ?.also { receiver -> + receiver(mappedList.mapNotNull { it.toMediaGroupUpdate() }) + } ?: editedChannelPostCallback ?.also { receiver -> + mappedList.forEach { receiver(it) } + } + } + } + is EditMessageUpdate -> editedMessageCallback ?.invoke(update) + is ChannelPostUpdate -> channelPostCallback ?.invoke(update) + is EditChannelPostUpdate -> editedChannelPostCallback ?.invoke(update) + is ChosenInlineResultUpdate -> chosenInlineResultCallback ?.invoke(update) + is InlineQueryUpdate -> inlineQueryCallback ?.invoke(update) + is CallbackQueryUpdate -> callbackQueryCallback ?.invoke(update) + is ShippingQueryUpdate -> shippingQueryCallback ?.invoke(update) + is PreCheckoutQueryUpdate -> preCheckoutQueryCallback ?.invoke(update) + } + } +} + +fun createSimpleUpdateFilter( + 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 +): UpdatesFilter = UpdatesFilter( + 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 +) diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesPoller.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesPoller.kt new file mode 100644 index 0000000000..fa604d42ab --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesPoller.kt @@ -0,0 +1,94 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor +import com.github.insanusmokrassar.TelegramBotAPI.requests.GetUpdates +import com.github.insanusmokrassar.TelegramBotAPI.types.UpdateIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdate +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update +import com.github.insanusmokrassar.TelegramBotAPI.utils.mediaGroupId +import com.github.insanusmokrassar.TelegramBotAPI.utils.toMediaGroupUpdate +import kotlinx.coroutines.* +import java.util.concurrent.Executors + +class UpdatesPoller( + private val executor: RequestsExecutor, + private val requestsDelayMillis: Long = 1000, + private val scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + private val allowedUpdates: List? = null, + private val block: UpdateReceiver +) { + private var lastHandledUpdate: UpdateIdentifier = 0L + private val mediaGroup: MutableList = mutableListOf() + + private var pollerJob: Job? = null + + private suspend fun sendToBlock(data: Any) { + block(data) + lastHandledUpdate = when (data) { + is Update -> data.updateId + is List<*> -> (data.last() as? Update) ?.updateId ?: throw IllegalStateException( + "Found non-updates oriented list" + ) + else -> throw IllegalStateException( + "Unknown type of data" + ) + } + } + + private suspend fun pushMediaGroupUpdate(mediaGroupUpdate: MediaGroupUpdate? = null) { + val inputMediaGroupId = mediaGroupUpdate ?.data ?.mediaGroupId + if (mediaGroup.isNotEmpty() && inputMediaGroupId ?.equals(mediaGroup.mediaGroupId) != true) { + sendToBlock(listOf(*mediaGroup.toTypedArray())) + mediaGroup.clear() + } + mediaGroupUpdate ?.let { + mediaGroup.add(it) + } + } + + private suspend fun getUpdates(): List { + return executor.execute( + GetUpdates( + lastHandledUpdate + 1, // incremented because offset counted from 1 when updates id from 0 + allowed_updates = allowedUpdates + ) + ).map { + it.asUpdate + } + } + + private suspend fun handleUpdates(updates: List) { + updates.forEach { update -> + val mediaGroupUpdate = (update as? BaseMessageUpdate) ?.toMediaGroupUpdate() + mediaGroupUpdate ?.let { _ -> + pushMediaGroupUpdate(mediaGroupUpdate) + } ?: let { + pushMediaGroupUpdate() + sendToBlock(update) + } + } + + pushMediaGroupUpdate() + } + + fun start(): Job { + return pollerJob ?: scope.launch { + while (isActive) { + delay(requestsDelayMillis) + try { + val updates = getUpdates() + handleUpdates(updates) + } catch (e: Exception) { + e.printStackTrace() + } + } + }.also { + pollerJob = it + } + } + + suspend fun stop() { + pollerJob ?.cancelAndJoin() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesPolling.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesPolling.kt new file mode 100644 index 0000000000..f4f6480035 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesPolling.kt @@ -0,0 +1,89 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor +import com.github.insanusmokrassar.TelegramBotAPI.types.update.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate +import kotlinx.coroutines.* +import java.util.concurrent.Executors + +typealias UpdateReceiver = suspend (T) -> Unit + +fun RequestsExecutor.startGettingOfUpdates( + requestsDelayMillis: Long = 1000, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + allowedUpdates: List? = null, + block: UpdateReceiver +): Job { + return UpdatesPoller(this, requestsDelayMillis, scope, allowedUpdates, block).start() +} + +fun RequestsExecutor.startGettingOfUpdates( + 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, + requestsDelayMillis: Long = 1000, + scope: CoroutineScope = GlobalScope +): Job { + val filter = UpdatesFilter( + messageCallback, + messageMediaGroupCallback, + editedMessageCallback, + editedMessageMediaGroupCallback, + channelPostCallback, + channelPostMediaGroupCallback, + editedChannelPostCallback, + editedChannelPostMediaGroupCallback, + chosenInlineResultCallback, + inlineQueryCallback, + callbackQueryCallback, + shippingQueryCallback, + preCheckoutQueryCallback + ) + return startGettingOfUpdates( + requestsDelayMillis, + scope, + filter.allowedUpdates, + filter.asUpdateReceiver + ) +} + +fun RequestsExecutor.startGettingOfUpdates( + 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, + requestsDelayMillis: Long = 1000, + scope: CoroutineScope = GlobalScope +): Job = startGettingOfUpdates( + 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, + requestsDelayMillis = requestsDelayMillis, + scope = scope +) diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/WebhookPrivateKeyConfig.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/WebhookPrivateKeyConfig.kt new file mode 100644 index 0000000000..bb5f4810c4 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/WebhookPrivateKeyConfig.kt @@ -0,0 +1,23 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import java.io.FileInputStream +import java.security.KeyStore + +@Serializable +data class WebhookPrivateKeyConfig( + private val keyStorePath: String, + private val keyStorePassword: String, + val aliasName: String, + private val aliasPassword: String +) { + @Transient + val keyStore = KeyStore.getInstance("JKS").apply { + load(FileInputStream(keyStorePath), keyStorePassword()) + } + + fun keyStorePassword(): CharArray = keyStorePassword.toCharArray() + + fun aliasPassword(): CharArray = aliasPassword.toCharArray() +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt new file mode 100644 index 0000000000..e7124e23c6 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/Webhooks.kt @@ -0,0 +1,154 @@ +package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions + +import com.github.insanusmokrassar.TelegramBotAPI.bot.RequestsExecutor +import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.InputFile +import com.github.insanusmokrassar.TelegramBotAPI.requests.webhook.SetWebhook +import com.github.insanusmokrassar.TelegramBotAPI.types.MediaGroupIdentifier +import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.MediaGroupMessage +import com.github.insanusmokrassar.TelegramBotAPI.types.update.* +import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.Update +import io.ktor.application.Application +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 io.ktor.server.netty.Netty +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.serialization.json.Json +import java.io.FileInputStream +import java.security.KeyStore +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + + + +/** + * Reverse proxy webhook. + * + * @param url URL of webhook WITHOUT including of [port] + * @param port port which will be listen by bot + * @param certificate [com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.MultipartFile] or [com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.FileId] + * which will be used by telegram to send encrypted messages + * @param scope Scope which will be used for + */ +suspend fun RequestsExecutor.setWebhook( + url: String, + port: Int, + certificate: InputFile? = null, + privateKeyConfig: WebhookPrivateKeyConfig? = null, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + allowedUpdates: List? = null, + maxAllowedConnections: Int? = null, + engineFactory: ApplicationEngineFactory<*, *> = Netty, + block: UpdateReceiver +): Job { + val executeDeferred = certificate ?.let { + executeAsync( + SetWebhook( + url, + certificate, + maxAllowedConnections, + allowedUpdates + ) + ) + } ?: executeAsync( + SetWebhook( + url, + maxAllowedConnections, + allowedUpdates + ) + ) + val updatesChannel = Channel(Channel.UNLIMITED) + val mediaGroupChannel = Channel>(Channel.UNLIMITED) + val mediaGroupAccumulatedChannel = mediaGroupChannel.accumulateByKey( + 1000L, + scope = scope + ) + val env = applicationEngineEnvironment { + + module { + fun Application.main() { + routing { + post { + val deserialized = call.receiveText() + val update = Json.nonstrict.parse( + RawUpdate.serializer(), + deserialized + ) + updatesChannel.send(update.asUpdate) + call.respond("Ok") + } + } + } + main() + } + privateKeyConfig ?.let { + sslConnector( + privateKeyConfig.keyStore, + privateKeyConfig.aliasName, + privateKeyConfig::keyStorePassword, + privateKeyConfig::aliasPassword + ) { + host = "0.0.0.0" + this.port = port + } + } ?: connector { + host = "localhost" + this.port = port + } + + } + val engine = embeddedServer(engineFactory, env) + + try { + executeDeferred.await() + } catch (e: Exception) { + env.stop() + throw e + } + + return scope.launch { + launch { + for (update in updatesChannel) { + val data = update.data + when (data) { + is MediaGroupMessage -> mediaGroupChannel.send(data.mediaGroupId to update) + else -> block(update) + } + } + } + launch { + for (mediaGroupUpdate in mediaGroupAccumulatedChannel) { + block(mediaGroupUpdate.second) + } + } + engine.start(false) + }.also { + it.invokeOnCompletion { + engine.stop(1000L, 0L, TimeUnit.MILLISECONDS) + } + } +} + +suspend fun RequestsExecutor.setWebhook( + url: String, + port: Int, + filter: UpdatesFilter, + certificate: InputFile? = null, + privateKeyConfig: WebhookPrivateKeyConfig? = null, + scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + maxAllowedConnections: Int? = null, + engineFactory: ApplicationEngineFactory<*, *> = Netty +): Job = setWebhook( + url, + port, + certificate, + privateKeyConfig, + scope, + filter.allowedUpdates, + maxAllowedConnections, + engineFactory, + filter.asUpdateReceiver +)