mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI.git
synced 2025-12-02 12:25:42 +00:00
artifacts names has been changed
This commit is contained in:
295
tgbotapi.extensions.utils/README.md
Normal file
295
tgbotapi.extensions.utils/README.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# TelegramBotAPI Util Extensions
|
||||
|
||||
|
||||
- [TelegramBotAPI Util Extensions](#telegrambotapi-util-extensions)
|
||||
* [What is it?](#what-is-it)
|
||||
* [How to implement library?](#how-to-implement-library)
|
||||
+ [Maven](#maven)
|
||||
+ [Gradle](#gradle)
|
||||
* [How to use?](#how-to-use)
|
||||
+ [Updates](#updates)
|
||||
- [Long polling](#long-polling)
|
||||
- [WebHooks (currently JVM-only)](#webhooks--currently-jvm-only)
|
||||
+ [Filters](#filters)
|
||||
- [Sent messages](#sent-messages)
|
||||
* [Common messages](#common-messages)
|
||||
* [Chat actions](#chat-actions)
|
||||
+ [Shortcuts](#shortcuts)
|
||||
- [ScheduledCloseInfo](#scheduledcloseinfo)
|
||||
|
||||
<small><i><a href='http://ecotrust-canada.github.io/markdown-toc/'>Table of contents generated with markdown-toc</a></i></small>
|
||||
|
||||
[ ](https://bintray.com/insanusmokrassar/TelegramBotAPI/tgbotapi.extensions.utils/_latestVersion)
|
||||
[](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi.extensions.utils)
|
||||
|
||||
## What is it?
|
||||
|
||||
It is wrapper library for [tgbotapi.core](../tgbotapi.core/README.md). Currently, this library contains some usefull filters for commands, updates types and different others.
|
||||
|
||||
## How to implement library?
|
||||
|
||||
Common ways to implement this library are presented here. In some cases it will require additional steps
|
||||
like inserting of additional libraries (like `kotlin stdlib`). In the examples will be used variable
|
||||
`telegrambotapi-extensions-utils_version`, which must be set up by developer. Available versions are presented on
|
||||
[bintray](https://bintray.com/insanusmokrassar/TelegramBotAPI/tgbotapi.extensions.utils), next version is last published:
|
||||
|
||||
[ ](https://bintray.com/insanusmokrassar/TelegramBotAPI/tgbotapi.extensions.utils/_latestVersion)
|
||||
|
||||
### Maven
|
||||
|
||||
Dependency config presented here:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>dev.inmo</groupId>
|
||||
<artifactId>tgbotapi.extensions.utils</artifactId>
|
||||
<version>${telegrambotapi-extensions-utils_version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Gradle
|
||||
|
||||
To use last versions you will need to add one line in repositories block of your `build.gradle`:
|
||||
|
||||
`jcenter()` or `mavenCentral()`
|
||||
|
||||
And add next line to your dependencies block:
|
||||
|
||||
```groovy
|
||||
implementation "dev.inmo:tgbotapi.extensions.utils:$telegrambotapi-extensions-utils_version"
|
||||
```
|
||||
|
||||
or for old gradle:
|
||||
|
||||
```groovy
|
||||
compile "dev.inmo:tgbotapi.extensions.utils:$telegrambotapi-extensions-utils_version"
|
||||
```
|
||||
|
||||
## How to use?
|
||||
|
||||
Here will be presented several examples of usage. In all cases it is expected that you have created your bot and filter:
|
||||
|
||||
```kotlin
|
||||
val bot: RequestsExecutor = KtorRequestsExecutor(
|
||||
TelegramAPIUrlsKeeper(BOT_TOKEN)
|
||||
)
|
||||
val filter = FlowsUpdatesFilter(64)
|
||||
```
|
||||
|
||||
Alternative way to use the things below:
|
||||
|
||||
```kotlin
|
||||
val filter = bot.startGettingFlowsUpdatesByLongPolling(
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
) {
|
||||
// place code from examples here with replacing of `filter` by `this`
|
||||
}
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
As mentioned in [Telegram Bot API reference](https://core.telegram.org/bots/api#getting-updates), there are two ways for
|
||||
updates retrieving:
|
||||
|
||||
* Webhooks
|
||||
* Long Polling
|
||||
|
||||
Both of them you could use in your project using [tgbotapi.core](../tgbotapi.core/README.md), but here there are
|
||||
several useful extensions for both of them.
|
||||
|
||||
Anyway, in both of ways it will be useful to know that it is possible to create `UpdateReceiver` object using function
|
||||
`flowsUpdatesFilter`:
|
||||
|
||||
```kotlin
|
||||
val internalChannelsSizes = 128
|
||||
flowsUpdatesFilter(internalChannelsSizes/* default is 64 */) {
|
||||
textMessages().onEach {
|
||||
println("I have received text message: ${it.content}")
|
||||
}.launchIn(someCoroutineScope)
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
#### Long polling
|
||||
|
||||
The most simple way is Long Polling and one of the usages was mentioned above:
|
||||
|
||||
```kotlin
|
||||
val filter = bot.startGettingFlowsUpdatesByLongPolling(
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
) {
|
||||
// place code from examples here with replacing of `filter` by `this`
|
||||
}
|
||||
```
|
||||
|
||||
Extension `startGettingFlowsUpdatesByLongPolling` was used in this example, but there are a lot of variations of
|
||||
`startGettingOfUpdatesByLongPolling` and others for getting the same result. Usually, it is supposed that you already
|
||||
have created `filter` object (or something like this) and will pass it into extension:
|
||||
|
||||
```kotlin
|
||||
val filter = FlowsUpdatesFilter(64)
|
||||
bot.startGettingOfUpdatesByLongPolling(
|
||||
filter
|
||||
)
|
||||
```
|
||||
|
||||
But also there are extensions which allow to pass lambdas directly:
|
||||
|
||||
```kotlin
|
||||
bot.startGettingOfUpdatesByLongPolling(
|
||||
{
|
||||
println("Received message update: $it")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Anyway, it is strictly recommended to pass your `CoroutineScope` object to this method at least for more comfortable
|
||||
management of updates.
|
||||
|
||||
#### WebHooks (currently JVM-only)
|
||||
|
||||
For webhooks there are less number of functions and extensions than for Long Polling (but it is still fully automated):
|
||||
|
||||
```kotlin
|
||||
startListenWebhooks(
|
||||
8081,
|
||||
CIO // require to implement this engine dependency
|
||||
) {
|
||||
// here will be all updates one by one in $it
|
||||
}
|
||||
```
|
||||
|
||||
Besides, there are two additional opportunities:
|
||||
|
||||
* Extension `Route#includeWebhookHandlingInRoute`, which allow you to include webhook processing inside your ktor
|
||||
application without creating of new one server (as it is happening in `startListenWebhooks`)
|
||||
* Also, you can use `Route#includeWebhookHandlingInRouteWithFlows` to use it like `flowUpdatesFilter` fun, but apply
|
||||
`FlowsUpdatesFilter` to the block
|
||||
* Extension `RequestsExecutor#setWebhookInfoAndStartListenWebhooks`. It is allow to set up full server (in fact, with
|
||||
`startListenWebhooks`), but also send `SetWebhook` request before and check that it was successful
|
||||
|
||||
### Filters
|
||||
|
||||
There are several filters for flows.
|
||||
|
||||
#### Updates
|
||||
|
||||
In the next table it is supposed that you are using some `Flow` with type from `Base type of update` and apply
|
||||
extension `Extension` and will get `Flow` with type from `Result type of update` column.
|
||||
|
||||
| Base type of update | Extension | Result type of update |
|
||||
| ------------------- | --------- | --------------------- |
|
||||
| `Update` | `onlyBaseMessageUpdates` | `BaseMessageUpdate` |
|
||||
| | | |
|
||||
| `BaseMessageUpdate` | `onlySentMessageUpdates` | `BaseSentMessageUpdate` |
|
||||
| `BaseMessageUpdate` | `onlyEditMessageUpdates` | `BaseEditMessageUpdate` |
|
||||
| `BaseMessageUpdate` | `onlyMediaGroupsUpdates` | `MediaGroupUpdate` |
|
||||
| | | |
|
||||
| `MediaGroupUpdate` | `onlySentMediaGroupUpdates` | `SentMediaGroupUpdate` |
|
||||
| `MediaGroupUpdate` | `onlyEditMediaGroupUpdates` | `EditMediaGroupUpdate` |
|
||||
|
||||
All of these extensions was made for more simple work with the others:
|
||||
|
||||
```kotlin
|
||||
val flow: Flow<BaseMessageUpdate> = ...; // here we are getting flow from somewhere,
|
||||
// for example, FlowsUpdatesFilter#messageFlow
|
||||
flow.onlySentMessageUpdates().filterExactCommands(Regex("start"))
|
||||
```
|
||||
|
||||
Here we have used filter `filterExactCommands` which will pass only `ContentMessage` with only one command `start`
|
||||
|
||||
#### Sent messages
|
||||
|
||||
All sent messages can be filtered for three types:
|
||||
|
||||
| Type | Description | Flow extension |
|
||||
|:---- |:----------- |:-------------- |
|
||||
| Common messages | Simple messages with text, media, location, etc. | `asContentMessagesFlow` |
|
||||
| Chat actions | New chat member, rename of chat, etc. | `asChatEventsFlow` |
|
||||
| Unknown events | Any other messages, that contain unsupported data | `asUnknownMessagesFlow` |
|
||||
|
||||
##### Common messages
|
||||
|
||||
Unfortunately, due to the erasing of generic types, when you are using `asContentMessagesFlow` you will retrieve
|
||||
data with type `ContentMessage<*>`. For correct filtering of content type for retrieved objects, was created special
|
||||
filters:
|
||||
|
||||
| Content type | Result type | Flow extension |
|
||||
|:---- |:----------- |:-------------- |
|
||||
| Animation | `ContentMessage<AnimationContent>`| `onlyAnimationContentMessages` |
|
||||
| Audio | `ContentMessage<AudioContent>` | `onlyAudioContentMessages` |
|
||||
| Contact | `ContentMessage<ContactContent>` | `onlyContactContentMessages` |
|
||||
| Dice | `ContentMessage<DiceContent>` | `onlyDiceContentMessages` |
|
||||
| Document | `ContentMessage<DocumentContent>` | `onlyDocumentContentMessages` |
|
||||
| Game | `ContentMessage<GameContent>` | `onlyGameContentMessages` |
|
||||
| Invoice | `ContentMessage<InvoiceContent>` | `onlyInvoiceContentMessages` |
|
||||
| Location | `ContentMessage<LocationContent>` | `onlyLocationContentMessages` |
|
||||
| Photo | `ContentMessage<PhotoContent>` | `onlyPhotoContentMessages` |
|
||||
| Poll | `ContentMessage<PollContent>` | `onlyPollContentMessages` |
|
||||
| Sticker | `ContentMessage<StickerContent>` | `onlyStickerContentMessages` |
|
||||
| Text | `ContentMessage<TextContent>` | `onlyTextContentMessages` |
|
||||
| Venue | `ContentMessage<VenueContent>` | `onlyVenueContentMessages` |
|
||||
| Video | `ContentMessage<VideoContent>` | `onlyVideoContentMessages` |
|
||||
| VideoNote | `ContentMessage<VideoNoteContent>` | `onlyVideoNoteContentMessages` |
|
||||
| Voice | `ContentMessage<VoiceContent>` | `onlyVoiceContentMessages` |
|
||||
|
||||
For example, if you wish to get only photo messages from private chats of groups, you should call next code:
|
||||
|
||||
```kotlin
|
||||
filter.messageFlow.asContentMessagesFlow().onlyPhotoContentMessages().onEach {
|
||||
println(it.content)
|
||||
}.launchIn(
|
||||
CoroutineScope(Dispatchers.Default)
|
||||
)
|
||||
```
|
||||
|
||||
##### Chat actions
|
||||
|
||||
Chat actions can be divided for three types of events source:
|
||||
|
||||
| Type | Flow extension |
|
||||
|:---- |:-------------- |
|
||||
| Channel events | `onlyChannelEvents` |
|
||||
| Group events | `onlyGroupEvents` |
|
||||
| Supergroup events | `onlySupergroupEvents` |
|
||||
|
||||
According to this table, if you want to add filtering by supergroup events, you will use code like this:
|
||||
|
||||
```kotlin
|
||||
filter.messageFlow.asChatEventsFlow().onlySupergroupEvents().onEach {
|
||||
println(it.chatEvent)
|
||||
}.launchIn(
|
||||
CoroutineScope(Dispatchers.Default)
|
||||
)
|
||||
```
|
||||
|
||||
### Shortcuts
|
||||
|
||||
With shortcuts you are able to use simple factories for several things.
|
||||
|
||||
#### ScheduledCloseInfo
|
||||
|
||||
In case if you are creating some poll, you able to use next shortcuts.
|
||||
|
||||
Next sample will use info with closing at the 10 seconds after now:
|
||||
|
||||
```kotlin
|
||||
closePollExactAt(DateTime.now() + TimeSpan(10000.0))
|
||||
```
|
||||
|
||||
In this example we will do the same, but in another way:
|
||||
|
||||
```kotlin
|
||||
closePollExactAfter(10)
|
||||
```
|
||||
|
||||
Here we have passed `10` seconds and will get the same result object.
|
||||
|
||||
In opposite to previous shortcuts, the next one will create `approximate` closing schedule:
|
||||
|
||||
```kotlin
|
||||
closePollAfter(10)
|
||||
```
|
||||
|
||||
The main difference here is that the last one will be closed after 10 seconds since the sending. With first samples
|
||||
will be created **exact** time for closing of poll
|
||||
51
tgbotapi.extensions.utils/build.gradle
Normal file
51
tgbotapi.extensions.utils/build.gradle
Normal file
@@ -0,0 +1,51 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
}
|
||||
|
||||
project.version = "$library_version"
|
||||
project.group = "$library_group"
|
||||
|
||||
apply from: "publish.gradle"
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(BOTH) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation kotlin('stdlib')
|
||||
if ((project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true") {
|
||||
api "${project.group}:tgbotapi.core:$library_version"
|
||||
} else {
|
||||
api project(":tgbotapi.core")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
tgbotapi.extensions.utils/maven.publish.gradle
Normal file
53
tgbotapi.extensions.utils/maven.publish.gradle
Normal file
@@ -0,0 +1,53 @@
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
project.publishing.publications.all {
|
||||
// rename artifacts
|
||||
groupId "${project.group}"
|
||||
if (it.name.contains('kotlinMultiplatform')) {
|
||||
artifactId = "${project.name}"
|
||||
} else {
|
||||
artifactId = "${project.name}-$name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications.all {
|
||||
artifact javadocsJar
|
||||
|
||||
pom {
|
||||
description = "Util extensions for more useful work with updates and other things"
|
||||
name = "Telegram Bot API Utility Extensions"
|
||||
url = "https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-extensions-utils"
|
||||
|
||||
scm {
|
||||
developerConnection = "scm:git:[fetch=]https://github.com/insanusmokrassar/TelegramBotAPI.git[push=]https://github.com/insanusmokrassar/TelegramBotAPI.git"
|
||||
url = "https://github.com/insanusmokrassar/TelegramBotAPI.git"
|
||||
}
|
||||
|
||||
developers {
|
||||
|
||||
developer {
|
||||
id = "InsanusMokrassar"
|
||||
name = "Ovsiannikov Aleksei"
|
||||
email = "ovsyannikov.alexey95@gmail.com"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
licenses {
|
||||
|
||||
license {
|
||||
name = "Apache Software License 2.0"
|
||||
url = "https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
tgbotapi.extensions.utils/mpp_publish_template.kpsb
Normal file
1
tgbotapi.extensions.utils/mpp_publish_template.kpsb
Normal file
@@ -0,0 +1 @@
|
||||
{"bintrayConfig":{"repo":"TelegramBotAPI","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI","autoPublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API Utility Extensions","description":"Util extensions for more useful work with updates and other things","url":"https://insanusmokrassar.github.io/TelegramBotAPI/TelegramBotAPI-extensions-utils","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"Multiplatform"}
|
||||
58
tgbotapi.extensions.utils/publish.gradle
Normal file
58
tgbotapi.extensions.utils/publish.gradle
Normal file
@@ -0,0 +1,58 @@
|
||||
apply plugin: 'com.jfrog.bintray'
|
||||
|
||||
apply from: "maven.publish.gradle"
|
||||
|
||||
bintray {
|
||||
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
|
||||
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
|
||||
filesSpec {
|
||||
from "${buildDir}/publications/"
|
||||
eachFile {
|
||||
String directorySubname = it.getFile().parentFile.name
|
||||
if (it.getName() == "module.json") {
|
||||
if (directorySubname == "kotlinMultiplatform") {
|
||||
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.module")
|
||||
} else {
|
||||
it.setPath("${project.name}-${directorySubname}/${project.version}/${project.name}-${directorySubname}-${project.version}.module")
|
||||
}
|
||||
} else {
|
||||
if (directorySubname == "kotlinMultiplatform" && it.getName() == "pom-default.xml") {
|
||||
it.setPath("${project.name}/${project.version}/${project.name}-${project.version}.pom")
|
||||
} else {
|
||||
it.exclude()
|
||||
}
|
||||
}
|
||||
}
|
||||
into "${project.group}".replace(".", "/")
|
||||
}
|
||||
|
||||
publish = true
|
||||
|
||||
pkg {
|
||||
repo = "TelegramBotAPI"
|
||||
name = "${project.name}"
|
||||
vcsUrl = "https://github.com/InsanusMokrassar/TelegramBotAPI"
|
||||
licenses = ["Apache-2.0"]
|
||||
version {
|
||||
name = "${project.version}"
|
||||
released = new Date()
|
||||
vcsTag = "${project.version}"
|
||||
gpg {
|
||||
sign = true
|
||||
passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bintrayUpload.doFirst {
|
||||
publications = publishing.publications.collect {
|
||||
if (it.name.contains('kotlinMultiplatform')) {
|
||||
null
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
} - null
|
||||
}
|
||||
|
||||
bintrayUpload.dependsOn publishToMavenLocal
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils
|
||||
|
||||
import dev.inmo.tgbotapi.types.CallbackQuery.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
fun <T : CallbackQuery> Flow<T>.onlyMessageDataCallbackQueries() = mapNotNull {
|
||||
it as? MessageDataCallbackQuery
|
||||
}
|
||||
fun <T : CallbackQuery> Flow<T>.onlyInlineMessageIdDataCallbackQueries() = mapNotNull {
|
||||
it as? InlineMessageIdDataCallbackQuery
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils
|
||||
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.*
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.PossiblySentViaBotCommonMessage
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Simple factory to convert [ContentMessage] to a [CommonMessage]
|
||||
*/
|
||||
fun <C: MessageContent, T : ContentMessage<C>> Flow<T>.onlyCommonMessages() = filterIsInstance<CommonMessage<C>>()
|
||||
|
||||
/**
|
||||
* Shortcut for [onlyCommonMessages]
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <C: MessageContent, T : ContentMessage<C>> Flow<T>.commonMessages() = onlyCommonMessages()
|
||||
|
||||
/**
|
||||
* Filter the messages and checking that incoming [CommonMessage] is [PossiblySentViaBotCommonMessage] and its
|
||||
* [PossiblySentViaBotCommonMessage.senderBot] is not null
|
||||
*/
|
||||
fun <MC : MessageContent, M : ContentMessage<MC>> Flow<M>.onlySentViaBot() = mapNotNull {
|
||||
if (it is PossiblySentViaBot && it.senderBot != null) {
|
||||
it
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the messages and checking that incoming [CommonMessage] not is [PossiblySentViaBotCommonMessage] or its
|
||||
* [PossiblySentViaBotCommonMessage.senderBot] is null
|
||||
*/
|
||||
fun <MC : MessageContent, M : ContentMessage<MC>> Flow<M>.withoutSentViaBot() = filter {
|
||||
it !is PossiblySentViaBot || it.senderBot == null
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils
|
||||
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.*
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent
|
||||
import dev.inmo.tgbotapi.types.message.content.media.*
|
||||
import dev.inmo.tgbotapi.types.message.payments.InvoiceContent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun <T : MessageContent> Flow<ContentMessage<*>>.withContentType(contentType: KClass<T>) = mapNotNull {
|
||||
if (contentType.isInstance(it.content)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as ContentMessage<T>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Flow<ContentMessage<*>>.onlyAnimationContentMessages() = withContentType(AnimationContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyAudioContentMessages() = withContentType(AudioContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyContactContentMessages() = withContentType(ContactContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyDiceContentMessages() = withContentType(DiceContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyDocumentContentMessages() = withContentType(DocumentContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyGameContentMessages() = withContentType(GameContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyInvoiceContentMessages() = withContentType(InvoiceContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyLocationContentMessages() = withContentType(LocationContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyPhotoContentMessages() = withContentType(PhotoContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyPollContentMessages() = withContentType(PollContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyStickerContentMessages() = withContentType(StickerContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyTextContentMessages() = withContentType(TextContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyVenueContentMessages() = withContentType(VenueContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyVideoContentMessages() = withContentType(VideoContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyVideoNoteContentMessages() = withContentType(VideoNoteContent::class)
|
||||
fun Flow<ContentMessage<*>>.onlyVoiceContentMessages() = withContentType(VoiceContent::class)
|
||||
@@ -0,0 +1,39 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Analog of [merge] function for [Flow]s. The difference is in the usage of [BroadcastChannel] in this case
|
||||
*/
|
||||
fun <T> aggregateFlows(
|
||||
withScope: CoroutineScope,
|
||||
vararg flows: Flow<T>,
|
||||
internalBufferSize: Int = Channel.BUFFERED
|
||||
): Flow<T> {
|
||||
val bc = BroadcastChannel<T>(internalBufferSize)
|
||||
flows.forEach {
|
||||
it.onEach {
|
||||
safely { bc.send(it) }
|
||||
}.launchIn(withScope)
|
||||
}
|
||||
return bc.asFlow()
|
||||
}
|
||||
|
||||
fun <T> Flow<Iterable<T>>.flatMap(): Flow<T> = flow {
|
||||
collect {
|
||||
it.forEach {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T, R> Flow<T>.flatMap(mapper: (T) -> Iterable<R>): Flow<R> = flow {
|
||||
collect {
|
||||
mapper(it).forEach {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
internal val nonstrictJsonFormat = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
allowSpecialFloatingPointValues = true
|
||||
useArrayPolymorphism = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils
|
||||
|
||||
import dev.inmo.tgbotapi.utils.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Shortcut for [handleSafely]. It was created for more comfortable way of handling different things
|
||||
*/
|
||||
@PreviewFeature
|
||||
suspend inline fun <T> safely(
|
||||
noinline onException: ExceptionHandler<T> = { throw it },
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): T = handleSafely(
|
||||
onException,
|
||||
block
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.chat_events
|
||||
|
||||
import dev.inmo.tgbotapi.types.message.*
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ChatEventMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
fun <T : ChatEventMessage> Flow<ChatEventMessage>.divideBySource(contentType: KClass<T>) = mapNotNull {
|
||||
if (contentType.isInstance(it)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as T
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Flow<ChatEventMessage>.onlyChannelEvents() = divideBySource(ChannelEventMessage::class)
|
||||
fun Flow<ChatEventMessage>.onlyGroupEvents() = divideBySource(CommonGroupEventMessage::class)
|
||||
fun Flow<ChatEventMessage>.onlySupergroupEvents() = divideBySource(CommonSupergroupEventMessage::class)
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.extensions
|
||||
|
||||
import dev.inmo.tgbotapi.types.files.PathedFile
|
||||
import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
|
||||
suspend fun HttpClient.loadFile(
|
||||
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
|
||||
filePath: String
|
||||
) = get<ByteArray>("${telegramAPIUrlsKeeper.fileBaseUrl}/$filePath")
|
||||
|
||||
suspend fun HttpClient.loadFile(
|
||||
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
|
||||
pathedFile: PathedFile
|
||||
) = loadFile(telegramAPIUrlsKeeper, pathedFile.filePath)
|
||||
|
||||
suspend fun PathedFile.download(
|
||||
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
|
||||
client: HttpClient = HttpClient()
|
||||
) = client.loadFile(telegramAPIUrlsKeeper, this)
|
||||
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.extensions
|
||||
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.SentMediaGroupUpdate
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.BaseSentMessageUpdate
|
||||
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.merge
|
||||
|
||||
val FlowsUpdatesFilter.allSentMessagesFlow: Flow<BaseSentMessageUpdate>
|
||||
get() = merge(
|
||||
messageFlow,
|
||||
channelPostFlow
|
||||
)
|
||||
|
||||
val FlowsUpdatesFilter.allSentMediaGroupsFlow: Flow<SentMediaGroupUpdate>
|
||||
get() = merge(
|
||||
messageMediaGroupFlow,
|
||||
channelPostMediaGroupFlow
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.extensions.venue
|
||||
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.venue.Venue
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
val Venue.foursquare: Foursquare?
|
||||
get() = foursquareId ?.let {
|
||||
Foursquare(it, foursquareType)
|
||||
}
|
||||
|
||||
fun Venue(
|
||||
location: Location,
|
||||
title: String,
|
||||
address: String,
|
||||
foursquare: Foursquare
|
||||
) = Venue(location, title, address, foursquare.id, foursquare.type)
|
||||
|
||||
@Serializable
|
||||
data class Foursquare(
|
||||
@SerialName(foursquareIdField)
|
||||
val id: FoursquareId,
|
||||
@SerialName(foursquareTypeField)
|
||||
val type: FoursquareType? = null
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.formatting
|
||||
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.ParseMode.*
|
||||
import dev.inmo.tgbotapi.types.chat.abstracts.PrivateChat
|
||||
import dev.inmo.tgbotapi.types.chat.abstracts.UsernameChat
|
||||
import dev.inmo.tgbotapi.types.chat.abstracts.extended.ExtendedChat
|
||||
|
||||
private const val internalLinkBeginning = "https://t.me"
|
||||
|
||||
fun makeLinkToMessage(
|
||||
username: String,
|
||||
messageId: MessageIdentifier
|
||||
): String = "$internalLinkBeginning/$username/$messageId"
|
||||
fun makeLinkToMessage(
|
||||
username: Username,
|
||||
messageId: MessageIdentifier
|
||||
): String = makeLinkToMessage(username.username, messageId)
|
||||
fun makeLinkToMessage(
|
||||
chat: UsernameChat,
|
||||
messageId: MessageIdentifier
|
||||
): String? = chat.username ?.let { makeLinkToMessage(it, messageId) }
|
||||
|
||||
private val linkIdRedundantPartRegex = Regex("^-100")
|
||||
private val usernameBeginSymbolRegex = Regex("^@")
|
||||
|
||||
fun makeLinkToMessage(
|
||||
chat: ExtendedChat,
|
||||
messageId: MessageIdentifier
|
||||
): String? {
|
||||
return when {
|
||||
chat is UsernameChat && chat.username != null -> {
|
||||
"$internalLinkBeginning/${chat.username ?.username ?.replace(
|
||||
usernameBeginSymbolRegex, "")}/$messageId"
|
||||
}
|
||||
chat !is PrivateChat -> chat.id.chatId.toString().replace(
|
||||
linkIdRedundantPartRegex,
|
||||
""
|
||||
).let { bareId ->
|
||||
"$internalLinkBeginning/c/$bareId/$messageId"
|
||||
}
|
||||
else -> return null
|
||||
}
|
||||
}
|
||||
|
||||
private const val stickerSetAddingLinkPrefix = "$internalLinkBeginning/addstickers"
|
||||
|
||||
/**
|
||||
* Create a link for adding of sticker set with name [stickerSetName]. Was added thanks to user Djaler and based on
|
||||
* https://github.com/Djaler/evil-bot/blob/master/src/main/kotlin/com/github/djaler/evilbot/utils/StickerUtils.kt#L6-L8
|
||||
*
|
||||
* @see [makeLinkToAddStickerSetInMarkdownV2]
|
||||
* @see [makeLinkToAddStickerSetInMarkdown]
|
||||
* @see [makeLinkToAddStickerSetInHtml]
|
||||
*/
|
||||
fun makeLinkToAddStickerSet(
|
||||
stickerSetName: StickerSetName,
|
||||
parseMode: ParseMode
|
||||
) = (stickerSetName to "$stickerSetAddingLinkPrefix/$stickerSetName").link(
|
||||
parseMode
|
||||
)
|
||||
|
||||
/**
|
||||
* @return Link for adding of sticker set with name [stickerSetName] with formatting for [MarkdownV2]
|
||||
*/
|
||||
fun makeLinkToAddStickerSetInMarkdownV2(stickerSetName: StickerSetName) =
|
||||
makeLinkToAddStickerSet(
|
||||
stickerSetName,
|
||||
MarkdownV2
|
||||
)
|
||||
/**
|
||||
* @return Link for adding of sticker set with name [stickerSetName] with formatting for [Markdown]
|
||||
*/
|
||||
fun makeLinkToAddStickerSetInMarkdown(stickerSetName: StickerSetName) =
|
||||
makeLinkToAddStickerSet(
|
||||
stickerSetName,
|
||||
Markdown
|
||||
)
|
||||
/**
|
||||
* @return Link for adding of sticker set with name [stickerSetName] with formatting for [HTML]
|
||||
*/
|
||||
fun makeLinkToAddStickerSetInHtml(stickerSetName: StickerSetName) =
|
||||
makeLinkToAddStickerSet(
|
||||
stickerSetName,
|
||||
HTML
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.formatting
|
||||
|
||||
import dev.inmo.tgbotapi.CommonAbstracts.*
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.ParseMode.*
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.types.message.content.fullEntitiesList
|
||||
|
||||
fun createFormattedText(
|
||||
entities: FullTextSourcesList,
|
||||
partLength: Int = textLength.last,
|
||||
mode: ParseMode = MarkdownParseMode
|
||||
): List<String> {
|
||||
val texts = mutableListOf<String>()
|
||||
val textBuilder = StringBuilder(partLength)
|
||||
for (entity in entities) {
|
||||
val string = when (mode) {
|
||||
is MarkdownParseMode -> entity.asMarkdownSource
|
||||
is MarkdownV2ParseMode -> entity.asMarkdownV2Source
|
||||
is HTMLParseMode -> entity.asHtmlSource
|
||||
}
|
||||
if (textBuilder.length + string.length > partLength) {
|
||||
if (textBuilder.isNotEmpty()) {
|
||||
texts.add(textBuilder.toString())
|
||||
textBuilder.clear()
|
||||
}
|
||||
val chunked = string.chunked(partLength)
|
||||
val last = chunked.last()
|
||||
textBuilder.append(last)
|
||||
val listToAdd = if (chunked.size > 1) {
|
||||
chunked.subList(0, chunked.size - 1)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
listToAdd.forEach {
|
||||
texts.add(it)
|
||||
}
|
||||
} else {
|
||||
textBuilder.append(string)
|
||||
}
|
||||
}
|
||||
if (textBuilder.isNotEmpty()) {
|
||||
texts.add(textBuilder.toString())
|
||||
textBuilder.clear()
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
|
||||
fun createMarkdownText(
|
||||
entities: FullTextSourcesList,
|
||||
partLength: Int = textLength.last
|
||||
): List<String> = createFormattedText(entities, partLength, MarkdownParseMode)
|
||||
|
||||
fun FullTextSourcesList.toMarkdownCaptions(): List<String> = createMarkdownText(
|
||||
this,
|
||||
captionLength.last
|
||||
)
|
||||
fun CaptionedInput.toMarkdownCaptions(): List<String> = fullEntitiesList().toMarkdownCaptions()
|
||||
|
||||
fun FullTextSourcesList.toMarkdownTexts(): List<String> = createMarkdownText(
|
||||
this,
|
||||
textLength.last
|
||||
)
|
||||
fun TextContent.toMarkdownTexts(): List<String> = fullEntitiesList().toMarkdownTexts()
|
||||
|
||||
fun FullTextSourcesList.toMarkdownExplanations(): List<String> = createMarkdownText(
|
||||
this,
|
||||
explanationLimit.last
|
||||
)
|
||||
fun ExplainedInput.toMarkdownExplanations(): List<String> = fullEntitiesList().toMarkdownTexts()
|
||||
|
||||
|
||||
fun createMarkdownV2Text(
|
||||
entities: FullTextSourcesList,
|
||||
partLength: Int = textLength.last
|
||||
): List<String> = createFormattedText(entities, partLength, MarkdownV2ParseMode)
|
||||
|
||||
fun FullTextSourcesList.toMarkdownV2Captions(): List<String> = createMarkdownV2Text(
|
||||
this,
|
||||
captionLength.last
|
||||
)
|
||||
fun CaptionedInput.toMarkdownV2Captions(): List<String> = fullEntitiesList().toMarkdownV2Captions()
|
||||
|
||||
fun FullTextSourcesList.toMarkdownV2Texts(): List<String> = createMarkdownV2Text(
|
||||
this,
|
||||
textLength.last
|
||||
)
|
||||
fun TextContent.toMarkdownV2Texts(): List<String> = fullEntitiesList().toMarkdownV2Texts()
|
||||
|
||||
fun FullTextSourcesList.toMarkdownV2Explanations(): List<String> = createMarkdownV2Text(
|
||||
this,
|
||||
explanationLimit.last
|
||||
)
|
||||
fun ExplainedInput.toMarkdownV2Explanations(): List<String> = fullEntitiesList().toMarkdownV2Texts()
|
||||
|
||||
|
||||
fun createHtmlText(
|
||||
entities: FullTextSourcesList,
|
||||
partLength: Int = textLength.last
|
||||
): List<String> = createFormattedText(entities, partLength, HTMLParseMode)
|
||||
|
||||
fun FullTextSourcesList.toHtmlCaptions(): List<String> = createHtmlText(
|
||||
this,
|
||||
captionLength.last
|
||||
)
|
||||
fun CaptionedInput.toHtmlCaptions(): List<String> = fullEntitiesList().toHtmlCaptions()
|
||||
|
||||
fun FullTextSourcesList.toHtmlTexts(): List<String> = createHtmlText(
|
||||
this,
|
||||
textLength.last
|
||||
)
|
||||
fun TextContent.toHtmlTexts(): List<String> = fullEntitiesList().toHtmlTexts()
|
||||
|
||||
fun FullTextSourcesList.toHtmlExplanations(): List<String> = createHtmlText(
|
||||
this,
|
||||
explanationLimit.last
|
||||
)
|
||||
fun ExplainedInput.toHtmlExplanations(): List<String> = fullEntitiesList().toHtmlTexts()
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.formatting
|
||||
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.ParseMode.*
|
||||
import dev.inmo.tgbotapi.utils.extensions.*
|
||||
|
||||
const val markdownBoldControl = "*"
|
||||
const val markdownItalicControl = "_"
|
||||
const val markdownCodeControl = "`"
|
||||
const val markdownPreControl = "```"
|
||||
|
||||
const val markdownV2ItalicUnderlineDelimiter = "\u0013"
|
||||
const val markdownV2StrikethroughControl = "~"
|
||||
const val markdownV2UnderlineControl = "__"
|
||||
const val markdownV2UnderlineEndControl = "$markdownV2UnderlineControl$markdownV2ItalicUnderlineDelimiter"
|
||||
const val markdownV2ItalicEndControl = "$markdownItalicControl$markdownV2ItalicUnderlineDelimiter"
|
||||
|
||||
const val htmlBoldControl = "b"
|
||||
const val htmlItalicControl = "i"
|
||||
const val htmlCodeControl = "code"
|
||||
const val htmlPreControl = "pre"
|
||||
const val htmlUnderlineControl = "u"
|
||||
const val htmlStrikethroughControl = "s"
|
||||
|
||||
private fun String.markdownDefault(
|
||||
openControlSymbol: String,
|
||||
closeControlSymbol: String = openControlSymbol
|
||||
) = "$openControlSymbol${toMarkdown()}$closeControlSymbol"
|
||||
private fun String.markdownV2Default(
|
||||
openControlSymbol: String,
|
||||
closeControlSymbol: String = openControlSymbol,
|
||||
escapeFun: String.() -> String = String::escapeMarkdownV2Common
|
||||
) = "$openControlSymbol${escapeFun()}$closeControlSymbol"
|
||||
private fun String.htmlDefault(
|
||||
openControlSymbol: String,
|
||||
closeControlSymbol: String = openControlSymbol
|
||||
) = "<$openControlSymbol>${toHtml()}</$closeControlSymbol>"
|
||||
|
||||
fun String.linkMarkdown(link: String): String = "[${toMarkdown()}](${link.toMarkdown()})"
|
||||
fun String.linkMarkdownV2(link: String): String = "[${escapeMarkdownV2Common()}](${link.escapeMarkdownV2Link()})"
|
||||
fun String.linkHTML(link: String): String = "<a href=\"$link\">${toHtml()}</a>"
|
||||
|
||||
|
||||
fun String.boldMarkdown(): String = markdownDefault(markdownBoldControl)
|
||||
fun String.boldMarkdownV2(): String = markdownV2Default(markdownBoldControl)
|
||||
fun String.boldHTML(): String = htmlDefault(htmlBoldControl)
|
||||
|
||||
|
||||
fun String.italicMarkdown(): String = markdownDefault(markdownItalicControl)
|
||||
fun String.italicMarkdownV2(): String = markdownV2Default(markdownItalicControl, markdownV2ItalicEndControl)
|
||||
fun String.italicHTML(): String = htmlDefault(htmlItalicControl)
|
||||
|
||||
/**
|
||||
* Crutch for support of strikethrough in default markdown. Simply add modifier, but it will not look like correct
|
||||
*/
|
||||
fun String.strikethroughMarkdown(): String = map { it + "\u0336" }.joinToString("")
|
||||
fun String.strikethroughMarkdownV2(): String = markdownV2Default(markdownV2StrikethroughControl)
|
||||
fun String.strikethroughHTML(): String = htmlDefault(htmlStrikethroughControl)
|
||||
|
||||
|
||||
/**
|
||||
* Crutch for support of underline in default markdown. Simply add modifier, but it will not look like correct
|
||||
*/
|
||||
fun String.underlineMarkdown(): String = map { it + "\u0347" }.joinToString("")
|
||||
fun String.underlineMarkdownV2(): String = markdownV2Default(markdownV2UnderlineControl, markdownV2UnderlineEndControl)
|
||||
fun String.underlineHTML(): String = htmlDefault(htmlUnderlineControl)
|
||||
|
||||
|
||||
fun String.codeMarkdown(): String = markdownDefault(markdownCodeControl)
|
||||
fun String.codeMarkdownV2(): String = markdownV2Default(markdownCodeControl, escapeFun = String::escapeMarkdownV2PreAndCode)
|
||||
fun String.codeHTML(): String = htmlDefault(htmlCodeControl)
|
||||
|
||||
|
||||
fun String.preMarkdown(language: String? = null): String = markdownDefault(
|
||||
"$markdownPreControl${language ?: ""}\n",
|
||||
"\n$markdownPreControl"
|
||||
)
|
||||
fun String.preMarkdownV2(language: String? = null): String = markdownV2Default(
|
||||
"$markdownPreControl${language ?: ""}\n",
|
||||
"\n$markdownPreControl",
|
||||
String::escapeMarkdownV2PreAndCode
|
||||
)
|
||||
fun String.preHTML(language: String? = null): String = htmlDefault(
|
||||
language ?.let {
|
||||
"$htmlPreControl><$htmlCodeControl class=\"language-$language\""
|
||||
} ?: htmlPreControl,
|
||||
language ?.let {
|
||||
"$htmlCodeControl></$htmlPreControl"
|
||||
} ?: htmlPreControl
|
||||
)
|
||||
|
||||
|
||||
fun String.emailMarkdown(): String = linkMarkdown("mailto://$${toMarkdown()}")
|
||||
fun String.emailMarkdownV2(): String = linkMarkdownV2("mailto://$${toMarkdown()}")
|
||||
fun String.emailHTML(): String = linkHTML("mailto://$${toHtml()}")
|
||||
|
||||
|
||||
private inline fun String.mention(adapt: String.() -> String): String = if (startsWith("@")) {
|
||||
adapt()
|
||||
} else {
|
||||
"@${adapt()}"
|
||||
}
|
||||
|
||||
|
||||
private inline fun String.hashTag(adapt: String.() -> String): String = if (startsWith("#")) {
|
||||
adapt()
|
||||
} else {
|
||||
"#${adapt()}"
|
||||
}
|
||||
|
||||
|
||||
fun String.textMentionMarkdown(userId: UserId): String = linkMarkdown(userId.link)
|
||||
fun String.textMentionMarkdownV2(userId: UserId): String = linkMarkdownV2(userId.link)
|
||||
fun String.textMentionHTML(userId: UserId): String = linkHTML(userId.link)
|
||||
|
||||
|
||||
fun String.mentionMarkdown(): String = mention(String::toMarkdown)
|
||||
fun String.mentionMarkdownV2(): String = mention(String::escapeMarkdownV2Common)
|
||||
fun String.mentionHTML(): String = mention(String::toHtml)
|
||||
|
||||
|
||||
fun String.hashTagMarkdown(): String = hashTag(String::toMarkdown)
|
||||
fun String.hashTagMarkdownV2(): String = hashTag(String::escapeMarkdownV2Common).escapeMarkdownV2Common()
|
||||
fun String.hashTagHTML(): String = hashTag(String::toHtml)
|
||||
|
||||
|
||||
fun String.phoneMarkdown(): String = toMarkdown()
|
||||
fun String.phoneMarkdownV2(): String = escapeMarkdownV2Common()
|
||||
fun String.phoneHTML(): String = toHtml()
|
||||
|
||||
|
||||
fun String.command(adapt: String.() -> String): String = if (startsWith("/")) {
|
||||
adapt()
|
||||
} else {
|
||||
"/${adapt()}"
|
||||
}
|
||||
|
||||
fun String.commandMarkdown(): String = command(String::toMarkdown)
|
||||
fun String.commandMarkdownV2(): String = command(String::escapeMarkdownV2Common)
|
||||
fun String.commandHTML(): String = command(String::toHtml)
|
||||
|
||||
|
||||
fun String.regularMarkdown(): String = toMarkdown()
|
||||
fun String.regularMarkdownV2(): String = escapeMarkdownV2Common()
|
||||
fun String.regularHtml(): String = toHtml()
|
||||
|
||||
|
||||
fun String.cashTagMarkdown(): String = toMarkdown()
|
||||
fun String.cashTagMarkdownV2(): String = escapeMarkdownV2Common()
|
||||
fun String.cashTagHtml(): String = toHtml()
|
||||
|
||||
|
||||
infix fun String.bold(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> boldHTML()
|
||||
is Markdown -> boldMarkdown()
|
||||
is MarkdownV2 -> boldMarkdownV2()
|
||||
}
|
||||
|
||||
|
||||
infix fun String.italic(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> italicHTML()
|
||||
is Markdown -> italicMarkdown()
|
||||
is MarkdownV2 -> italicMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.hashTag(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> hashTagHTML()
|
||||
is Markdown -> hashTagMarkdown()
|
||||
is MarkdownV2 -> hashTagMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.code(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> codeHTML()
|
||||
is Markdown -> codeMarkdown()
|
||||
is MarkdownV2 -> codeMarkdownV2()
|
||||
}
|
||||
|
||||
fun String.pre(parseMode: ParseMode, language: String? = null): String = when (parseMode) {
|
||||
is HTML -> preHTML(language)
|
||||
is Markdown -> preMarkdown(language)
|
||||
is MarkdownV2 -> preMarkdownV2(language)
|
||||
}
|
||||
infix fun String.pre(parseMode: ParseMode): String = pre(parseMode, null)
|
||||
|
||||
infix fun String.email(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> emailHTML()
|
||||
is Markdown -> emailMarkdown()
|
||||
is MarkdownV2 -> emailMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun Pair<String, String>.link(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> first.linkHTML(second)
|
||||
is Markdown -> first.linkMarkdown(second)
|
||||
is MarkdownV2 -> first.linkMarkdownV2(second)
|
||||
}
|
||||
|
||||
infix fun String.mention(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> mentionHTML()
|
||||
is Markdown -> mentionMarkdown()
|
||||
is MarkdownV2 -> mentionMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun Pair<String, ChatId>.mention(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> first.textMentionHTML(second)
|
||||
is Markdown -> first.textMentionMarkdown(second)
|
||||
is MarkdownV2 -> first.textMentionMarkdownV2(second)
|
||||
}
|
||||
|
||||
infix fun String.phone(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> phoneHTML()
|
||||
is Markdown -> phoneMarkdown()
|
||||
is MarkdownV2 -> phoneMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.command(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> commandHTML()
|
||||
is Markdown -> commandMarkdown()
|
||||
is MarkdownV2 -> commandMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.underline(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> underlineHTML()
|
||||
is Markdown -> underlineMarkdown()
|
||||
is MarkdownV2 -> underlineMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.strikethrough(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> strikethroughHTML()
|
||||
is Markdown -> strikethroughMarkdown()
|
||||
is MarkdownV2 -> strikethroughMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.regular(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> regularHtml()
|
||||
is Markdown -> regularMarkdown()
|
||||
is MarkdownV2 -> regularMarkdownV2()
|
||||
}
|
||||
|
||||
infix fun String.cashtag(parseMode: ParseMode): String = when (parseMode) {
|
||||
is HTML -> cashTagHtml()
|
||||
is Markdown -> cashTagMarkdown()
|
||||
is MarkdownV2 -> cashTagMarkdownV2()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.internal_utils
|
||||
|
||||
import dev.inmo.tgbotapi.types.UpdateIdentifier
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
internal inline fun <reified T : Any, UT : Update> Flow<UT>.onlySpecifiedTypeOfDataWithUpdates(): Flow<Pair<UpdateIdentifier, T>> {
|
||||
return mapNotNull {
|
||||
it.updateId to (it.data as? T ?: return@mapNotNull null)
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <reified T : Any, UT : Update> Flow<UT>.onlySpecifiedTypeOfData(): Flow<T> {
|
||||
return mapNotNull { it as? T }
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.shortcuts
|
||||
|
||||
import dev.inmo.tgbotapi.CommonAbstracts.TextSource
|
||||
import dev.inmo.tgbotapi.extensions.utils.onlyTextContentMessages
|
||||
import dev.inmo.tgbotapi.extensions.utils.updates.asContentMessagesFlow
|
||||
import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource
|
||||
import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.types.message.content.fullEntitiesList
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Convert incoming [dev.inmo.tgbotapi.types.message.abstracts.ContentMessage.content] of
|
||||
* messages with [fullEntitiesList] and check that incoming message contains ONLY ONE [TextSource] and that is
|
||||
* [BotCommandTextSource]. Besides, it is checking that [BotCommandTextSource.command] [Regex.matches] with incoming
|
||||
* [commandRegex]
|
||||
*
|
||||
* @return The same message in case if it contains only [BotCommandTextSource] with [Regex.matches]
|
||||
* [BotCommandTextSource.command]
|
||||
*
|
||||
* @see fullEntitiesList
|
||||
* @see asContentMessagesFlow
|
||||
* @see onlyTextContentMessages
|
||||
* @see textMessages
|
||||
*/
|
||||
fun <T : ContentMessage<TextContent>> Flow<T>.filterExactCommands(
|
||||
commandRegex: Regex
|
||||
) = filter { contentMessage ->
|
||||
(contentMessage.content.fullEntitiesList().singleOrNull() as? BotCommandTextSource) ?.let { commandRegex.matches(it.command) } == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert incoming [dev.inmo.tgbotapi.types.message.abstracts.ContentMessage.content] of
|
||||
* messages with [fullEntitiesList] and check that incoming message contains [BotCommandTextSource]. Besides, it is
|
||||
* checking that [BotCommandTextSource.command] [Regex.matches] with incoming [commandRegex]
|
||||
*
|
||||
* @return The same message in case if it contains somewhere in text [BotCommandTextSource] with [Regex.matches]
|
||||
* [BotCommandTextSource.command]
|
||||
*
|
||||
* @see fullEntitiesList
|
||||
* @see asContentMessagesFlow
|
||||
* @see onlyTextContentMessages
|
||||
* @see textMessages
|
||||
*/
|
||||
fun <T : ContentMessage<TextContent>> Flow<T>.filterCommandsInsideTextMessages(
|
||||
commandRegex: Regex
|
||||
) = filter { contentMessage ->
|
||||
contentMessage.content.fullEntitiesList().any {
|
||||
(it as? BotCommandTextSource) ?.let { commandRegex.matches(it.command) } == true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert incoming [dev.inmo.tgbotapi.types.message.abstracts.ContentMessage.content] of
|
||||
* messages with [fullEntitiesList] and check that incoming message contains first [TextSource] as
|
||||
* [BotCommandTextSource]. Besides, it is checking that [BotCommandTextSource.command] [Regex.matches] with incoming
|
||||
* [commandRegex] and for other [TextSource] objects used next rules: all incoming text sources will be passed as is,
|
||||
* [RegularTextSource] will be split by " " for several [RegularTextSource] which will contains not empty args without
|
||||
* spaces.
|
||||
*
|
||||
* @return Paired original message and converted list with first entity [BotCommandTextSource] and than all others
|
||||
* according to rules in description
|
||||
*
|
||||
* @see fullEntitiesList
|
||||
* @see asContentMessagesFlow
|
||||
* @see onlyTextContentMessages
|
||||
* @see textMessages
|
||||
*/
|
||||
fun <T : ContentMessage<TextContent>> Flow<T>.filterCommandsWithArgs(
|
||||
commandRegex: Regex
|
||||
) = mapNotNull { contentMessage ->
|
||||
val allEntities = contentMessage.content.fullEntitiesList()
|
||||
(allEntities.firstOrNull() as? BotCommandTextSource) ?.let {
|
||||
if (commandRegex.matches(it.command)) {
|
||||
contentMessage to allEntities.flatMap {
|
||||
when (it) {
|
||||
is RegularTextSource -> it.source.split(" ").mapNotNull { regularTextSourcePart ->
|
||||
if (regularTextSourcePart.isNotBlank()) {
|
||||
RegularTextSource(regularTextSourcePart)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> listOf(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.shortcuts
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.utils.aggregateFlows
|
||||
import dev.inmo.tgbotapi.extensions.utils.flatMap
|
||||
import dev.inmo.tgbotapi.extensions.utils.updates.asContentMessagesFlow
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.*
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent
|
||||
import dev.inmo.tgbotapi.types.message.content.media.*
|
||||
import dev.inmo.tgbotapi.types.message.payments.InvoiceContent
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.SentMediaGroupUpdate
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.BaseSentMessageUpdate
|
||||
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
inline fun <reified T : MessageContent> filterForContentMessage(): suspend (ContentMessage<*>) -> ContentMessage<T>? = {
|
||||
if (it.content is T) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as ContentMessage<T>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T: MessageContent> Flow<BaseSentMessageUpdate>.filterContentMessages(
|
||||
): Flow<ContentMessage<T>> = asContentMessagesFlow().mapNotNull(filterForContentMessage())
|
||||
|
||||
inline fun <reified T : MediaGroupContent> Flow<SentMediaGroupUpdate>.filterMediaGroupMessages(
|
||||
): Flow<List<CommonMessage<T>>> = map {
|
||||
it.data.mapNotNull { message ->
|
||||
if (message.content is T) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
message as CommonMessage<T>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param scopeToIncludeChannels This parameter is required when you want to include [textMessages] for channels too.
|
||||
* In this case will be created new channel which will aggregate messages from [FlowsUpdatesFilter.messageFlow] and
|
||||
* [FlowsUpdatesFilter.channelPostFlow]. In case it is null will be used [Flow]s mapping
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T: MessageContent> FlowsUpdatesFilter.filterContentMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
): Flow<ContentMessage<T>> {
|
||||
return (scopeToIncludeChannels ?.let { scope ->
|
||||
aggregateFlows(
|
||||
scope,
|
||||
messageFlow,
|
||||
channelPostFlow
|
||||
)
|
||||
} ?: messageFlow).filterContentMessages()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param scopeToIncludeChannels This parameter is required when you want to include [SentMediaGroupUpdate] for channels
|
||||
* too. In this case will be created new channel which will aggregate messages from [FlowsUpdatesFilter.messageFlow] and
|
||||
* [FlowsUpdatesFilter.channelPostFlow]. In case it is null will be used [Flow]s mapping
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T: MediaGroupContent> FlowsUpdatesFilter.filterMediaGroupMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
): Flow<List<CommonMessage<T>>> {
|
||||
return (scopeToIncludeChannels ?.let { scope ->
|
||||
aggregateFlows(
|
||||
scope,
|
||||
messageMediaGroupFlow,
|
||||
channelPostMediaGroupFlow
|
||||
)
|
||||
} ?: messageMediaGroupFlow).filterMediaGroupMessages()
|
||||
}
|
||||
|
||||
fun FlowsUpdatesFilter.sentMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
): Flow<ContentMessage<MessageContent>> = filterContentMessages(scopeToIncludeChannels)
|
||||
fun FlowsUpdatesFilter.sentMessagesWithMediaGroups(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
): Flow<ContentMessage<MessageContent>> = merge(
|
||||
sentMessages(scopeToIncludeChannels),
|
||||
mediaGroupMessages(scopeToIncludeChannels).flatMap {
|
||||
it.mapNotNull {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as? ContentMessage<MessageContent>
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.animationMessages() = filterContentMessages<AnimationContent>()
|
||||
fun FlowsUpdatesFilter.animationMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<AnimationContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.audioMessages() = filterContentMessages<AudioContent>()
|
||||
fun FlowsUpdatesFilter.audioMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<AudioContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.contactMessages() = filterContentMessages<ContactContent>()
|
||||
fun FlowsUpdatesFilter.contactMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<ContactContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.diceMessages() = filterContentMessages<DiceContent>()
|
||||
fun FlowsUpdatesFilter.diceMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<DiceContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.documentMessages() = filterContentMessages<DocumentContent>()
|
||||
fun FlowsUpdatesFilter.documentMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<DocumentContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.gameMessages() = filterContentMessages<GameContent>()
|
||||
fun FlowsUpdatesFilter.gameMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<GameContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.invoiceMessages() = filterContentMessages<InvoiceContent>()
|
||||
fun FlowsUpdatesFilter.invoiceMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<InvoiceContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.locationMessages() = filterContentMessages<LocationContent>()
|
||||
fun FlowsUpdatesFilter.locationMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<LocationContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.photoMessages() = filterContentMessages<PhotoContent>()
|
||||
fun Flow<BaseSentMessageUpdate>.imageMessages() = photoMessages()
|
||||
fun FlowsUpdatesFilter.photoMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<PhotoContent>(scopeToIncludeChannels)
|
||||
fun FlowsUpdatesFilter.photoMessagesWithMediaGroups(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = merge(
|
||||
filterContentMessages<PhotoContent>(scopeToIncludeChannels),
|
||||
mediaGroupPhotosMessages(scopeToIncludeChannels).flatMap()
|
||||
)
|
||||
/**
|
||||
* Shortcut for [photoMessages]
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun FlowsUpdatesFilter.imageMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = photoMessages(scopeToIncludeChannels)
|
||||
fun FlowsUpdatesFilter.imageMessagesWithMediaGroups(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = photoMessagesWithMediaGroups(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.pollMessages() = filterContentMessages<PollContent>()
|
||||
fun FlowsUpdatesFilter.pollMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<PollContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.stickerMessages() = filterContentMessages<StickerContent>()
|
||||
fun FlowsUpdatesFilter.stickerMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<StickerContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.textMessages() = filterContentMessages<TextContent>()
|
||||
fun FlowsUpdatesFilter.textMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<TextContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.venueMessages() = filterContentMessages<VenueContent>()
|
||||
fun FlowsUpdatesFilter.venueMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<VenueContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.videoMessages() = filterContentMessages<VideoContent>()
|
||||
fun FlowsUpdatesFilter.videoMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<VideoContent>(scopeToIncludeChannels)
|
||||
fun FlowsUpdatesFilter.videoMessagesWithMediaGroups(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = merge(
|
||||
filterContentMessages<VideoContent>(scopeToIncludeChannels),
|
||||
mediaGroupVideosMessages(scopeToIncludeChannels).flatMap()
|
||||
)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.videoNoteMessages() = filterContentMessages<VideoNoteContent>()
|
||||
fun FlowsUpdatesFilter.videoNoteMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<VideoNoteContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<BaseSentMessageUpdate>.voiceMessages() = filterContentMessages<VoiceContent>()
|
||||
fun FlowsUpdatesFilter.voiceMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterContentMessages<VoiceContent>(scopeToIncludeChannels)
|
||||
|
||||
|
||||
fun Flow<SentMediaGroupUpdate>.mediaGroupMessages() = filterMediaGroupMessages<MediaGroupContent>()
|
||||
fun FlowsUpdatesFilter.mediaGroupMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterMediaGroupMessages<MediaGroupContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<SentMediaGroupUpdate>.mediaGroupPhotosMessages() = filterMediaGroupMessages<PhotoContent>()
|
||||
fun FlowsUpdatesFilter.mediaGroupPhotosMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterMediaGroupMessages<PhotoContent>(scopeToIncludeChannels)
|
||||
|
||||
fun Flow<SentMediaGroupUpdate>.mediaGroupVideosMessages() = filterMediaGroupMessages<VideoContent>()
|
||||
fun FlowsUpdatesFilter.mediaGroupVideosMessages(
|
||||
scopeToIncludeChannels: CoroutineScope? = null
|
||||
) = filterMediaGroupMessages<VideoContent>(scopeToIncludeChannels)
|
||||
@@ -0,0 +1,57 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.shortcuts
|
||||
|
||||
import dev.inmo.tgbotapi.requests.send.media.SendMediaGroup
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.chat.abstracts.Chat
|
||||
import dev.inmo.tgbotapi.types.message.ForwardInfo
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.*
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.SentMediaGroupUpdate
|
||||
|
||||
val List<CommonMessage<MediaGroupContent>>.forwardInfo: ForwardInfo?
|
||||
get() = firstOrNull() ?.forwardInfo
|
||||
val List<CommonMessage<MediaGroupContent>>.replyTo: Message?
|
||||
get() = firstOrNull() ?.replyTo
|
||||
val List<CommonMessage<MediaGroupContent>>.chat: Chat?
|
||||
get() = firstOrNull() ?.chat
|
||||
val List<MediaGroupMessage>.mediaGroupId: MediaGroupIdentifier?
|
||||
get() = firstOrNull() ?.mediaGroupId
|
||||
|
||||
val SentMediaGroupUpdate.forwardInfo: ForwardInfo?
|
||||
get() = data.first().forwardInfo
|
||||
val SentMediaGroupUpdate.replyTo: Message?
|
||||
get() = data.first().replyTo
|
||||
val SentMediaGroupUpdate.chat: Chat
|
||||
get() = data.chat!!
|
||||
val SentMediaGroupUpdate.mediaGroupId: MediaGroupIdentifier
|
||||
get() = data.mediaGroupId!!
|
||||
|
||||
fun List<CommonMessage<MediaGroupContent>>.createResend(
|
||||
chatId: ChatId,
|
||||
disableNotification: Boolean = false,
|
||||
replyTo: MessageIdentifier? = null
|
||||
) = SendMediaGroup(
|
||||
chatId,
|
||||
map { it.content.toMediaGroupMemberInputMedia() },
|
||||
disableNotification,
|
||||
replyTo
|
||||
)
|
||||
|
||||
fun List<CommonMessage<MediaGroupContent>>.createResend(
|
||||
chat: Chat,
|
||||
disableNotification: Boolean = false,
|
||||
replyTo: MessageIdentifier? = null
|
||||
) = createResend(
|
||||
chat.id,
|
||||
disableNotification,
|
||||
replyTo
|
||||
)
|
||||
|
||||
fun SentMediaGroupUpdate.createResend(
|
||||
disableNotification: Boolean = false,
|
||||
replyTo: MessageIdentifier? = null
|
||||
) = data.createResend(
|
||||
chat,
|
||||
disableNotification,
|
||||
replyTo
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.shortcuts
|
||||
|
||||
import dev.inmo.tgbotapi.types.LongSeconds
|
||||
import dev.inmo.tgbotapi.types.Seconds
|
||||
import dev.inmo.tgbotapi.types.polls.ApproximateScheduledCloseInfo
|
||||
import dev.inmo.tgbotapi.types.polls.ExactScheduledCloseInfo
|
||||
import com.soywiz.klock.DateTime
|
||||
import com.soywiz.klock.TimeSpan
|
||||
|
||||
fun closePollExactAt(
|
||||
dateTime: DateTime
|
||||
) = ExactScheduledCloseInfo(
|
||||
dateTime
|
||||
)
|
||||
|
||||
fun closePollExactAfter(
|
||||
seconds: LongSeconds
|
||||
) = closePollExactAt(
|
||||
DateTime.now() + TimeSpan(seconds.toDouble() * 1000L)
|
||||
)
|
||||
fun closePollExactAfter(
|
||||
seconds: Seconds
|
||||
) = closePollExactAfter(
|
||||
seconds.toLong()
|
||||
)
|
||||
|
||||
fun closePollAfter(
|
||||
seconds: LongSeconds
|
||||
) = ApproximateScheduledCloseInfo(
|
||||
TimeSpan(seconds.toDouble() * 1000L)
|
||||
)
|
||||
|
||||
fun closePollAfter(
|
||||
seconds: Seconds
|
||||
) = closePollAfter(seconds.toLong())
|
||||
@@ -0,0 +1,45 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.shortcuts
|
||||
|
||||
import dev.inmo.tgbotapi.bot.RequestsExecutor
|
||||
import dev.inmo.tgbotapi.requests.abstracts.Request
|
||||
import dev.inmo.tgbotapi.utils.handleSafely
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun <T: Any> RequestsExecutor.executeAsync(
|
||||
request: Request<T>,
|
||||
scope: CoroutineScope
|
||||
): Deferred<T> = scope.async {
|
||||
handleSafely {
|
||||
execute(request)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T: Any> RequestsExecutor.executeAsync(
|
||||
request: Request<T>
|
||||
): Deferred<T> = coroutineScope {
|
||||
executeAsync(request, this)
|
||||
}
|
||||
|
||||
suspend fun <T: Any> RequestsExecutor.executeUnsafe(
|
||||
request: Request<T>,
|
||||
retries: Int = 0,
|
||||
retriesDelay: Long = 1000L,
|
||||
onAllFailed: (suspend (exceptions: Array<Throwable>) -> Unit)? = null
|
||||
): T? {
|
||||
var leftRetries = retries
|
||||
val exceptions = onAllFailed ?.let { mutableListOf<Throwable>() }
|
||||
do {
|
||||
return handleSafely(
|
||||
{
|
||||
leftRetries--
|
||||
delay(retriesDelay)
|
||||
exceptions ?.add(it)
|
||||
null
|
||||
}
|
||||
) {
|
||||
execute(request)
|
||||
} ?: continue
|
||||
} while(leftRetries >= 0)
|
||||
onAllFailed ?.invoke(exceptions ?.toTypedArray() ?: emptyArray())
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.types.buttons
|
||||
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
|
||||
import dev.inmo.tgbotapi.utils.flatMatrix
|
||||
|
||||
fun InlineKeyboardMarkup(
|
||||
vararg buttons: InlineKeyboardButton
|
||||
): InlineKeyboardMarkup = InlineKeyboardMarkup(
|
||||
flatMatrix { buttons.forEach { +it } }
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.types.buttons
|
||||
|
||||
import dev.inmo.tgbotapi.types.buttons.KeyboardButton
|
||||
import dev.inmo.tgbotapi.types.buttons.ReplyKeyboardMarkup
|
||||
import dev.inmo.tgbotapi.utils.flatMatrix
|
||||
|
||||
fun ReplyKeyboardMarkup(
|
||||
vararg buttons: KeyboardButton,
|
||||
resizeKeyboard: Boolean? = null,
|
||||
oneTimeKeyboard: Boolean? = null,
|
||||
selective: Boolean? = null
|
||||
): ReplyKeyboardMarkup = ReplyKeyboardMarkup(
|
||||
flatMatrix { buttons.forEach { +it } },
|
||||
resizeKeyboard,
|
||||
oneTimeKeyboard,
|
||||
selective
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.*
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
||||
fun Flow<Update>.onlyBaseMessageUpdates(): Flow<BaseMessageUpdate> = filterIsInstance()
|
||||
|
||||
/**
|
||||
* Converts flow to [Flow] of [BaseSentMessageUpdate]
|
||||
*/
|
||||
fun Flow<BaseMessageUpdate>.onlySentMessageUpdates(): Flow<BaseSentMessageUpdate> = filterIsInstance()
|
||||
|
||||
/**
|
||||
* Converts flow to [Flow] of [BaseSentMessageUpdate]
|
||||
*/
|
||||
fun Flow<BaseMessageUpdate>.onlyEditMessageUpdates(): Flow<BaseEditMessageUpdate> = filterIsInstance()
|
||||
|
||||
/**
|
||||
* Converts flow to [Flow] of [MediaGroupUpdate]. Please, remember that it could be either [EditMediaGroupUpdate]
|
||||
* or [SentMediaGroupUpdate]
|
||||
*
|
||||
* @see onlySentMediaGroupUpdates
|
||||
* @see onlyEditMediaGroupUpdates
|
||||
*/
|
||||
fun Flow<BaseMessageUpdate>.onlyMediaGroupsUpdates(): Flow<MediaGroupUpdate> = filterIsInstance()
|
||||
|
||||
/**
|
||||
* Converts flow to [Flow] of [SentMediaGroupUpdate]
|
||||
*/
|
||||
fun Flow<MediaGroupUpdate>.onlySentMediaGroupUpdates(): Flow<SentMediaGroupUpdate> = filterIsInstance()
|
||||
|
||||
/**
|
||||
* Converts flow to [Flow] of [EditMediaGroupUpdate]
|
||||
*/
|
||||
fun Flow<MediaGroupUpdate>.onlyEditMediaGroupUpdates(): Flow<EditMediaGroupUpdate> = filterIsInstance()
|
||||
@@ -0,0 +1,25 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.types.CallbackQuery.*
|
||||
import dev.inmo.tgbotapi.types.update.CallbackQueryUpdate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* @return New [Flow] with [DataCallbackQuery] type, got from [CallbackQueryUpdate.data] field
|
||||
*/
|
||||
fun Flow<CallbackQueryUpdate>.asDataCallbackQueryFlow() = mapNotNull {
|
||||
it.data as? DataCallbackQuery
|
||||
}
|
||||
/**
|
||||
* @return New [Flow] with [GameShortNameCallbackQuery] type, got from [CallbackQueryUpdate.data] field
|
||||
*/
|
||||
fun Flow<CallbackQueryUpdate>.asGameShortNameCallbackQueryFlow() = mapNotNull {
|
||||
it.data as? GameShortNameCallbackQuery
|
||||
}
|
||||
/**
|
||||
* @return New [Flow] with [UnknownCallbackQueryType] type, got from [CallbackQueryUpdate.data] field
|
||||
*/
|
||||
fun Flow<CallbackQueryUpdate>.asUnknownCallbackQueryFlow() = mapNotNull {
|
||||
it.data as? UnknownCallbackQueryType
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.utils.internal_utils.onlySpecifiedTypeOfData
|
||||
import dev.inmo.tgbotapi.extensions.utils.internal_utils.onlySpecifiedTypeOfDataWithUpdates
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.ChosenInlineResult.BaseChosenInlineResult
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.ChosenInlineResult.LocationChosenInlineResult
|
||||
import dev.inmo.tgbotapi.types.UpdateIdentifier
|
||||
import dev.inmo.tgbotapi.types.update.ChosenInlineResultUpdate
|
||||
import dev.inmo.tgbotapi.types.update.InlineQueryUpdate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* @return Mapped [Flow] with [Pair]s. [Pair.first] in this pair will be [UpdateIdentifier]. It could be useful in
|
||||
* cases you are using [InlineQueryUpdate.updateId] for some reasons. [Pair.second] will always be [BaseChosenInlineResult].
|
||||
*/
|
||||
fun Flow<ChosenInlineResultUpdate>.onlyBaseChosenInlineResultsWithUpdates(): Flow<Pair<UpdateIdentifier, BaseChosenInlineResult>> = onlySpecifiedTypeOfDataWithUpdates()
|
||||
|
||||
/**
|
||||
* @return Filter updates only with [BaseChosenInlineResult] and map it to a [Flow] with values [BaseChosenInlineResult]
|
||||
*
|
||||
* @see onlyBaseChosenInlineResultsWithUpdates
|
||||
*/
|
||||
fun Flow<ChosenInlineResultUpdate>.onlyBaseChosenInlineResults(): Flow<BaseChosenInlineResult> = onlySpecifiedTypeOfData()
|
||||
|
||||
/**
|
||||
* @return Mapped [Flow] with [Pair]s. [Pair.first] in this pair will be [UpdateIdentifier]. It could be useful in
|
||||
* cases you are using [InlineQueryUpdate.updateId] for some reasons. [Pair.second] will always be [LocationChosenInlineResult].
|
||||
*/
|
||||
fun Flow<ChosenInlineResultUpdate>.onlyLocationChosenInlineResultsWithUpdates(): Flow<Pair<UpdateIdentifier, LocationChosenInlineResult>> = onlySpecifiedTypeOfDataWithUpdates()
|
||||
|
||||
/**
|
||||
* @return Filter updates only with [LocationChosenInlineResult] and map it to a [Flow] with values [LocationChosenInlineResult]
|
||||
*
|
||||
* @see onlyLocationChosenInlineResultsWithUpdates
|
||||
*/
|
||||
fun Flow<ChosenInlineResultUpdate>.onlyLocationChosenInlineResults(): Flow<LocationChosenInlineResult> = onlySpecifiedTypeOfData()
|
||||
@@ -0,0 +1,64 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.CommonAbstracts.TextSource
|
||||
import dev.inmo.tgbotapi.extensions.utils.onlyTextContentMessages
|
||||
import dev.inmo.tgbotapi.extensions.utils.shortcuts.*
|
||||
import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource
|
||||
import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.types.message.content.fullEntitiesList
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.BaseSentMessageUpdate
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Convert incoming [dev.inmo.tgbotapi.types.message.abstracts.ContentMessage.content] of
|
||||
* messages with [fullEntitiesList] and check that incoming message contains ONLY ONE [TextSource] and that is
|
||||
* [BotCommandTextSource]. Besides, it is checking that [BotCommandTextSource.command] [Regex.matches] with incoming
|
||||
* [commandRegex]
|
||||
*
|
||||
* @return The same message in case if it contains only [BotCommandTextSource] with [Regex.matches]
|
||||
* [BotCommandTextSource.command]
|
||||
*
|
||||
* @see fullEntitiesList
|
||||
* @see asContentMessagesFlow
|
||||
* @see onlyTextContentMessages
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.filterExactCommands(
|
||||
commandRegex: Regex
|
||||
) = textMessages().filterExactCommands(commandRegex)
|
||||
|
||||
/**
|
||||
* Convert incoming [dev.inmo.tgbotapi.types.message.abstracts.ContentMessage.content] of
|
||||
* messages with [fullEntitiesList] and check that incoming message contains [BotCommandTextSource]. Besides, it is
|
||||
* checking that [BotCommandTextSource.command] [Regex.matches] with incoming [commandRegex]
|
||||
*
|
||||
* @return The same message in case if it contains somewhere in text [BotCommandTextSource] with [Regex.matches]
|
||||
* [BotCommandTextSource.command]
|
||||
*
|
||||
* @see fullEntitiesList
|
||||
* @see asContentMessagesFlow
|
||||
* @see onlyTextContentMessages
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.filterCommandsInsideTextMessages(
|
||||
commandRegex: Regex
|
||||
) = textMessages().filterCommandsInsideTextMessages(commandRegex)
|
||||
|
||||
/**
|
||||
* Convert incoming [dev.inmo.tgbotapi.types.message.abstracts.ContentMessage.content] of
|
||||
* messages with [fullEntitiesList] and check that incoming message contains first [TextSource] as
|
||||
* [BotCommandTextSource]. Besides, it is checking that [BotCommandTextSource.command] [Regex.matches] with incoming
|
||||
* [commandRegex] and for other [TextSource] objects used next rules: all incoming text sources will be passed as is,
|
||||
* [RegularTextSource] will be split by " " for several [RegularTextSource] which will contains not empty args without
|
||||
* spaces.
|
||||
*
|
||||
* @return Paired original message and converted list with first entity [BotCommandTextSource] and than all others
|
||||
* according to rules in description
|
||||
*
|
||||
* @see fullEntitiesList
|
||||
* @see asContentMessagesFlow
|
||||
* @see onlyTextContentMessages
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.filterCommandsWithArgs(
|
||||
commandRegex: Regex
|
||||
): Flow<Pair<ContentMessage<TextContent>, List<TextSource>>> = textMessages().filterCommandsWithArgs(commandRegex)
|
||||
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
|
||||
/**
|
||||
* Non-suspendable function for easy-to-use creating of [FlowsUpdatesFilter] and applying the block to it
|
||||
*
|
||||
* @see flowsUpdatesFilter
|
||||
*/
|
||||
inline fun flowsUpdatesFilter(
|
||||
internalChannelsSizes: Int = 100,
|
||||
block: FlowsUpdatesFilter.() -> Unit
|
||||
): FlowsUpdatesFilter {
|
||||
val filter = FlowsUpdatesFilter(internalChannelsSizes)
|
||||
filter.block()
|
||||
return filter
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.utils.internal_utils.onlySpecifiedTypeOfData
|
||||
import dev.inmo.tgbotapi.extensions.utils.internal_utils.onlySpecifiedTypeOfDataWithUpdates
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.query.BaseInlineQuery
|
||||
import dev.inmo.tgbotapi.types.InlineQueries.query.LocationInlineQuery
|
||||
import dev.inmo.tgbotapi.types.UpdateIdentifier
|
||||
import dev.inmo.tgbotapi.types.update.InlineQueryUpdate
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* @return Mapped [Flow] with [Pair]s. [Pair.first] in this pair will be [UpdateIdentifier]. It could be useful in
|
||||
* cases you are using [InlineQueryUpdate.updateId] for some reasons. [Pair.second] will always be [BaseInlineQuery].
|
||||
*/
|
||||
fun Flow<InlineQueryUpdate>.onlyBaseInlineQueriesWithUpdates(): Flow<Pair<UpdateIdentifier, BaseInlineQuery>> = onlySpecifiedTypeOfDataWithUpdates()
|
||||
|
||||
/**
|
||||
* @return Filter updates only with [BaseInlineQuery] and map it to a [Flow] with values [BaseInlineQuery]
|
||||
*
|
||||
* @see onlyBaseInlineQueriesWithUpdates
|
||||
*/
|
||||
fun Flow<InlineQueryUpdate>.onlyBaseInlineQueries(): Flow<BaseInlineQuery> = onlySpecifiedTypeOfData()
|
||||
|
||||
/**
|
||||
* @return Mapped [Flow] with [Pair]s. [Pair.first] in this pair will be [UpdateIdentifier]. It could be useful in
|
||||
* cases you are using [InlineQueryUpdate.updateId] for some reasons. [Pair.second] will always be [LocationInlineQuery].
|
||||
*/
|
||||
fun Flow<InlineQueryUpdate>.onlyLocationInlineQueriesWithUpdates(): Flow<Pair<UpdateIdentifier, LocationInlineQuery>> = onlySpecifiedTypeOfDataWithUpdates()
|
||||
|
||||
/**
|
||||
* @return Filter updates only with [LocationInlineQuery] and map it to a [Flow] with values [LocationInlineQuery]
|
||||
*
|
||||
* @see onlyLocationInlineQueriesWithUpdates
|
||||
*/
|
||||
fun Flow<InlineQueryUpdate>.onlyLocationInlineQueries(): Flow<LocationInlineQuery> = onlySpecifiedTypeOfData()
|
||||
@@ -0,0 +1,34 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.*
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.BaseSentMessageUpdate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
/**
|
||||
* Will map incoming [BaseSentMessageUpdate]s to [ContentMessage] from [BaseSentMessageUpdate.data]
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.asContentMessagesFlow() = mapNotNull {
|
||||
it.data as? ContentMessage<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map incoming [BaseSentMessageUpdate]s to [CommonMessage] from [BaseSentMessageUpdate.data]
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.asCommonMessagesFlow() = mapNotNull {
|
||||
it.data as? CommonMessage<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map incoming [BaseSentMessageUpdate]s to [ChatEventMessage] from [BaseSentMessageUpdate.data]
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.asChatEventsFlow() = mapNotNull {
|
||||
it.data as? ChatEventMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map incoming [BaseSentMessageUpdate]s to [UnknownMessageType] from [BaseSentMessageUpdate.data]
|
||||
*/
|
||||
fun <T : BaseSentMessageUpdate> Flow<T>.asUnknownMessagesFlow() = mapNotNull {
|
||||
it.data as? UnknownMessageType
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.utils.nonstrictJsonFormat
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.UpdateDeserializationStrategy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
/**
|
||||
* @return Deserialize [source] as [dev.inmo.tgbotapi.types.update.abstracts.Update]
|
||||
*/
|
||||
fun Json.toTelegramUpdate(source: String) = decodeFromString(UpdateDeserializationStrategy, source)
|
||||
/**
|
||||
* @return Deserialize [source] as [dev.inmo.tgbotapi.types.update.abstracts.Update]
|
||||
*/
|
||||
fun Json.toTelegramUpdate(source: JsonElement) = decodeFromJsonElement(UpdateDeserializationStrategy, source)
|
||||
|
||||
/**
|
||||
* @return Deserialize [this] as [dev.inmo.tgbotapi.types.update.abstracts.Update]. In fact,
|
||||
* it is must be JSON
|
||||
*
|
||||
* @see Json.toTelegramUpdate
|
||||
*/
|
||||
fun String.toTelegramUpdate() = nonstrictJsonFormat.toTelegramUpdate(this)
|
||||
/**
|
||||
* @return Deserialize [this] as [dev.inmo.tgbotapi.types.update.abstracts.Update]
|
||||
*
|
||||
* @see Json.toTelegramUpdate
|
||||
*/
|
||||
fun JsonElement.toTelegramUpdate() = nonstrictJsonFormat.toTelegramUpdate(this)
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.chat.abstracts.Chat
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.SentMediaGroupUpdate
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.BaseMessageUpdate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
/**
|
||||
* [Flow.filter] incoming [BaseMessageUpdate]s by their [ChatId]
|
||||
*/
|
||||
fun <T : BaseMessageUpdate> Flow<T>.filterBaseMessageUpdatesByChatId(chatId: ChatId): Flow<T> = filter { it.data.chat.id == chatId }
|
||||
/**
|
||||
* [Flow.filter] incoming [BaseMessageUpdate]s by their [ChatId] using [Chat.id] of [chat]
|
||||
*/
|
||||
fun <T : BaseMessageUpdate> Flow<T>.filterBaseMessageUpdatesByChat(chat: Chat): Flow<T> = filterBaseMessageUpdatesByChatId(chat.id)
|
||||
|
||||
|
||||
/**
|
||||
* [Flow.filter] incoming [SentMediaGroupUpdate]s by their [ChatId]
|
||||
*/
|
||||
fun <T : SentMediaGroupUpdate> Flow<T>.filterSentMediaGroupUpdatesByChatId(chatId: ChatId): Flow<T> = filter { it.data.first().chat.id == chatId }
|
||||
/**
|
||||
* [Flow.filter] incoming [SentMediaGroupUpdate]s by their [ChatId] using [Chat.id] of [chat]
|
||||
*/
|
||||
fun <T : SentMediaGroupUpdate> Flow<T>.filterSentMediaGroupUpdatesByChat(chat: Chat): Flow<T> = filterSentMediaGroupUpdatesByChatId(chat.id)
|
||||
@@ -0,0 +1,93 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates
|
||||
|
||||
import dev.inmo.tgbotapi.types.MediaGroupIdentifier
|
||||
import dev.inmo.tgbotapi.types.UpdateIdentifier
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage
|
||||
import dev.inmo.tgbotapi.types.update.*
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.*
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.*
|
||||
|
||||
/**
|
||||
* @return If [this] is [SentMediaGroupUpdate] - [Update.updateId] of [last] element, or its own [Update.updateId]
|
||||
*/
|
||||
fun Update.lastUpdateIdentifier(): UpdateIdentifier {
|
||||
return if (this is SentMediaGroupUpdate) {
|
||||
origins.last().updateId
|
||||
} else {
|
||||
updateId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The biggest [UpdateIdentifier] OR null
|
||||
*
|
||||
* @see [Update.lastUpdateIdentifier]
|
||||
*/
|
||||
fun List<Update>.lastUpdateIdentifier(): UpdateIdentifier? {
|
||||
return maxByOrNull { it.updateId } ?.lastUpdateIdentifier()
|
||||
}
|
||||
|
||||
/**
|
||||
* Will convert incoming list of updates to list with [MediaGroupUpdate]s
|
||||
*/
|
||||
fun List<Update>.convertWithMediaGroupUpdates(): List<Update> {
|
||||
val resultUpdates = mutableListOf<Update>()
|
||||
val mediaGroups = mutableMapOf<MediaGroupIdentifier, MutableList<BaseSentMessageUpdate>>()
|
||||
for (update in this) {
|
||||
val data = (update.data as? MediaGroupMessage)
|
||||
if (data == null) {
|
||||
resultUpdates.add(update)
|
||||
continue
|
||||
}
|
||||
when (update) {
|
||||
is BaseEditMessageUpdate -> resultUpdates.add(
|
||||
update.toEditMediaGroupUpdate()
|
||||
)
|
||||
is BaseSentMessageUpdate -> {
|
||||
mediaGroups.getOrPut(data.mediaGroupId) {
|
||||
mutableListOf()
|
||||
}.add(update)
|
||||
}
|
||||
else -> resultUpdates.add(update)
|
||||
}
|
||||
}
|
||||
mediaGroups.values.map {
|
||||
it.toSentMediaGroupUpdate() ?.let { mediaGroupUpdate ->
|
||||
resultUpdates.add(mediaGroupUpdate)
|
||||
}
|
||||
}
|
||||
resultUpdates.sortBy { it.updateId }
|
||||
return resultUpdates
|
||||
}
|
||||
|
||||
/**
|
||||
* @receiver List of [BaseSentMessageUpdate] where [BaseSentMessageUpdate.data] is [MediaGroupMessage] and all messages
|
||||
* have the same [MediaGroupMessage.mediaGroupId]
|
||||
* @return [MessageMediaGroupUpdate] in case if [first] object of [this] is [MessageUpdate]. When [first] object is
|
||||
* [ChannelPostUpdate] instance - will return [ChannelPostMediaGroupUpdate]. Otherwise will be returned null
|
||||
*/
|
||||
fun List<BaseSentMessageUpdate>.toSentMediaGroupUpdate(): SentMediaGroupUpdate? = (this as? SentMediaGroupUpdate) ?: let {
|
||||
if (isEmpty()) {
|
||||
return@let null
|
||||
}
|
||||
val resultList = sortedBy { it.updateId }
|
||||
when (first()) {
|
||||
is MessageUpdate -> MessageMediaGroupUpdate(resultList)
|
||||
is ChannelPostUpdate -> ChannelPostMediaGroupUpdate(resultList)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return [EditMessageMediaGroupUpdate] in case if [this] is [EditMessageUpdate]. When [this] object is
|
||||
* [EditChannelPostUpdate] instance - will return [EditChannelPostMediaGroupUpdate]
|
||||
*
|
||||
* @throws IllegalStateException
|
||||
*/
|
||||
fun BaseEditMessageUpdate.toEditMediaGroupUpdate(): EditMediaGroupUpdate = (this as? EditMediaGroupUpdate) ?: let {
|
||||
when (this) {
|
||||
is EditMessageUpdate -> EditMessageMediaGroupUpdate(this)
|
||||
is EditChannelPostUpdate -> EditChannelPostMediaGroupUpdate(this)
|
||||
else -> error("Unsupported type of ${BaseEditMessageUpdate::class.simpleName}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates.retrieving
|
||||
|
||||
import dev.inmo.tgbotapi.bot.RequestsExecutor
|
||||
import dev.inmo.tgbotapi.bot.exceptions.RequestException
|
||||
import dev.inmo.tgbotapi.extensions.utils.updates.convertWithMediaGroupUpdates
|
||||
import dev.inmo.tgbotapi.extensions.utils.updates.lastUpdateIdentifier
|
||||
import dev.inmo.tgbotapi.requests.GetUpdates
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.update.*
|
||||
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.*
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
import dev.inmo.tgbotapi.updateshandlers.*
|
||||
import dev.inmo.tgbotapi.utils.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
fun RequestsExecutor.startGettingOfUpdatesByLongPolling(
|
||||
timeoutSeconds: Seconds = 30,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||
exceptionsHandler: (ExceptionHandler<Unit>)? = null,
|
||||
allowedUpdates: List<String>? = null,
|
||||
updatesReceiver: UpdateReceiver<Update>
|
||||
): Job = scope.launch {
|
||||
var lastUpdateIdentifier: UpdateIdentifier? = null
|
||||
|
||||
while (isActive) {
|
||||
handleSafely(
|
||||
{ e ->
|
||||
exceptionsHandler ?.invoke(e)
|
||||
if (e is RequestException) {
|
||||
delay(1000L)
|
||||
}
|
||||
}
|
||||
) {
|
||||
val updates = execute(
|
||||
GetUpdates(
|
||||
offset = lastUpdateIdentifier?.plus(1),
|
||||
timeout = timeoutSeconds,
|
||||
allowed_updates = allowedUpdates
|
||||
)
|
||||
).let { originalUpdates ->
|
||||
val converted = originalUpdates.convertWithMediaGroupUpdates()
|
||||
/**
|
||||
* Dirty hack for cases when the media group was retrieved not fully:
|
||||
*
|
||||
* We are throw out the last media group and will reretrieve it again in the next get updates
|
||||
* and it will guarantee that it is full
|
||||
*/
|
||||
if (originalUpdates.size == getUpdatesLimit.last && converted.last() is SentMediaGroupUpdate) {
|
||||
converted - converted.last()
|
||||
} else {
|
||||
converted
|
||||
}
|
||||
}
|
||||
|
||||
handleSafely {
|
||||
for (update in updates) {
|
||||
updatesReceiver(update)
|
||||
|
||||
lastUpdateIdentifier = update.lastUpdateIdentifier()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will create a new one [FlowsUpdatesFilter]. This method could be unsafe due to the fact that it will start
|
||||
* getting updates IMMEDIATELY. That means that your bot will be able to skip some of them until you will call
|
||||
* [kotlinx.coroutines.flow.Flow.collect] on one of [FlowsUpdatesFilter] flows. To avoid it, you can pass
|
||||
* [flowUpdatesPreset] lambda - it will be called BEFORE starting updates getting
|
||||
*/
|
||||
@FlowPreview
|
||||
@PreviewFeature
|
||||
@Suppress("unused")
|
||||
fun RequestsExecutor.startGettingFlowsUpdatesByLongPolling(
|
||||
timeoutSeconds: Seconds = 30,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
|
||||
exceptionsHandler: ExceptionHandler<Unit>? = null,
|
||||
flowsUpdatesFilterUpdatesKeeperCount: Int = 100,
|
||||
flowUpdatesPreset: FlowsUpdatesFilter.() -> Unit = {}
|
||||
): FlowsUpdatesFilter = FlowsUpdatesFilter(flowsUpdatesFilterUpdatesKeeperCount).apply {
|
||||
flowUpdatesPreset()
|
||||
startGettingOfUpdatesByLongPolling(timeoutSeconds, scope, exceptionsHandler, allowedUpdates, asUpdateReceiver)
|
||||
}
|
||||
|
||||
fun RequestsExecutor.startGettingOfUpdatesByLongPolling(
|
||||
updatesFilter: UpdatesFilter,
|
||||
timeoutSeconds: Seconds = 30,
|
||||
exceptionsHandler: ExceptionHandler<Unit>? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
): Job = startGettingOfUpdatesByLongPolling(
|
||||
timeoutSeconds,
|
||||
scope,
|
||||
exceptionsHandler,
|
||||
updatesFilter.allowedUpdates,
|
||||
updatesFilter.asUpdateReceiver
|
||||
)
|
||||
|
||||
fun RequestsExecutor.startGettingOfUpdatesByLongPolling(
|
||||
messageCallback: UpdateReceiver<MessageUpdate>? = null,
|
||||
messageMediaGroupCallback: UpdateReceiver<MessageMediaGroupUpdate>? = null,
|
||||
editedMessageCallback: UpdateReceiver<EditMessageUpdate>? = null,
|
||||
editedMessageMediaGroupCallback: UpdateReceiver<EditMessageMediaGroupUpdate>? = null,
|
||||
channelPostCallback: UpdateReceiver<ChannelPostUpdate>? = null,
|
||||
channelPostMediaGroupCallback: UpdateReceiver<ChannelPostMediaGroupUpdate>? = null,
|
||||
editedChannelPostCallback: UpdateReceiver<EditChannelPostUpdate>? = null,
|
||||
editedChannelPostMediaGroupCallback: UpdateReceiver<EditChannelPostMediaGroupUpdate>? = null,
|
||||
chosenInlineResultCallback: UpdateReceiver<ChosenInlineResultUpdate>? = null,
|
||||
inlineQueryCallback: UpdateReceiver<InlineQueryUpdate>? = null,
|
||||
callbackQueryCallback: UpdateReceiver<CallbackQueryUpdate>? = null,
|
||||
shippingQueryCallback: UpdateReceiver<ShippingQueryUpdate>? = null,
|
||||
preCheckoutQueryCallback: UpdateReceiver<PreCheckoutQueryUpdate>? = null,
|
||||
pollCallback: UpdateReceiver<PollUpdate>? = null,
|
||||
pollAnswerCallback: UpdateReceiver<PollAnswerUpdate>? = null,
|
||||
timeoutSeconds: Seconds = 30,
|
||||
exceptionsHandler: ExceptionHandler<Unit>? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
): Job {
|
||||
return startGettingOfUpdatesByLongPolling(
|
||||
SimpleUpdatesFilter(
|
||||
messageCallback,
|
||||
messageMediaGroupCallback,
|
||||
editedMessageCallback,
|
||||
editedMessageMediaGroupCallback,
|
||||
channelPostCallback,
|
||||
channelPostMediaGroupCallback,
|
||||
editedChannelPostCallback,
|
||||
editedChannelPostMediaGroupCallback,
|
||||
chosenInlineResultCallback,
|
||||
inlineQueryCallback,
|
||||
callbackQueryCallback,
|
||||
shippingQueryCallback,
|
||||
preCheckoutQueryCallback,
|
||||
pollCallback,
|
||||
pollAnswerCallback
|
||||
),
|
||||
timeoutSeconds,
|
||||
exceptionsHandler,
|
||||
scope
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun RequestsExecutor.startGettingOfUpdatesByLongPolling(
|
||||
messageCallback: UpdateReceiver<MessageUpdate>? = null,
|
||||
mediaGroupCallback: UpdateReceiver<MediaGroupUpdate>? = 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,
|
||||
pollCallback: UpdateReceiver<PollUpdate>? = null,
|
||||
pollAnswerCallback: UpdateReceiver<PollAnswerUpdate>? = null,
|
||||
timeoutSeconds: Seconds = 30,
|
||||
exceptionsHandler: ExceptionHandler<Unit>? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
): Job = startGettingOfUpdatesByLongPolling(
|
||||
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,
|
||||
pollCallback = pollCallback,
|
||||
pollAnswerCallback = pollAnswerCallback,
|
||||
timeoutSeconds = timeoutSeconds,
|
||||
exceptionsHandler = exceptionsHandler,
|
||||
scope = scope
|
||||
)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates.retrieving
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.utils.updates.convertWithMediaGroupUpdates
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.BaseMessageUpdate
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
import dev.inmo.tgbotapi.updateshandlers.UpdateReceiver
|
||||
import dev.inmo.tgbotapi.utils.extensions.accumulateByKey
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
/**
|
||||
* Create [UpdateReceiver] object which will correctly accumulate updates and send into output updates which INCLUDE
|
||||
* [dev.inmo.tgbotapi.types.update.MediaGroupUpdates.MediaGroupUpdate]s.
|
||||
*
|
||||
* @see UpdateReceiver
|
||||
*/
|
||||
fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation(
|
||||
output: UpdateReceiver<Update>,
|
||||
debounceTimeMillis: Long = 1000L
|
||||
): UpdateReceiver<Update> {
|
||||
val updatesChannel = Channel<Update>(Channel.UNLIMITED)
|
||||
val mediaGroupChannel = Channel<Pair<String, BaseMessageUpdate>>(Channel.UNLIMITED)
|
||||
val mediaGroupAccumulatedChannel = mediaGroupChannel.accumulateByKey(
|
||||
debounceTimeMillis,
|
||||
scope = this
|
||||
)
|
||||
|
||||
launch {
|
||||
launch {
|
||||
for (update in updatesChannel) {
|
||||
when (val data = update.data) {
|
||||
is MediaGroupMessage -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate)
|
||||
else -> output(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
for ((_, mediaGroup) in mediaGroupAccumulatedChannel) {
|
||||
mediaGroup.convertWithMediaGroupUpdates().forEach {
|
||||
output(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updatesChannel.send(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create [UpdateReceiver] object which will correctly accumulate updates and send into output updates which INCLUDE
|
||||
* [dev.inmo.tgbotapi.types.update.MediaGroupUpdates.MediaGroupUpdate]s.
|
||||
*
|
||||
* @see UpdateReceiver
|
||||
*/
|
||||
fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation(
|
||||
output: UpdateReceiver<Update>
|
||||
) = updateHandlerWithMediaGroupsAdaptation(output, 1000L)
|
||||
@@ -0,0 +1,207 @@
|
||||
package dev.inmo.tgbotapi.extensions.utils.updates.retrieving
|
||||
|
||||
import dev.inmo.tgbotapi.bot.RequestsExecutor
|
||||
import dev.inmo.tgbotapi.extensions.utils.nonstrictJsonFormat
|
||||
import dev.inmo.tgbotapi.extensions.utils.updates.flowsUpdatesFilter
|
||||
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
|
||||
import dev.inmo.tgbotapi.requests.abstracts.Request
|
||||
import dev.inmo.tgbotapi.requests.send.media.base.MultipartRequestImpl
|
||||
import dev.inmo.tgbotapi.requests.webhook.SetWebhook
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.Update
|
||||
import dev.inmo.tgbotapi.types.update.abstracts.UpdateDeserializationStrategy
|
||||
import dev.inmo.tgbotapi.updateshandlers.*
|
||||
import dev.inmo.tgbotapi.updateshandlers.webhook.WebhookPrivateKeyConfig
|
||||
import dev.inmo.tgbotapi.utils.ExceptionHandler
|
||||
import dev.inmo.tgbotapi.utils.handleSafely
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
/**
|
||||
* Allows to include webhook in custom route everywhere in your server
|
||||
*
|
||||
* @param [scope] Will be used for mapping of media groups
|
||||
* @param [exceptionsHandler] Pass this parameter to set custom exception handler for getting updates
|
||||
* @param [block] Some receiver block like [dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter]
|
||||
*
|
||||
* @see dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
* @see UpdatesFilter
|
||||
* @see UpdatesFilter.asUpdateReceiver
|
||||
*/
|
||||
fun Route.includeWebhookHandlingInRoute(
|
||||
scope: CoroutineScope,
|
||||
exceptionsHandler: ExceptionHandler<Unit>? = null,
|
||||
block: UpdateReceiver<Update>
|
||||
) {
|
||||
val transformer = scope.updateHandlerWithMediaGroupsAdaptation(block)
|
||||
post {
|
||||
handleSafely(
|
||||
exceptionsHandler ?: {}
|
||||
) {
|
||||
val asJson =
|
||||
nonstrictJsonFormat.parseToJsonElement(call.receiveText())
|
||||
val update = nonstrictJsonFormat.decodeFromJsonElement(
|
||||
UpdateDeserializationStrategy,
|
||||
asJson
|
||||
)
|
||||
transformer(update)
|
||||
}
|
||||
call.respond("Ok")
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.includeWebhookHandlingInRouteWithFlows(
|
||||
scope: CoroutineScope,
|
||||
exceptionsHandler: ExceptionHandler<Unit>? = null,
|
||||
block: FlowsUpdatesFilter.() -> Unit
|
||||
) = includeWebhookHandlingInRoute(
|
||||
scope,
|
||||
exceptionsHandler,
|
||||
flowsUpdatesFilter(block = block).asUpdateReceiver
|
||||
)
|
||||
|
||||
/**
|
||||
* Setting up ktor server, set webhook info via [SetWebhook] request.
|
||||
*
|
||||
* @param listenPort port which will be listen by bot
|
||||
* @param listenRoute address to listen by bot. If null - will be set up in root of host
|
||||
* @param scope Scope which will be used for
|
||||
* @param privateKeyConfig If configured - server will be created with [sslConnector]. [connector] will be used otherwise
|
||||
*
|
||||
* @see dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
* @see UpdatesFilter
|
||||
* @see UpdatesFilter.asUpdateReceiver
|
||||
*/
|
||||
fun startListenWebhooks(
|
||||
listenPort: Int,
|
||||
engineFactory: ApplicationEngineFactory<*, *>,
|
||||
exceptionsHandler: ExceptionHandler<Unit>,
|
||||
listenHost: String = "0.0.0.0",
|
||||
listenRoute: String? = null,
|
||||
privateKeyConfig: WebhookPrivateKeyConfig? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
|
||||
block: UpdateReceiver<Update>
|
||||
): ApplicationEngine {
|
||||
val env = applicationEngineEnvironment {
|
||||
|
||||
module {
|
||||
routing {
|
||||
listenRoute ?.also {
|
||||
createRouteFromPath(it).includeWebhookHandlingInRoute(scope, exceptionsHandler, block)
|
||||
} ?: includeWebhookHandlingInRoute(scope, exceptionsHandler, block)
|
||||
}
|
||||
}
|
||||
privateKeyConfig ?.let {
|
||||
sslConnector(
|
||||
privateKeyConfig.keyStore,
|
||||
privateKeyConfig.aliasName,
|
||||
privateKeyConfig::keyStorePassword,
|
||||
privateKeyConfig::aliasPassword
|
||||
) {
|
||||
host = listenHost
|
||||
port = listenPort
|
||||
}
|
||||
} ?: connector {
|
||||
host = listenHost
|
||||
port = listenPort
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return embeddedServer(engineFactory, env).also {
|
||||
it.start(false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun RequestsExecutor.internalSetWebhookInfoAndStartListenWebhooks(
|
||||
listenPort: Int,
|
||||
engineFactory: ApplicationEngineFactory<*, *>,
|
||||
setWebhookRequest: Request<Boolean>,
|
||||
exceptionsHandler: ExceptionHandler<Unit> = {},
|
||||
listenHost: String = "0.0.0.0",
|
||||
listenRoute: String? = null,
|
||||
privateKeyConfig: WebhookPrivateKeyConfig? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
|
||||
block: UpdateReceiver<Update>
|
||||
): ApplicationEngine {
|
||||
return try {
|
||||
execute(setWebhookRequest)
|
||||
startListenWebhooks(listenPort, engineFactory, exceptionsHandler, listenHost, listenRoute, privateKeyConfig, scope, block)
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting up ktor server, set webhook info via [SetWebhook] request.
|
||||
*
|
||||
* @param listenPort port which will be listen by bot
|
||||
* @param listenRoute address to listen by bot
|
||||
* @param scope Scope which will be used for
|
||||
*
|
||||
* @see dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
* @see UpdatesFilter
|
||||
* @see UpdatesFilter.asUpdateReceiver
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun RequestsExecutor.setWebhookInfoAndStartListenWebhooks(
|
||||
listenPort: Int,
|
||||
engineFactory: ApplicationEngineFactory<*, *>,
|
||||
setWebhookRequest: SetWebhook,
|
||||
exceptionsHandler: ExceptionHandler<Unit> = {},
|
||||
listenHost: String = "0.0.0.0",
|
||||
listenRoute: String = "/",
|
||||
privateKeyConfig: WebhookPrivateKeyConfig? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
|
||||
block: UpdateReceiver<Update>
|
||||
): ApplicationEngine = internalSetWebhookInfoAndStartListenWebhooks(
|
||||
listenPort,
|
||||
engineFactory,
|
||||
setWebhookRequest as Request<Boolean>,
|
||||
exceptionsHandler,
|
||||
listenHost,
|
||||
listenRoute,
|
||||
privateKeyConfig,
|
||||
scope,
|
||||
block
|
||||
)
|
||||
|
||||
/**
|
||||
* Setting up ktor server, set webhook info via [SetWebhook] request.
|
||||
*
|
||||
* @param listenPort port which will be listen by bot
|
||||
* @param listenRoute address to listen by bot
|
||||
* @param scope Scope which will be used for
|
||||
*
|
||||
* @see dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
|
||||
* @see UpdatesFilter
|
||||
* @see UpdatesFilter.asUpdateReceiver
|
||||
*/
|
||||
@Suppress("unused")
|
||||
suspend fun RequestsExecutor.setWebhookInfoAndStartListenWebhooks(
|
||||
listenPort: Int,
|
||||
engineFactory: ApplicationEngineFactory<*, *>,
|
||||
setWebhookRequest: MultipartRequestImpl<SetWebhook, Map<String, MultipartFile>, Boolean>,
|
||||
exceptionsHandler: ExceptionHandler<Unit> = {},
|
||||
listenHost: String = "0.0.0.0",
|
||||
listenRoute: String? = null,
|
||||
privateKeyConfig: WebhookPrivateKeyConfig? = null,
|
||||
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
|
||||
block: UpdateReceiver<Update>
|
||||
): ApplicationEngine = internalSetWebhookInfoAndStartListenWebhooks(
|
||||
listenPort,
|
||||
engineFactory,
|
||||
setWebhookRequest as Request<Boolean>,
|
||||
exceptionsHandler,
|
||||
listenHost,
|
||||
listenRoute,
|
||||
privateKeyConfig,
|
||||
scope,
|
||||
block
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.tgbotapi.types.files
|
||||
|
||||
import dev.inmo.tgbotapi.types.files.*
|
||||
import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
|
||||
fun PathedFile.asStream(
|
||||
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper
|
||||
): InputStream = URL(this.fullUrl(telegramAPIUrlsKeeper)).openStream()
|
||||
|
||||
fun PathedFile.asFile(
|
||||
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
|
||||
dest: File = File.createTempFile(this.fileUniqueId, this.filename),
|
||||
defaultBufferSize: Int = DEFAULT_BUFFER_SIZE
|
||||
): File {
|
||||
this.asStream(telegramAPIUrlsKeeper).use { input ->
|
||||
FileOutputStream(dest).use { out ->
|
||||
input.copyTo(out, defaultBufferSize)
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
fun PathedFile.asBytes(
|
||||
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper
|
||||
): ByteArray = this.asStream(telegramAPIUrlsKeeper)
|
||||
.use { input -> input.readBytes() }
|
||||
Reference in New Issue
Block a user