1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2024-11-22 16:23:48 +00:00

Merge pull request #22 from InsanusMokrassar/0.12.0

0.12.0
This commit is contained in:
InsanusMokrassar 2019-03-13 18:30:32 -05:00 committed by GitHub
commit 7abaacb96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 925 additions and 301 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package com.github.insanusmokrassar.TelegramBotAPI.utils.extensions
import java.lang.ref.WeakReference
fun <T> T.asReference() = WeakReference(this)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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