Merge pull request #34 from InsanusMokrassar/0.14.0

0.14.0
This commit is contained in:
InsanusMokrassar 2019-05-07 08:21:45 +08:00 committed by GitHub
commit 59680439fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 236 additions and 200 deletions

View File

@ -1,5 +1,23 @@
# TelegramBotAPI changelog
## 0.14.0
* Now library have no default engine for both webhooks and requests executor. It is required for clients to set
some default library
* All proxy help methods was removed . They are will be replaced in separated project
* `Ktor` version `1.1.3` -> `1.1.4`
* Requests results now always decoding as `UTF-8`
* `AbstractRequestCallFactory` was added with cache of methods urls to avoid memory leaks
* Small refactoring of work with response in `KtorRequestsExecutor`
* Kotlin version `1.3.30` -> `1.3.31`
* Kotlin coroutines `1.2.0` -> `1.2.1`
* `CommonForwardedMessage` was renamed to `UserForwardedMessage`
* All forwarded messages are now just childs of `ForwardedMessage`:
* `AnonymousForwardedMessage` - for messages without forwarded info
* `UserForwardedMessage` - for messages from users and groups (contains not message id)
* `ForwardedFromChannelMessage` - for messages from channels
* Changed logic of forwarded messages preparing
## 0.13.0 Telegram Polls
* Type `PollOption` and `AnonymousPollOption` added

View File

