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
## 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
* 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
solves or unuseful moments are describing by official [Telegram Bot API](https://core.telegram.org/bots/api).
## Compatibility
This version compatible with [July 2018 update of TelegramBotAPI](https://core.telegram.org/bots/api#july-26-2018). That means that
most part of API has been implemented (according to last [August 2018 update of TelegramBotAPI](https://core.telegram.org/bots/api#august-27-2018))
except the Passport API which will be included as soon as possible.
## How to work with library?
By default in any documentation will be meaning that you have variable in scope with names
@ -27,3 +33,34 @@ executor.execute(GetMe())
As a result you will receive `User` object. This object used as is now (as in API documentation), but it is possible
that this class will be renamed to `RawUser` and you will be able to get real realisation of this object like `Bot` (in
cases when `isBot` == `true`) or `User` (otherwise)
## Getting updates
In this library currently realised two ways to get updates from telegram:
* Polling - in this case bot will request updates from time to time (you can set up delay between requests)
* Webhook via reverse proxy or something like this
### Updates filters
Currently webhook method contains `UpdatesFilter` as necessary argument for getting updates.
`UpdatesFilter` will sort updates and throw their into different callbacks. Currently supporting
separate getting updates for media groups - they are accumulating with debounce in one second
(for being sure that all objects of media group was received).
Updates polling also support `UpdatesFilter` but you must not use it and can get updates directly
in `UpdateReceiver`, which you will provide to `startGettingOfUpdates` method
### Webhook set up
If you wish to use webhook method, you will need:
* White IP - your IP address or host, which available for calling. [TelegramBotAPI](https://core.telegram.org/bots/api#setwebhook)
recommend to use some unique address for each bot which you are using
* SSL certificate. Usually you can obtain the certificate using your domain provider, [Let'sEncrypt](https://letsencrypt.org/) or [create it](https://core.telegram.org/bots/self-signed)
* Nginx or something like this
Template for Nginx server config you can find in [this gist](https://gist.github.com/InsanusMokrassar/fcc6e09cebd07e46e8f0fdec234750c4#file-nginxssl-conf).
For webhook you must provide `File` with public part of certificate, `URL` where bot placed and inner `PORT` which
will be used to start receiving of updates.

View File

@ -1,4 +1,4 @@
project.version = "0.11.0"
project.version = "0.12.0"
project.group = "com.github.insanusmokrassar"
buildscript {
@ -34,9 +34,13 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlin_serialisation_runtime_version"
implementation "joda-time:joda-time:$joda_time_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
implementation "io.ktor:ktor-server-core:$ktor_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
}

View File

@ -77,7 +77,8 @@ class KtorRequestsExecutor(
} ?: call.let {
throw newRequestException(
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(
response: Response<*>,
plainAnswer: String,
message: String? = null,
cause: Throwable? = null
) = when (response.description) {
"Bad Request: reply message not found" -> ReplyMessageNotFound(response, message, cause)
else -> RequestException(response, message, cause)
"Bad Request: reply message not found" -> ReplyMessageNotFoundException(response, plainAnswer, message, cause)
"Unauthorized" -> UnauthorizedException(response, plainAnswer, message, cause)
else -> CommonRequestException(response, plainAnswer, message, cause)
}
open class RequestException internal constructor(
sealed class RequestException constructor(
val response: Response<*>,
val plainAnswer: String,
message: String? = null,
cause: Throwable? = null
) : IOException(
message,
cause
)
)
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.types.UpdateIdentifier
import com.github.insanusmokrassar.TelegramBotAPI.types.ALL_UPDATES_LIST
import com.github.insanusmokrassar.TelegramBotAPI.types.update.RawUpdate
import kotlinx.serialization.*
import kotlinx.serialization.internal.ArrayListSerializer
const val UPDATE_MESSAGE = "message"
const val UPDATE_EDITED_MESSAGE = "edited_message"
const val UPDATE_CHANNEL_POST = "channel_post"
const val UPDATE_EDITED_CHANNEL_POST = "edited_channel_post"
const val UPDATE_CHOSEN_INLINE_RESULT = "chosen_inline_result"
const val UPDATE_INLINE_QUERY = "inline_query"
const val UPDATE_CALLBACK_QUERY = "callback_query"
const val UPDATE_SHIPPING_QUERY = "shipping_query"
const val UPDATE_PRE_CHECKOUT_QUERY = "pre_checkout_query"
/*
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_MESSAGE", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_MESSAGE"))
const val UPDATE_MESSAGE = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_MESSAGE
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_EDITED_MESSAGE", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_MESSAGE"))
const val UPDATE_EDITED_MESSAGE = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_MESSAGE
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CHANNEL_POST", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHANNEL_POST"))
const val UPDATE_CHANNEL_POST = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHANNEL_POST
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_EDITED_CHANNEL_POST", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_CHANNEL_POST"))
const val UPDATE_EDITED_CHANNEL_POST = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_EDITED_CHANNEL_POST
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CHOSEN_INLINE_RESULT", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHOSEN_INLINE_RESULT"))
const val UPDATE_CHOSEN_INLINE_RESULT = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CHOSEN_INLINE_RESULT
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_INLINE_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_INLINE_QUERY"))
const val UPDATE_INLINE_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_INLINE_QUERY
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_CALLBACK_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CALLBACK_QUERY"))
const val UPDATE_CALLBACK_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_CALLBACK_QUERY
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_SHIPPING_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_SHIPPING_QUERY"))
const val UPDATE_SHIPPING_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_SHIPPING_QUERY
@Deprecated("Replaced to other package", ReplaceWith("UPDATE_PRE_CHECKOUT_QUERY", "com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_PRE_CHECKOUT_QUERY"))
const val UPDATE_PRE_CHECKOUT_QUERY = com.github.insanusmokrassar.TelegramBotAPI.types.UPDATE_PRE_CHECKOUT_QUERY
@Optional private val inline_query: RawInlineQuery? = null,
@Optional private val chosen_inline_result: Unit? = null,
@Optional private val callback_query: RawCallbackQuery? = null,
@Optional private val shipping_query: Unit? = null,
@Optional private val pre_checkout_query: Unit? = null
*/
@Serializable
data class GetUpdates(
@Optional
@ -32,17 +35,7 @@ data class GetUpdates(
@Optional
val timeout: Int? = null,
@Optional
val allowed_updates: List<String>? = listOf(
UPDATE_MESSAGE,
UPDATE_EDITED_MESSAGE,
UPDATE_CHANNEL_POST,
UPDATE_EDITED_CHANNEL_POST,
UPDATE_CHOSEN_INLINE_RESULT,
UPDATE_INLINE_QUERY,
UPDATE_CALLBACK_QUERY,
UPDATE_SHIPPING_QUERY,
UPDATE_PRE_CHECKOUT_QUERY
)
val allowed_updates: List<String>? = ALL_UPDATES_LIST
): SimpleRequest<List<RawUpdate>> {
override fun method(): String = "getUpdates"

View File

@ -5,12 +5,13 @@ import com.github.insanusmokrassar.TelegramBotAPI.utils.toJsonWithoutNulls
import kotlinx.serialization.*
import kotlinx.serialization.json.JsonObject
@Serializable(RequestSerializer::class)
interface Request<T: Any> {
fun method(): String
fun resultSerializer(): KSerializer<T>
@ImplicitReflectionSerializer
fun json(): JsonObject = toJsonWithoutNulls()
fun json(): JsonObject = toJsonWithoutNulls(RequestSerializer)
}
object RequestSerializer : KSerializer<Request<*>> by ContextSerializer(Request::class)
fun <T : Any> StringFormat.extractResult(
from: String,

View File

@ -87,7 +87,7 @@ data class SendAnimationData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>,

View File

@ -88,7 +88,7 @@ data class SendAudioData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>,

View File

@ -72,7 +72,7 @@ data class SendDocumentData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>,

View File

@ -66,7 +66,7 @@ data class SendMediaGroupData internal constructor(
@SerialName(replyToMessageIdField)
@Optional
override val replyToMessageId: MessageIdentifier? = null
) : Data<List<RawMessage>>,
) : DataRequest<List<RawMessage>>,
SendMessageRequest<List<RawMessage>>
{
@SerialName(mediaField)

View File

@ -58,7 +58,7 @@ data class SendPhotoData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>

View File

@ -92,7 +92,7 @@ data class SendVideoData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>,

View File

@ -82,7 +82,7 @@ data class SendVideoNoteData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>,

View File

@ -77,7 +77,7 @@ data class SendVoiceData internal constructor(
@SerialName(replyMarkupField)
@Optional
override val replyMarkup: KeyboardMarkup? = null
) : Data<RawMessage>,
) : DataRequest<RawMessage>,
SendMessageRequest<RawMessage>,
ReplyingMarkupSendMessageRequest<RawMessage>,
TextableSendMessageRequest<RawMessage>,

View File

@ -1,5 +1,10 @@
package com.github.insanusmokrassar.TelegramBotAPI.requests.send.media.base
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.SimpleRequest
interface Data<T: Any> : SimpleRequest<T>
@Deprecated(
"Renamed to DataRequest",
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
*/
class MultipartRequestImpl<D: Data<R>, F: Files, R: Any>(
class MultipartRequestImpl<D: DataRequest<R>, F: Files, R: Any>(
val data: D,
val files: F
) : 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 chatDescriptionLength = 0 until 256
val inlineResultQueryIdLingth = 1 until 64
val allowedConnectionsLength = 1 .. 100
val invoiceTitleLimit = 1 until 32
val invoiceDescriptionLimit = 1 until 256
@ -68,6 +69,12 @@ const val isPersonalField = "is_personal"
const val nextOffsetField = "next_offset"
const val switchPmTextField = "switch_pm_text"
const val switchPmParameterField = "switch_pm_parameter"
const val maxAllowedConnectionsField = "max_connections"
const val allowedUpdatesField = "allowed_updates"
const val hasCustomCertificateField = "has_custom_certificate"
const val pendingUpdateCountField = "pending_update_count"
const val lastErrorDateField = "last_error_date"
const val lastErrorMessageField = "last_error_message"
const val photoUrlField = "photo_url"
@ -172,6 +179,7 @@ const val pricesField = "prices"
const val payloadField = "payload"
const val vcardField = "vcard"
const val resultsField = "results"
const val certificateField = "certificate"
const val pointField = "point"
const val xShiftField = "x_shift"

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.json.*
@Deprecated("This method can throw exceptions")
@ImplicitReflectionSerializer
inline fun <reified T: Any> T.toJsonWithoutNulls(): JsonObject = Json.nonstrict.toJson(
this

View File

@ -1,8 +1,10 @@
package com.github.insanusmokrassar.TelegramBotAPI.utils
import com.github.insanusmokrassar.TelegramBotAPI.types.MediaGroupIdentifier
import com.github.insanusmokrassar.TelegramBotAPI.types.chat.Chat
import com.github.insanusmokrassar.TelegramBotAPI.types.message.ForwardedMessage
import com.github.insanusmokrassar.TelegramBotAPI.types.message.abstracts.*
import com.github.insanusmokrassar.TelegramBotAPI.types.update.MediaGroupUpdate
import com.github.insanusmokrassar.TelegramBotAPI.types.update.abstracts.BaseMessageUpdate
val List<BaseMessageUpdate>.forwarded: ForwardedMessage?
@ -17,3 +19,6 @@ val List<BaseMessageUpdate>.replyTo: Message?
val List<BaseMessageUpdate>.chat: 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
)