mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI.git
synced 2024-11-22 16:23:48 +00:00
commit
7abaacb96d
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,5 +1,28 @@
|
|||||||
# TelegramBotAPI changelog
|
# 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<BaseMessageUpdate>#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
|
## 0.11.0
|
||||||
|
|
||||||
* Kotlin `1.3.11` -> `1.3.21`
|
* Kotlin `1.3.11` -> `1.3.21`
|
||||||
|
37
README.md
37
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
|
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).
|
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?
|
## How to work with library?
|
||||||
|
|
||||||
By default in any documentation will be meaning that you have variable in scope with names
|
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
|
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
|
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)
|
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.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
project.version = "0.11.0"
|
project.version = "0.12.0"
|
||||||
project.group = "com.github.insanusmokrassar"
|
project.group = "com.github.insanusmokrassar"
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
@ -34,9 +34,13 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlin_serialisation_runtime_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlin_serialisation_runtime_version"
|
||||||
implementation "joda-time:joda-time:$joda_time_version"
|
implementation "joda-time:joda-time:$joda_time_version"
|
||||||
|
|
||||||
implementation "io.ktor:ktor-client-core:$ktor_version"
|
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||||
implementation "io.ktor:ktor-client-okhttp:$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
|
// Use JUnit test framework
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,8 @@ class KtorRequestsExecutor(
|
|||||||
} ?: call.let {
|
} ?: call.let {
|
||||||
throw newRequestException(
|
throw newRequestException(
|
||||||
responseObject,
|
responseObject,
|
||||||
"Can't get result object"
|
content,
|
||||||
|
"Can't get result object from $content"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
@ -5,18 +5,36 @@ import java.io.IOException
|
|||||||
|
|
||||||
fun newRequestException(
|
fun newRequestException(
|
||||||
response: Response<*>,
|
response: Response<*>,
|
||||||
|
plainAnswer: String,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
cause: Throwable? = null
|
cause: Throwable? = null
|
||||||
) = when (response.description) {
|
) = when (response.description) {
|
||||||
"Bad Request: reply message not found" -> ReplyMessageNotFound(response, message, cause)
|
"Bad Request: reply message not found" -> ReplyMessageNotFoundException(response, plainAnswer, message, cause)
|
||||||
else -> RequestException(response, 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 response: Response<*>,
|
||||||
|
val plainAnswer: String,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
cause: Throwable? = null
|
cause: Throwable? = null
|
||||||
) : IOException(
|
) : IOException(
|
||||||
message,
|
message,
|
||||||
cause
|
cause
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
@ -2,27 +2,30 @@ package com.github.insanusmokrassar.TelegramBotAPI.requests
|
|||||||
|
|
||||||
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest
|
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest
|
||||||
import com.github.insanusmokrassar.TelegramBotAPI.types.UpdateIdentifier
|
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 com.github.insanusmokrassar.TelegramBotAPI.types.update.RawUpdate
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.internal.ArrayListSerializer
|
import kotlinx.serialization.internal.ArrayListSerializer
|
||||||
|
|
||||||
const val UPDATE_MESSAGE = "message"
|
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_MESSAGE", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_MESSAGE"))
|
||||||
const val UPDATE_EDITED_MESSAGE = "edited_message"
|
const val UPDATE_MESSAGE = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_MESSAGE
|
||||||
const val UPDATE_CHANNEL_POST = "channel_post"
|
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_EDITED_MESSAGE", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_MESSAGE"))
|
||||||
const val UPDATE_EDITED_CHANNEL_POST = "edited_channel_post"
|
const val UPDATE_EDITED_MESSAGE = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_MESSAGE
|
||||||
const val UPDATE_CHOSEN_INLINE_RESULT = "chosen_inline_result"
|
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CHANNEL_POST", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHANNEL_POST"))
|
||||||
const val UPDATE_INLINE_QUERY = "inline_query"
|
const val UPDATE_CHANNEL_POST = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHANNEL_POST
|
||||||
const val UPDATE_CALLBACK_QUERY = "callback_query"
|
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_EDITED_CHANNEL_POST", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_CHANNEL_POST"))
|
||||||
const val UPDATE_SHIPPING_QUERY = "shipping_query"
|
const val UPDATE_EDITED_CHANNEL_POST = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_CHANNEL_POST
|
||||||
const val UPDATE_PRE_CHECKOUT_QUERY = "pre_checkout_query"
|
@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
|
@Serializable
|
||||||
data class GetUpdates(
|
data class GetUpdates(
|
||||||
@Optional
|
@Optional
|
||||||
@ -32,17 +35,7 @@ data class GetUpdates(
|
|||||||
@Optional
|
@Optional
|
||||||
val timeout: Int? = null,
|
val timeout: Int? = null,
|
||||||
@Optional
|
@Optional
|
||||||
val allowed_updates: List<String>? = listOf(
|
val allowed_updates: List<String>? = ALL_UPDATES_LIST
|
||||||
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
|
|
||||||
)
|
|
||||||
): SimpleRequest<List<RawUpdate>> {
|
): SimpleRequest<List<RawUpdate>> {
|
||||||
override fun method(): String = "getUpdates"
|
override fun method(): String = "getUpdates"
|
||||||
|
|
||||||
|
@ -5,12 +5,13 @@ import com.github.insanusmokrassar.TelegramBotAPI.utils.toJsonWithoutNulls
|
|||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
|
@Serializable(RequestSerializer::class)
|
||||||
interface Request<T: Any> {
|
interface Request<T: Any> {
|
||||||
fun method(): String
|
fun method(): String
|
||||||
fun resultSerializer(): KSerializer<T>
|
fun resultSerializer(): KSerializer<T>
|
||||||
@ImplicitReflectionSerializer
|
fun json(): JsonObject = toJsonWithoutNulls(RequestSerializer)
|
||||||
fun json(): JsonObject = toJsonWithoutNulls()
|
|
||||||
}
|
}
|
||||||
|
object RequestSerializer : KSerializer<Request<*>> by ContextSerializer(Request::class)
|
||||||
|
|
||||||
fun <T : Any> StringFormat.extractResult(
|
fun <T : Any> StringFormat.extractResult(
|
||||||
from: String,
|
from: String,
|
||||||
|
@ -87,7 +87,7 @@ data class SendAnimationData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>,
|
TextableSendMessageRequest<RawMessage>,
|
||||||
|
@ -88,7 +88,7 @@ data class SendAudioData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>,
|
TextableSendMessageRequest<RawMessage>,
|
||||||
|
@ -72,7 +72,7 @@ data class SendDocumentData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>,
|
TextableSendMessageRequest<RawMessage>,
|
||||||
|
@ -66,7 +66,7 @@ data class SendMediaGroupData internal constructor(
|
|||||||
@SerialName(replyToMessageIdField)
|
@SerialName(replyToMessageIdField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyToMessageId: MessageIdentifier? = null
|
override val replyToMessageId: MessageIdentifier? = null
|
||||||
) : Data<List<RawMessage>>,
|
) : DataRequest<List<RawMessage>>,
|
||||||
SendMessageRequest<List<RawMessage>>
|
SendMessageRequest<List<RawMessage>>
|
||||||
{
|
{
|
||||||
@SerialName(mediaField)
|
@SerialName(mediaField)
|
||||||
|
@ -58,7 +58,7 @@ data class SendPhotoData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>
|
TextableSendMessageRequest<RawMessage>
|
||||||
|
@ -92,7 +92,7 @@ data class SendVideoData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>,
|
TextableSendMessageRequest<RawMessage>,
|
||||||
|
@ -82,7 +82,7 @@ data class SendVideoNoteData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>,
|
TextableSendMessageRequest<RawMessage>,
|
||||||
|
@ -77,7 +77,7 @@ data class SendVoiceData internal constructor(
|
|||||||
@SerialName(replyMarkupField)
|
@SerialName(replyMarkupField)
|
||||||
@Optional
|
@Optional
|
||||||
override val replyMarkup: KeyboardMarkup? = null
|
override val replyMarkup: KeyboardMarkup? = null
|
||||||
) : Data<RawMessage>,
|
) : DataRequest<RawMessage>,
|
||||||
SendMessageRequest<RawMessage>,
|
SendMessageRequest<RawMessage>,
|
||||||
ReplyingMarkupSendMessageRequest<RawMessage>,
|
ReplyingMarkupSendMessageRequest<RawMessage>,
|
||||||
TextableSendMessageRequest<RawMessage>,
|
TextableSendMessageRequest<RawMessage>,
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base
|
package com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base
|
||||||
|
|
||||||
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest
|
@Deprecated(
|
||||||
|
"Renamed to DataRequest",
|
||||||
interface Data<T: Any> : SimpleRequest<T>
|
ReplaceWith(
|
||||||
|
"DataRequest",
|
||||||
|
"com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base.DataRequest"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
typealias Data<T> = DataRequest<T>
|
||||||
|
@ -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<T: Any> : SimpleRequest<T>
|
||||||
|
|
||||||
|
object DataRequestSerializer : KSerializer<DataRequest<*>> by ContextSerializer(DataRequest::class)
|
@ -9,7 +9,7 @@ import kotlinx.serialization.json.JsonObject
|
|||||||
/**
|
/**
|
||||||
* Will be used as SimpleRequest if
|
* Will be used as SimpleRequest if
|
||||||
*/
|
*/
|
||||||
class MultipartRequestImpl<D: Data<R>, F: Files, R: Any>(
|
class MultipartRequestImpl<D: DataRequest<R>, F: Files, R: Any>(
|
||||||
val data: D,
|
val data: D,
|
||||||
val files: F
|
val files: F
|
||||||
) : MultipartRequest<R> {
|
) : MultipartRequest<R> {
|
||||||
|
@ -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<WebhookInfo> {
|
||||||
|
override fun method(): String = "getWebhookInfo"
|
||||||
|
|
||||||
|
override fun resultSerializer(): KSerializer<WebhookInfo> = WebhookInfo.serializer()
|
||||||
|
}
|
@ -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<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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SetWebhook(
|
||||||
|
url: String,
|
||||||
|
maxAllowedConnections: Int? = null,
|
||||||
|
allowedUpdates: List<String>? = null
|
||||||
|
) : Request<Boolean> = 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<String>? = null
|
||||||
|
) : DataRequest<Boolean> {
|
||||||
|
override fun method(): String = "setWebhook"
|
||||||
|
override fun resultSerializer(): KSerializer<Boolean> = BooleanSerializer
|
||||||
|
|
||||||
|
init {
|
||||||
|
maxAllowedConnections ?.let {
|
||||||
|
if (it !in allowedConnectionsLength) {
|
||||||
|
throw IllegalArgumentException("Allowed connection for webhook must be in $allowedConnectionsLength range (but passed $it)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ val userProfilePhotosRequestLimit = 0 .. 100
|
|||||||
val chatTitleLength = 1 until 255
|
val chatTitleLength = 1 until 255
|
||||||
val chatDescriptionLength = 0 until 256
|
val chatDescriptionLength = 0 until 256
|
||||||
val inlineResultQueryIdLingth = 1 until 64
|
val inlineResultQueryIdLingth = 1 until 64
|
||||||
|
val allowedConnectionsLength = 1 .. 100
|
||||||
|
|
||||||
val invoiceTitleLimit = 1 until 32
|
val invoiceTitleLimit = 1 until 32
|
||||||
val invoiceDescriptionLimit = 1 until 256
|
val invoiceDescriptionLimit = 1 until 256
|
||||||
@ -68,6 +69,12 @@ const val isPersonalField = "is_personal"
|
|||||||
const val nextOffsetField = "next_offset"
|
const val nextOffsetField = "next_offset"
|
||||||
const val switchPmTextField = "switch_pm_text"
|
const val switchPmTextField = "switch_pm_text"
|
||||||
const val switchPmParameterField = "switch_pm_parameter"
|
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"
|
const val photoUrlField = "photo_url"
|
||||||
@ -172,6 +179,7 @@ const val pricesField = "prices"
|
|||||||
const val payloadField = "payload"
|
const val payloadField = "payload"
|
||||||
const val vcardField = "vcard"
|
const val vcardField = "vcard"
|
||||||
const val resultsField = "results"
|
const val resultsField = "results"
|
||||||
|
const val certificateField = "certificate"
|
||||||
|
|
||||||
const val pointField = "point"
|
const val pointField = "point"
|
||||||
const val xShiftField = "x_shift"
|
const val xShiftField = "x_shift"
|
||||||
|
@ -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
|
||||||
|
)
|
@ -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<String> = 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
|
||||||
|
}
|
@ -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
|
@ -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)
|
||||||
|
})
|
@ -3,6 +3,7 @@ package com.github.insanusmokrassar.TelegramBotAPI.utils
|
|||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
@Deprecated("This method can throw exceptions")
|
||||||
@ImplicitReflectionSerializer
|
@ImplicitReflectionSerializer
|
||||||
inline fun <reified T: Any> T.toJsonWithoutNulls(): JsonObject = Json.nonstrict.toJson(
|
inline fun <reified T: Any> T.toJsonWithoutNulls(): JsonObject = Json.nonstrict.toJson(
|
||||||
this
|
this
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package com.github.insanusmokrassar.TelegramBotAPI.utils
|
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.chat.Chat
|
||||||
import com.github.insanusmokrassar.TelegramBotAPI.types.message.ForwardedMessage
|
import com.github.insanusmokrassar.TelegramBotAPI.types.message.ForwardedMessage
|
||||||
import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.*
|
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
|
import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate
|
||||||
|
|
||||||
val List<BaseMessageUpdate>.forwarded: ForwardedMessage?
|
val List<BaseMessageUpdate>.forwarded: ForwardedMessage?
|
||||||
@ -17,3 +19,6 @@ val List<BaseMessageUpdate>.replyTo: Message?
|
|||||||
|
|
||||||
val List<BaseMessageUpdate>.chat: Chat?
|
val List<BaseMessageUpdate>.chat: Chat?
|
||||||
get() = first().data.chat
|
get() = first().data.chat
|
||||||
|
|
||||||
|
val List<BaseMessageUpdate>.mediaGroupId: MediaGroupIdentifier?
|
||||||
|
get() = (first().data as? MediaGroupMessage) ?.mediaGroupId
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
fun <T> T.asReference() = WeakReference(this)
|
@ -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 <T: Any> RequestsExecutor.executeAsync(
|
||||||
|
request: Request<T>,
|
||||||
|
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 <T: Any> RequestsExecutor.executeAsync(
|
||||||
|
request: Request<T>,
|
||||||
|
scope: CoroutineScope = GlobalScope
|
||||||
|
): Deferred<T> {
|
||||||
|
return scope.async { execute(request) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T: Any> RequestsExecutor.executeUnsafe(
|
||||||
|
request: Request<T>,
|
||||||
|
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
|
||||||
|
}
|
@ -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<T> {
|
||||||
|
abstract val value: T
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AddValue<T>(override val value: T) : DebounceAction<T>()
|
||||||
|
private data class RemoveJob<T>(override val value: T, val job: Job) : DebounceAction<T>()
|
||||||
|
|
||||||
|
fun <T> ReceiveChannel<T>.debounceByValue(
|
||||||
|
delayMillis: Long,
|
||||||
|
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||||
|
resultBroadcastChannelCapacity: Int = 32
|
||||||
|
): ReceiveChannel<T> {
|
||||||
|
val outChannel = Channel<T>(resultBroadcastChannelCapacity)
|
||||||
|
val values = HashMap<T, Job>()
|
||||||
|
|
||||||
|
val channel = Channel<DebounceAction<T>>(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<K, V> = Pair<K, List<V>>
|
||||||
|
|
||||||
|
fun <K, V> ReceiveChannel<Pair<K, V>>.accumulateByKey(
|
||||||
|
delayMillis: Long,
|
||||||
|
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||||
|
resultBroadcastChannelCapacity: Int = 32
|
||||||
|
): ReceiveChannel<AccumulatedValues<K, V>> {
|
||||||
|
val outChannel = Channel<AccumulatedValues<K, V>>(resultBroadcastChannelCapacity)
|
||||||
|
val values = HashMap<K, MutableList<V>>()
|
||||||
|
val jobs = HashMap<K, Job>()
|
||||||
|
|
||||||
|
val channel = Channel<DebounceAction<Pair<K, V>>>(Channel.UNLIMITED)
|
||||||
|
scope.launch {
|
||||||
|
for (action in channel) {
|
||||||
|
val (key, value) = action.value
|
||||||
|
when (action) {
|
||||||
|
is AddValue -> {
|
||||||
|
jobs[key] ?.cancel()
|
||||||
|
(values[key] ?: mutableListOf<V>().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
|
||||||
|
}
|
@ -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<T> = suspend (T) -> Unit
|
|
||||||
|
|
||||||
fun RequestsExecutor.startGettingOfUpdates(
|
|
||||||
requestsDelayMillis: Long = 1000,
|
|
||||||
scope: CoroutineScope = GlobalScope,
|
|
||||||
allowedUpdates: List<String>? = null,
|
|
||||||
block: UpdateReceiver<Any>
|
|
||||||
): 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<Any>()
|
|
||||||
var mediaGroup: MutableList<Update>? = 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<MessageUpdate>? = null,
|
|
||||||
messageMediaGroupCallback: UpdateReceiver<List<MessageUpdate>>? = null,
|
|
||||||
editedMessageCallback: UpdateReceiver<EditMessageUpdate>? = null,
|
|
||||||
editedMessageMediaGroupCallback: UpdateReceiver<List<EditMessageUpdate>>? = null,
|
|
||||||
channelPostCallback: UpdateReceiver<ChannelPostUpdate>? = null,
|
|
||||||
channelPostMediaGroupCallback: UpdateReceiver<List<ChannelPostUpdate>>? = null,
|
|
||||||
editedChannelPostCallback: UpdateReceiver<EditChannelPostUpdate>? = null,
|
|
||||||
editedChannelPostMediaGroupCallback: UpdateReceiver<List<EditChannelPostUpdate>>? = null,
|
|
||||||
chosenInlineResultCallback: UpdateReceiver<ChosenInlineResultUpdate>? = null,
|
|
||||||
inlineQueryCallback: UpdateReceiver<InlineQueryUpdate>? = null,
|
|
||||||
callbackQueryCallback: UpdateReceiver<CallbackQueryUpdate>? = null,
|
|
||||||
shippingQueryCallback: UpdateReceiver<ShippingQueryUpdate>? = null,
|
|
||||||
preCheckoutQueryCallback: UpdateReceiver<PreCheckoutQueryUpdate>? = 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<MessageUpdate>? = null,
|
|
||||||
mediaGroupCallback: UpdateReceiver<List<BaseMessageUpdate>>? = 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,
|
|
||||||
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 <T: Any> RequestsExecutor.executeAsync(
|
|
||||||
request: Request<T>,
|
|
||||||
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 <T: Any> RequestsExecutor.executeAsync(
|
|
||||||
request: Request<T>,
|
|
||||||
scope: CoroutineScope = GlobalScope
|
|
||||||
): Deferred<T> {
|
|
||||||
return scope.async { execute(request) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T: Any> RequestsExecutor.executeUnsafe(
|
|
||||||
request: Request<T>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<MessageUpdate>? = null,
|
||||||
|
private val messageMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
private val editedMessageCallback: UpdateReceiver<EditMessageUpdate>? = null,
|
||||||
|
private val editedMessageMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
private val channelPostCallback: UpdateReceiver<ChannelPostUpdate>? = null,
|
||||||
|
private val channelPostMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
private val editedChannelPostCallback: UpdateReceiver<EditChannelPostUpdate>? = null,
|
||||||
|
private val editedChannelPostMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
private val chosenInlineResultCallback: UpdateReceiver<ChosenInlineResultUpdate>? = null,
|
||||||
|
private val inlineQueryCallback: UpdateReceiver<InlineQueryUpdate>? = null,
|
||||||
|
private val callbackQueryCallback: UpdateReceiver<CallbackQueryUpdate>? = null,
|
||||||
|
private val shippingQueryCallback: UpdateReceiver<ShippingQueryUpdate>? = null,
|
||||||
|
private val preCheckoutQueryCallback: UpdateReceiver<PreCheckoutQueryUpdate>? = null
|
||||||
|
) {
|
||||||
|
val asUpdateReceiver: UpdateReceiver<Any> = 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<MessageUpdate>? = null,
|
||||||
|
mediaGroupCallback: UpdateReceiver<List<BaseMessageUpdate>>? = 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
|
||||||
|
): 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
|
||||||
|
)
|
@ -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<String>? = null,
|
||||||
|
private val block: UpdateReceiver<Any>
|
||||||
|
) {
|
||||||
|
private var lastHandledUpdate: UpdateIdentifier = 0L
|
||||||
|
private val mediaGroup: MutableList<MediaGroupUpdate> = 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<Update> {
|
||||||
|
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<Update>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -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<T> = suspend (T) -> Unit
|
||||||
|
|
||||||
|
fun RequestsExecutor.startGettingOfUpdates(
|
||||||
|
requestsDelayMillis: Long = 1000,
|
||||||
|
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
|
||||||
|
allowedUpdates: List<String>? = null,
|
||||||
|
block: UpdateReceiver<Any>
|
||||||
|
): Job {
|
||||||
|
return UpdatesPoller(this, requestsDelayMillis, scope, allowedUpdates, block).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RequestsExecutor.startGettingOfUpdates(
|
||||||
|
messageCallback: UpdateReceiver<MessageUpdate>? = null,
|
||||||
|
messageMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
editedMessageCallback: UpdateReceiver<EditMessageUpdate>? = null,
|
||||||
|
editedMessageMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
channelPostCallback: UpdateReceiver<ChannelPostUpdate>? = null,
|
||||||
|
channelPostMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
editedChannelPostCallback: UpdateReceiver<EditChannelPostUpdate>? = null,
|
||||||
|
editedChannelPostMediaGroupCallback: UpdateReceiver<List<MediaGroupUpdate>>? = null,
|
||||||
|
chosenInlineResultCallback: UpdateReceiver<ChosenInlineResultUpdate>? = null,
|
||||||
|
inlineQueryCallback: UpdateReceiver<InlineQueryUpdate>? = null,
|
||||||
|
callbackQueryCallback: UpdateReceiver<CallbackQueryUpdate>? = null,
|
||||||
|
shippingQueryCallback: UpdateReceiver<ShippingQueryUpdate>? = null,
|
||||||
|
preCheckoutQueryCallback: UpdateReceiver<PreCheckoutQueryUpdate>? = 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<MessageUpdate>? = null,
|
||||||
|
mediaGroupCallback: UpdateReceiver<List<BaseMessageUpdate>>? = 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,
|
||||||
|
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
|
||||||
|
)
|
@ -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()
|
||||||
|
}
|
@ -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<String>? = null,
|
||||||
|
maxAllowedConnections: Int? = null,
|
||||||
|
engineFactory: ApplicationEngineFactory<*, *> = Netty,
|
||||||
|
block: UpdateReceiver<Any>
|
||||||
|
): Job {
|
||||||
|
val executeDeferred = certificate ?.let {
|
||||||
|
executeAsync(
|
||||||
|
SetWebhook(
|
||||||
|
url,
|
||||||
|
certificate,
|
||||||
|
maxAllowedConnections,
|
||||||
|
allowedUpdates
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} ?: executeAsync(
|
||||||
|
SetWebhook(
|
||||||
|
url,
|
||||||
|
maxAllowedConnections,
|
||||||
|
allowedUpdates
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val updatesChannel = Channel<Update>(Channel.UNLIMITED)
|
||||||
|
val mediaGroupChannel = Channel<Pair<MediaGroupIdentifier, Update>>(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
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user