@ -46,23 +46,82 @@ compile "com.github.insanusmokrassar:TelegramBotAPI:${telegrambotapi.version}"
## How to work with library?
By default in any documentation will be meaning that you have variable in scope with names
For now, this library have no some API god-object. Instead of this, this library has several
important objects:
| Name of variable | Description | Where to get? (Examples) |
|:----------------:|:-----------:|:------------------------:|
| executor | [RequestsExecutor](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/RequestsExecutor.kt) | [Ktor RequestExecutor realisation](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/Ktor/KtorRequestsExecutor.kt) |
* [RequestsExecutor](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/bot/RequestsExecutor.kt)
* [Requests](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests)
* [Types](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types)
## Requests Examples
### Types
### Get Me
Types declare different objects representation. For example, `Chat` for now represented as
interface and has several realisations:
* PrivateChat
* GroupChat
* SupergroupChat
* ChannelChat
Instead of common garbage with all information as in original [Chat](https://core.telegram.org/bots/api#chat),
here it was separated for more obvious difference between chats types and their possible content.
The same principle work with a lot of others things in this Telegram bot API.
### Requests
Requests usually are very simple objects, but some of them are using their own
build factories. For example, the next code show, how to get information about bot:
```kotlin
executor.execute(GetMe())
val requestsExecutor: RequestsExecutor = ...
requestsExecutor.execute(GetMe())
```
The result type of [GetMe](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/requests/GetMe) request is
[User](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/types/User). In fact, in this result must contain
`isBot` equal to `true` always.
### RequestsExecutor
It is base object which can be used to execute requests in API. For now by default included Ktor
realisation of `RequestsExecutor`, but it is possible, that in future it will be extracted in separated
project. How to create `RequestsExecutor`:
```kotlin
val requestsExecutor = KtorRequestsExecutor(TOKEN)
```
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)
Here `KtorRequestsExecutor` - default realisation with Ktor. `TOKEN` is just a token of bot which was retrieved
according to [instruction](https://core.telegram.org/bots#3-how-do-i-create-a-bot).
Besides, for correct usage of this, you must implement in your project both one of engines for client and server
Ktor libraries:
```groovy
dependencies {
// ...
implementation "io.ktor:ktor-server-cio:$ktor_version"
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
// ...
}
```
It is able to avoid using of `server` dependency in case if will not be used `Webhook`s. In this case,
dependencies list will be simplify:
```groovy
dependencies {
// ...
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
// ...
}
```
Here was used `okhttp` realisation of client, but there are several others engines for Ktor. More information
available on ktor.io site for [client](https://ktor.io/clients/http-client/engines.html) and [server](https://ktor.io/quickstart/artifacts.html)
engines.
## Getting updates
@ -95,3 +154,22 @@ Template for Nginx server config you can find in [this gist](https://gist.github
For webhook you can provide `File` with public part of certificate, `URL` where bot will be available and inner `PORT` which
will be used to start receiving of updates. Actually, you can skip passing of `File` when you have something like
nginx for proxy forwarding.
In case of using `nginx` with reverse-proxy config, setting up of Webhook will look like:
```kotlin
requestsExecutor.setWebhook(
WEBHOOK_URL,
INTERNAL_PORT,
filter,
ENGINE_FACTORY
)
```
Here:
* `WEBHOOK_URL` - the url which will be used by Telegram system to send updates
* `INTERNAL_PORT` - the port which will be used in bot for listening of updates
* `filter` - instance of [UpdatesFilter](src/main/kotlin/com/github/insanusmokrassar/TelegramBotAPI/utils/extensions/UpdatesFilter.kt),
which will be used to filter incoming updates
* `ENGINE_FACTORY` - used factory name, for example, `CIO` in case of usage `io.ktor:ktor-server-cio` as server engine

View File

@ -1,4 +1,4 @@
project.version = "0.13.0"
project.version = "0.14.0"
project.group = "com.github.insanusmokrassar"
buildscript {
@ -26,7 +26,6 @@ repositories {
jcenter()
mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
maven { url "https://dl.bintray.com/kotlin/ktor" }
}
dependencies {
@ -35,14 +34,10 @@ dependencies {
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-client:$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'
implementation "io.ktor:ktor-server:$ktor_version"
implementation "io.ktor:ktor-server-host-common:$ktor_version"
}
compileKotlin {

View File

@ -1,9 +1,9 @@
kotlin.code.style=official
kotlin_version=1.3.30
kotlin_coroutines_version=1.2.0
kotlin_version=1.3.31
kotlin_coroutines_version=1.2.1
kotlin_serialisation_runtime_version=0.11.0
joda_time_version=2.10.1
ktor_version=1.1.3
ktor_version=1.1.4
gradle_bintray_plugin_version=1.8.4

View File

@ -1,16 +0,0 @@
package com.github.insanusmokrassar.TelegramBotAPI
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorRequestsExecutor
import io.ktor.client.engine.okhttp.OkHttp
import kotlinx.coroutines.runBlocking
fun main(args: Array<String>) {
runBlocking {
KtorRequestsExecutor(
args[0],
OkHttp.create()
).apply {
// It is just template of creating requests executor
}
}
}

View File

@ -12,7 +12,6 @@ import com.github.insanusmokrassar.TelegramBotAPI.types.RetryAfterError
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.util.cio.toByteArray
import kotlinx.coroutines.delay
import kotlinx.io.charsets.Charset
@ -20,7 +19,7 @@ import kotlinx.serialization.json.Json
class KtorRequestsExecutor(
token: String,
private val client: HttpClient = HttpClient(OkHttp),
private val client: HttpClient = HttpClient(),
hostUrl: String = "https://api.telegram.org",
callsFactories: List<KtorCallFactory> = emptyList(),
excludeDefaultFactories: Boolean = false,
@ -29,11 +28,11 @@ class KtorRequestsExecutor(
) : BaseRequestsExecutor(token, hostUrl) {
constructor(
token: String,
engine: HttpClientEngine = OkHttp.create(),
engine: HttpClientEngine? = null,
hostUrl: String = "https://api.telegram.org"
) : this(
token,
HttpClient(engine),
engine ?.let { HttpClient(engine) } ?: HttpClient(),
hostUrl
)
@ -61,7 +60,9 @@ class KtorRequestsExecutor(
if (call == null) {
throw IllegalArgumentException("Can't execute request: $request")
}
val content = call.response.content.toByteArray().toString(Charset.defaultCharset())
val content = call.response.use {
it.content.toByteArray().toString(Charsets.UTF_8)
}
val responseObject = jsonFormatter.parse(
Response.serializer(request.resultSerializer()),
content

View File

@ -1,32 +0,0 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor
import com.github.insanusmokrassar.TelegramBotAPI.bot.settings.ProxySettings
import io.ktor.http.HttpHeaders
import okhttp3.Credentials
import okhttp3.OkHttpClient
import java.net.InetSocketAddress
import java.net.Proxy
fun OkHttpClient.Builder.useWith(proxySettings: ProxySettings) {
proxy(
Proxy(
Proxy.Type.SOCKS,
InetSocketAddress(
proxySettings.host,
proxySettings.port
)
)
)
proxySettings.password ?.let {
password ->
proxyAuthenticator {
_, response ->
response.request().newBuilder().apply {
addHeader(
HttpHeaders.ProxyAuthorization,
Credentials.basic(proxySettings.username ?: "", password)
)
}.build()
}
}
}

View File

@ -0,0 +1,41 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorCallFactory
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
import io.ktor.client.call.call
import io.ktor.client.request.accept
import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
abstract class AbstractRequestCallFactory : KtorCallFactory {
private val methodsCache: MutableMap<String, String> = mutableMapOf()
override suspend fun <T : Any> prepareCall(
client: HttpClient,
baseUrl: String,
request: Request<T>
): HttpClientCall? {
val preparedBody = prepareCallBody(client, baseUrl, request) ?: return null
return client.call {
url(
methodsCache[request.method()] ?: "$baseUrl/${request.method()}".also {
methodsCache[request.method()] = it
}
)
method = HttpMethod.Post
accept(ContentType.Application.Json)
body = preparedBody
build()
}
}
protected abstract fun <T : Any> prepareCallBody(
client: HttpClient,
baseUrl: String,
request: Request<T>
): Any?
}

View File

@ -1,48 +1,37 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorCallFactory
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.*
import com.github.insanusmokrassar.TelegramBotAPI.utils.mapWithCommonValues
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
import io.ktor.client.call.call
import io.ktor.client.request.accept
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.url
import io.ktor.http.*
class MultipartRequestCallFactory : KtorCallFactory {
override suspend fun <T: Any> prepareCall(
class MultipartRequestCallFactory : AbstractRequestCallFactory() {
override fun <T : Any> prepareCallBody(
client: HttpClient,
baseUrl: String,
request: Request<T>
): HttpClientCall? = (request as? MultipartRequest) ?.let {
castedRequest ->
client.call {
url("$baseUrl/${castedRequest.method()}")
method = HttpMethod.Post
accept(ContentType.Application.Json)
body = MultiPartFormDataContent(
formData {
val params = castedRequest.paramsJson.mapWithCommonValues()
for ((key, value) in castedRequest.mediaMap + params) {
when (value) {
is MultipartFile -> append(
key,
value.file.asInput(),
Headers.build {
append(HttpHeaders.ContentType, value.mimeType)
append(HttpHeaders.ContentDisposition, "filename=${value.fileId}")
}
)
is FileId -> append(key, value.fileId)
else -> append(key, value.toString())
}
): Any? = (request as? MultipartRequest) ?.let { castedRequest ->
MultiPartFormDataContent(
formData {
val params = castedRequest.paramsJson.mapWithCommonValues()
for ((key, value) in castedRequest.mediaMap + params) {
when (value) {
is MultipartFile -> append(
key,
value.file.asInput(),
Headers.build {
append(HttpHeaders.ContentType, value.mimeType)
append(HttpHeaders.ContentDisposition, "filename=${value.fileId}")
}
)
is FileId -> append(key, value.fileId)
else -> append(key, value.toString())
}
}
)
build()
}
}
)
}
}

View File

@ -1,36 +1,23 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorCallFactory
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.*
import com.github.insanusmokrassar.TelegramBotAPI.utils.toJsonWithoutNulls
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
import io.ktor.client.call.call
import io.ktor.client.request.accept
import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
import io.ktor.http.content.TextContent
class SimpleRequestCallFactory : KtorCallFactory {
override suspend fun <T: Any> prepareCall(
class SimpleRequestCallFactory : AbstractRequestCallFactory() {
override fun <T : Any> prepareCallBody(
client: HttpClient,
baseUrl: String,
request: Request<T>
): HttpClientCall? = (request as? SimpleRequest<T>) ?.let {
castedRequest ->
client.call {
url("$baseUrl/${castedRequest.method()}")
method = HttpMethod.Post
accept(ContentType.Application.Json)
): Any? = (request as? SimpleRequest<T>) ?.let { _ ->
val content = request.toJsonWithoutNulls(SimpleRequestSerializer).toString()
val content = request.toJsonWithoutNulls(SimpleRequestSerializer).toString()
body = TextContent(
content,
ContentType.Application.Json
)
build()
}
TextContent(
content,
ContentType.Application.Json
)
}
}

View File

@ -1,18 +0,0 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.useWith
import com.github.insanusmokrassar.TelegramBotAPI.bot.settings.ProxySettings
import okhttp3.OkHttpClient
@Deprecated(
"Replaced in settings package",
ReplaceWith("ProxySettings", "com.github.insanusmokrassar.TelegramBotAPI.bot.settings.ProxySettings")
)
typealias ProxySettings = com.github.insanusmokrassar.TelegramBotAPI.bot.settings.ProxySettings
@Deprecated(
"Replaced in Ktor package",
ReplaceWith("useWith", "com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.useWith")
)
fun OkHttpClient.Builder.useWith(proxySettings: ProxySettings) = useWith(proxySettings)

View File

@ -64,6 +64,7 @@ const val inlineMessageIdField = "inline_message_id"
const val callbackDataField = "callback_data"
const val callbackQueryIdField = "callback_query_id"
const val inlineQueryIdField = "inline_query_id"
const val inlineKeyboardField = "inline_keyboard"
const val showAlertField = "show_alert"
const val cachedTimeField = "cached_time"
const val foursquareIdField = "foursquare_id"

View File

@ -1,5 +1,10 @@
package com.github.insanusmokrassar.TelegramBotAPI.types.buttons.InlineKeyboardButtons
import kotlinx.serialization.*
@Serializable(InlineKeyboardButtonSerializer::class)
interface InlineKeyboardButton {
val text: String
}
}
object InlineKeyboardButtonSerializer : KSerializer<InlineKeyboardButton> by ContextSerializer(InlineKeyboardButton::class)

View File

@ -1,16 +1,12 @@
package com.github.insanusmokrassar.TelegramBotAPI.types.buttons
import com.github.insanusmokrassar.TelegramBotAPI.types.buttons.InlineKeyboardButtons.InlineKeyboardButton
import com.github.insanusmokrassar.TelegramBotAPI.types.inlineKeyboardField
import kotlinx.serialization.*
import kotlinx.serialization.internal.ArrayListSerializer
@Serializable
data class InlineKeyboardMarkup(
@SerialName("inline_keyboard")
@Serializable(with = KeyboardSerializer::class)
@SerialName(inlineKeyboardField)
val keyboard: Matrix<InlineKeyboardButton>
) : KeyboardMarkup
object KeyboardSerializer : KSerializer<Matrix<InlineKeyboardButton>> by ArrayListSerializer(
ArrayListSerializer(ContextSerializer(InlineKeyboardButton::class))
)

View File

@ -12,21 +12,20 @@ data class AnonymousForwardedMessage(
val senderName: String
) : ForwardedMessage()
sealed class PublicForwardedMessage : ForwardedMessage() {
abstract val messageId: MessageIdentifier
abstract val from: User?
}
data class CommonForwardedMessage(
override val messageId: MessageIdentifier,
data class UserForwardedMessage(
override val dateOfOriginal: TelegramDate,
override val from: User
) : PublicForwardedMessage()
val from: User
) : ForwardedMessage()
@Deprecated(
"Renamed according to correct meaning",
ReplaceWith("UserForwardedMessage", "com.github.insanusmokrassar.TelegramBotAPI.types.message.UserForwardedMessage")
)
typealias CommonForwardedMessage = UserForwardedMessage
data class ForwardedFromChannelMessage(
override val messageId: MessageIdentifier,
override val dateOfOriginal: TelegramDate,
override val from: User?,
val messageId: MessageIdentifier,
val channelChat: Chat,
val signature: String? = null
) : PublicForwardedMessage()
) : ForwardedMessage()

View File

@ -134,26 +134,22 @@ data class RawMessage(
@Transient
private val forwarded: ForwardedMessage? by lazy {
forward_date ?: return@lazy null // According to the documentation, now any forwarded message contains this field
forward_from_message_id ?.let {
forward_from ?: throw IllegalStateException("For common forwarded messages author of original message declared as set up required")
forward_from_chat ?.let {
ForwardedFromChannelMessage(
forward_from_message_id,
forward_date,
forward_from,
forward_from_chat.extractChat(),
forward_signature
)
} ?: CommonForwardedMessage(
forward_from_message_id,
forward_date,
forward_from
)
} ?: forward_sender_name ?.let {
AnonymousForwardedMessage(
when {
forward_sender_name != null -> AnonymousForwardedMessage(
forward_date,
forward_sender_name
)
forward_from_chat != null -> ForwardedFromChannelMessage(
forward_date,
forward_from_message_id ?: throw IllegalStateException("Channel forwarded message must contain message id, but was not"),
forward_from_chat.extractChat(),
forward_signature
)
forward_from != null -> UserForwardedMessage(
forward_date,
forward_from
)
else -> null
}
}

View File

@ -16,7 +16,6 @@ import io.ktor.response.respond
import io.ktor.routing.post
import io.ktor.routing.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
@ -36,12 +35,12 @@ import java.util.concurrent.TimeUnit
suspend fun RequestsExecutor.setWebhook(
url: String,
port: Int,
engineFactory: ApplicationEngineFactory<*, *>,
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<Update>
): Job {
val executeDeferred = certificate ?.let {
@ -69,20 +68,17 @@ suspend fun RequestsExecutor.setWebhook(
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")
}
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(
@ -140,19 +136,19 @@ suspend fun RequestsExecutor.setWebhook(
url: String,
port: Int,
filter: UpdatesFilter,
engineFactory: ApplicationEngineFactory<*, *>,
certificate: InputFile? = null,
privateKeyConfig: WebhookPrivateKeyConfig? = null,
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
maxAllowedConnections: Int? = null,
engineFactory: ApplicationEngineFactory<*, *> = Netty
maxAllowedConnections: Int? = null
): Job = setWebhook(
url,
port,
engineFactory,
certificate,
privateKeyConfig,
scope,
filter.allowedUpdates,
maxAllowedConnections,
engineFactory,
filter.asUpdateReceiver
)