1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2025-09-03 23:29:33 +00:00

packages update

This commit is contained in:
2021-10-18 15:20:25 +06:00
parent 91212eaa3a
commit e60eb68b67
235 changed files with 681 additions and 109 deletions

293
tgbotapi.utils/README.md Normal file
View File

@@ -0,0 +1,293 @@
# TelegramBotAPI Util Extensions
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi.extensions.utils/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi.extensions.utils)
- [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>
## What is it?
It is wrapper library for [TelegramBotAPI 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:
[![Download](https://api.bintray.com/packages/insanusmokrassar/TelegramBotAPI/tgbotapi.extensions.utils/images/download.svg) ](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`:
`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 [TelegramBotAPI 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

View File

@@ -0,0 +1,67 @@
buildscript {
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_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()
mavenCentral()
}
kotlin {
jvm()
js(IR) {
browser()
nodejs()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api project(":tgbotapi.core")
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
jsTest {
dependencies {
implementation kotlin('test-junit')
implementation kotlin('test-js')
}
}
}
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
}
}

View File

@@ -0,0 +1 @@
{"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","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/TelegramBotAPI"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]}}

View File

@@ -0,0 +1,69 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
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"
}
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/TelegramBotAPI")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
}
}
signing {
useGpgCmd()
sign publishing.publications
}

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
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
private inline fun <reified T : MessageContent> Flow<ContentMessage<*>>.withContentType() = mapNotNull {
it.withContent<T>()
}
fun Flow<ContentMessage<*>>.onlyAnimationContentMessages() = withContentType<AnimationContent>()
fun Flow<ContentMessage<*>>.onlyAudioContentMessages() = withContentType<AudioContent>()
fun Flow<ContentMessage<*>>.onlyContactContentMessages() = withContentType<ContactContent>()
fun Flow<ContentMessage<*>>.onlyDiceContentMessages() = withContentType<DiceContent>()
fun Flow<ContentMessage<*>>.onlyDocumentContentMessages() = withContentType<DocumentContent>()
fun Flow<ContentMessage<*>>.onlyGameContentMessages() = withContentType<GameContent>()
fun Flow<ContentMessage<*>>.onlyInvoiceContentMessages() = withContentType<InvoiceContent>()
fun Flow<ContentMessage<*>>.onlyLocationContentMessages() = withContentType<LocationContent>()
fun Flow<ContentMessage<*>>.onlyPhotoContentMessages() = withContentType<PhotoContent>()
fun Flow<ContentMessage<*>>.onlyPollContentMessages() = withContentType<PollContent>()
fun Flow<ContentMessage<*>>.onlyStickerContentMessages() = withContentType<StickerContent>()
fun Flow<ContentMessage<*>>.onlyTextContentMessages() = withContentType<TextContent>()
fun Flow<ContentMessage<*>>.onlyVenueContentMessages() = withContentType<VenueContent>()
fun Flow<ContentMessage<*>>.onlyVideoContentMessages() = withContentType<VideoContent>()
fun Flow<ContentMessage<*>>.onlyVideoNoteContentMessages() = withContentType<VideoNoteContent>()
fun Flow<ContentMessage<*>>.onlyVoiceContentMessages() = withContentType<VoiceContent>()

View File

@@ -0,0 +1,39 @@
package dev.inmo.tgbotapi.extensions.utils
import dev.inmo.micro_utils.coroutines.safely
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
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 = 64
): Flow<T> {
val sharedFlow = MutableSharedFlow<T>(extraBufferCapacity = internalBufferSize)
flows.forEach {
it.onEach {
safely { sharedFlow.emit(it) }
}.launchIn(withScope)
}
return sharedFlow
}
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)
}
}
}

View File

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

View File

@@ -0,0 +1,55 @@
package dev.inmo.tgbotapi.extensions.utils
import dev.inmo.tgbotapi.types.DiceResult
import dev.inmo.tgbotapi.types.dice.Dice
import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
/**
* @param text Is a text representation
* @param number Internal representation of reel
*/
enum class SlotMachineReelImage(val text: String, val number: Int) {
BAR("[bar]", 0),
BERRIES("\uD83C\uDF52", 1),
LEMON("\uD83C\uDF4B", 2),
SEVEN("7", 3)
}
/**
* @return First [SlotMachineReelImage] with [SlotMachineReelImage.number] equal to receiver OR [SlotMachineReelImage.SEVEN]
*/
val Int.asSlotMachineReelImage
get() = SlotMachineReelImage.values().firstOrNull { it.number == this } ?: SlotMachineReelImage.SEVEN
/**
* @return First [SlotMachineReelImage] with [SlotMachineReelImage.text] equal to receiver OR [SlotMachineReelImage.SEVEN]
*/
val String.asSlotMachineReelImage
get() = SlotMachineReelImage.values().firstOrNull { it.text == this } ?: SlotMachineReelImage.SEVEN
@Serializable
data class SlotMachineResult(
val rawValue: DiceResult
) {
@Transient
val left = rawValue and 3
@Transient
val center = rawValue shr 2 and 3
@Transient
val right = rawValue shr 4
@Transient
val leftReel = left.asSlotMachineReelImage
@Transient
val centerReel = center.asSlotMachineReelImage
@Transient
val rightReel = right.asSlotMachineReelImage
}
fun Dice.calculateSlotMachineResult() = if (animationType == SlotMachineDiceAnimationType) {
SlotMachineResult(value - 1)
} else {
null
}

View File

@@ -0,0 +1,39 @@
@file:Suppress("UNCHECKED_CAST")
package dev.inmo.tgbotapi.extensions.utils
import dev.inmo.tgbotapi.types.message.abstracts.*
import dev.inmo.tgbotapi.types.message.content.abstracts.*
inline fun <reified T : MessageContent> ContentMessage<*>.withContent() = if (content is T) { this as ContentMessage<T> } else { null }
inline fun <reified T : MessageContent> ContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> CommonMessage<*>.withContent() = if (content is T) { this as CommonMessage<T> } else { null }
inline fun <reified T : MessageContent> CommonMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> PossiblySentViaBotCommonMessage<*>.withContent() = if (content is T) { this as PossiblySentViaBotCommonMessage<T> } else { null }
inline fun <reified T : MessageContent> PossiblySentViaBotCommonMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> ChannelContentMessage<*>.withContent() = if (content is T) { this as ChannelContentMessage<T> } else { null }
inline fun <reified T : MessageContent> ChannelContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> PrivateContentMessage<*>.withContent() = if (content is T) { this as PrivateContentMessage<T> } else { null }
inline fun <reified T : MessageContent> PrivateContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> PublicContentMessage<*>.withContent() = if (content is T) { this as PublicContentMessage<T> } else { null }
inline fun <reified T : MessageContent> PublicContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> GroupContentMessage<*>.withContent() = if (content is T) { this as GroupContentMessage<T> } else { null }
inline fun <reified T : MessageContent> GroupContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> FromChannelGroupContentMessage<*>.withContent() = if (content is T) { this as FromChannelGroupContentMessage<T> } else { null }
inline fun <reified T : MessageContent> FromChannelGroupContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> AnonymousGroupContentMessage<*>.withContent() = if (content is T) { this as AnonymousGroupContentMessage<T> } else { null }
inline fun <reified T : MessageContent> AnonymousGroupContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MessageContent> CommonGroupContentMessage<*>.withContent() = if (content is T) { this as CommonGroupContentMessage<T> } else { null }
inline fun <reified T : MessageContent> CommonGroupContentMessage<*>.requireWithContent() = withContent<T>()!!
inline fun <reified T : MediaGroupContent> MediaGroupMessage<*>.withContent() = if (content is T) { this as MediaGroupMessage<T> } else { null }
inline fun <reified T : MediaGroupContent> MediaGroupMessage<*>.requireWithContent() = withContent<T>()!!

View File

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

View File

@@ -0,0 +1,7 @@
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

View File

@@ -0,0 +1,54 @@
package dev.inmo.tgbotapi.extensions.utils.extensions
import dev.inmo.tgbotapi.CommonAbstracts.TextedWithTextSources
import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource
import dev.inmo.tgbotapi.types.MessageEntity.textsources.TextSource
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.TextContent
val defaultArgsSeparator = Regex(" ")
/**
* Parse commands and their args. Logic will find command, get all subsequent data as args until new command
*/
fun List<TextSource>.parseCommandsWithParams(
argsSeparator: Regex = defaultArgsSeparator
): MutableMap<String, Array<String>> {
val result = mutableMapOf<String, Array<String>>()
var currentBotCommandSource: BotCommandTextSource? = null
var currentArgs = ""
fun includeCurrent() = currentBotCommandSource ?.let {
currentArgs = currentArgs.trim()
result[it.command] = if (currentArgs.isNotEmpty()) {
currentArgs.split(argsSeparator).toTypedArray()
} else {
emptyArray()
}
currentArgs = ""
currentBotCommandSource = null
}
for (textSource in this) {
if (textSource is BotCommandTextSource) {
includeCurrent()
currentBotCommandSource = textSource
} else {
currentArgs += textSource.source
}
}
includeCurrent()
return result
}
/**
* Parse commands and their args. Logic will find command, get all subsequent data as args until new command
*/
fun TextedWithTextSources.parseCommandsWithParams(
argsSeparator: Regex = defaultArgsSeparator
) = textSources ?.parseCommandsWithParams(argsSeparator) ?: emptyMap()
/**
* Parse commands and their args. Logic will find command, get all subsequent data as args until new command
*/
fun ContentMessage<TextContent>.parseCommandsWithParams(
argsSeparator: Regex = defaultArgsSeparator
) = content.parseCommandsWithParams(argsSeparator)

View File

@@ -0,0 +1,37 @@
package dev.inmo.tgbotapi.extensions.utils.extensions
import dev.inmo.tgbotapi.CommonAbstracts.FromUser
import dev.inmo.tgbotapi.CommonAbstracts.WithUser
import dev.inmo.tgbotapi.extensions.utils.asFromUser
import dev.inmo.tgbotapi.extensions.utils.asUser
import dev.inmo.tgbotapi.extensions.utils.shortcuts.chat
import dev.inmo.tgbotapi.types.User
import dev.inmo.tgbotapi.types.chat.abstracts.Chat
import dev.inmo.tgbotapi.types.update.*
import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.*
import dev.inmo.tgbotapi.types.update.abstracts.BaseMessageUpdate
import dev.inmo.tgbotapi.types.update.abstracts.Update
import dev.inmo.tgbotapi.utils.PreviewFeature
@PreviewFeature
fun Update.sourceChat(): Chat? = when {
this is MediaGroupUpdate -> when (this) {
is SentMediaGroupUpdate -> data.chat
is EditMediaGroupUpdate -> data.chat
}
this is BaseMessageUpdate -> data.chat
else -> {
when (val data = data) {
is FromUser -> data.from
is WithUser -> data.user
else -> null
}
}
}
@PreviewFeature
fun Update.sourceUser(): User? = when (val data = data) {
is FromUser -> data.from
is WithUser -> data.user
else -> sourceChat()?.asUser()
}

View File

@@ -0,0 +1,27 @@
package dev.inmo.tgbotapi.extensions.utils.extensions.venue
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.location.StaticLocation
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: StaticLocation,
title: String,
address: String,
foursquare: Foursquare
) = Venue(location, title, address, foursquareId = foursquare.id, foursquareType = foursquare.type)
@Serializable
data class Foursquare(
@SerialName(foursquareIdField)
val id: FoursquareId,
@SerialName(foursquareTypeField)
val type: FoursquareType? = null
)

View File

@@ -0,0 +1,27 @@
package dev.inmo.tgbotapi.extensions.utils.extensions.venue
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.location.StaticLocation
import dev.inmo.tgbotapi.types.venue.Venue
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
val Venue.googlePlace: GooglePlace?
get() = googlePlaceId?.let {
GooglePlace(it, googlePlaceType)
}
fun Venue(
location: StaticLocation,
title: String,
address: String,
googlePlace: GooglePlace
) = Venue(location, title, address, googlePlaceId = googlePlace.id, googlePlaceType = googlePlace.type)
@Serializable
data class GooglePlace(
@SerialName(googlePlaceIdField)
val id: GooglePlaceId,
@SerialName(googlePlaceTypeField)
val type: GooglePlaceType? = null
)

View File

@@ -0,0 +1,454 @@
@file:Suppress("NOTHING_TO_INLINE", "unused")
package dev.inmo.tgbotapi.extensions.utils.formatting
import dev.inmo.tgbotapi.types.MessageEntity.textsources.*
import dev.inmo.tgbotapi.types.User
typealias EntitiesBuilderBody = EntitiesBuilder.() -> Unit
val newLine = regular("\n")
fun buildEntities(init: EntitiesBuilderBody): TextSourcesList = EntitiesBuilder().apply(init).build()
/**
* This builder can be used to provide building of [TextSource]s [List]
*
* @see buildEntities
*/
class EntitiesBuilder internal constructor(
private val entitiesList: MutableTextSourcesList = mutableListOf()
) {
/**
* It is not safe field which contains potentially changeable [List]
*/
val entities: TextSourcesList
get() = entitiesList
/**
* @return New immutable list which will be deattached from this builder
*/
fun build(): TextSourcesList = entities.toList()
fun add(source: TextSource): EntitiesBuilder {
entitiesList.add(source)
return this
}
fun addAll(sources: Iterable<TextSource>): EntitiesBuilder {
entitiesList.addAll(sources)
return this
}
operator fun TextSource.unaryPlus() = add(this)
operator fun TextSourcesList.unaryPlus() = addAll(this)
operator fun invoke(vararg source: TextSource) = addAll(source.toList())
operator fun String.unaryPlus(): EntitiesBuilder {
add(dev.inmo.tgbotapi.types.MessageEntity.textsources.regular(this))
return this@EntitiesBuilder
}
operator fun plus(text: String) = text.unaryPlus()
operator fun plus(source: TextSource) = add(source)
operator fun plus(sources: Iterable<TextSource>) = addAll(sources)
operator fun plus(other: EntitiesBuilder) = if (other == this) {
// do nothing; assume user is using something like regular("Hello, ") + bold("world") in buildEntities
this
} else {
addAll(other.entitiesList)
}
}
/**
* Add bold using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.bold]
*/
inline fun EntitiesBuilder.bold(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.bold(parts))
/**
* Version of [EntitiesBuilder.bold] with new line at the end
*/
inline fun EntitiesBuilder.boldln(parts: TextSourcesList) = bold(parts) + newLine
/**
* Add bold using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.bold]
*/
inline fun EntitiesBuilder.bold(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.bold(buildEntities(init)))
/**
* Version of [EntitiesBuilder.bold] with new line at the end
*/
inline fun EntitiesBuilder.boldln(noinline init: EntitiesBuilderBody) = bold(init) + newLine
/**
* Add bold using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.bold]
*/
inline fun EntitiesBuilder.bold(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.bold(*parts))
/**
* Version of [EntitiesBuilder.bold] with new line at the end
*/
inline fun EntitiesBuilder.boldln(vararg parts: TextSource) = bold(*parts) + newLine
/**
* Add bold using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.bold]
*/
inline fun EntitiesBuilder.bold(text: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.bold(text))
/**
* Version of [EntitiesBuilder.bold] with new line at the end
*/
inline fun EntitiesBuilder.boldln(text: String) = bold(text) + newLine
/**
* Add botCommand using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.botCommand]
*/
inline fun EntitiesBuilder.botCommand(command: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.botCommand(command))
/**
* Version of [EntitiesBuilder.botCommand] with new line at the end
*/
inline fun EntitiesBuilder.botCommandln(command: String) = botCommand(command) + newLine
/**
* Add cashTag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag]
*/
inline fun EntitiesBuilder.cashTag(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag(parts))
/**
* Version of [EntitiesBuilder.cashTag] with new line at the end
*/
inline fun EntitiesBuilder.cashTagln(parts: TextSourcesList) = cashTag(parts) + newLine
/**
* Add cashTag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag]
*/
inline fun EntitiesBuilder.cashTag(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag(buildEntities(init)))
/**
* Version of [EntitiesBuilder.cashTag] with new line at the end
*/
inline fun EntitiesBuilder.cashTagln(noinline init: EntitiesBuilderBody) = cashTag(init) + newLine
/**
* Add cashTag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag]
*/
inline fun EntitiesBuilder.cashTag(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag(*parts))
/**
* Version of [EntitiesBuilder.cashTag] with new line at the end
*/
inline fun EntitiesBuilder.cashTagln(vararg parts: TextSource) = cashTag(*parts) + newLine
/**
* Add cashTag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag]
*/
inline fun EntitiesBuilder.cashTag(text: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.cashTag(text))
/**
* Version of [EntitiesBuilder.cashTag] with new line at the end
*/
inline fun EntitiesBuilder.cashTagln(text: String) = cashTag(text) + newLine
/**
* Add code using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.code]
*/
inline fun EntitiesBuilder.code(code: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.code(code))
/**
* Version of [EntitiesBuilder.code] with new line at the end
*/
inline fun EntitiesBuilder.codeln(code: String) = code(code) + newLine
/**
* Add email using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.email]
*/
inline fun EntitiesBuilder.email(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.email(parts))
/**
* Version of [EntitiesBuilder.email] with new line at the end
*/
inline fun EntitiesBuilder.emailln(parts: TextSourcesList) = email(parts) + newLine
/**
* Add email using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.email]
*/
inline fun EntitiesBuilder.email(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.email(buildEntities(init)))
/**
* Version of [EntitiesBuilder.email] with new line at the end
*/
inline fun EntitiesBuilder.emailln(noinline init: EntitiesBuilderBody) = email(init) + newLine
/**
* Add email using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.email]
*/
inline fun EntitiesBuilder.email(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.email(*parts))
/**
* Version of [EntitiesBuilder.email] with new line at the end
*/
inline fun EntitiesBuilder.emailln(vararg parts: TextSource) = email(*parts) + newLine
/**
* Add email using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.email]
*/
inline fun EntitiesBuilder.email(emailAddress: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.email(emailAddress))
/**
* Version of [EntitiesBuilder.email] with new line at the end
*/
inline fun EntitiesBuilder.emailln(emailAddress: String) = email(emailAddress) + newLine
/**
* Add hashtag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag]
*/
inline fun EntitiesBuilder.hashtag(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag(parts))
/**
* Version of [EntitiesBuilder.hashtag] with new line at the end
*/
inline fun EntitiesBuilder.hashtagln(parts: TextSourcesList) = hashtag(parts) + newLine
/**
* Add hashtag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag]
*/
inline fun EntitiesBuilder.hashtag(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag(buildEntities(init)))
/**
* Version of [EntitiesBuilder.hashtag] with new line at the end
*/
inline fun EntitiesBuilder.hashtagln(noinline init: EntitiesBuilderBody) = hashtag(init) + newLine
/**
* Add hashtag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag]
*/
inline fun EntitiesBuilder.hashtag(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag(*parts))
/**
* Version of [EntitiesBuilder.hashtag] with new line at the end
*/
inline fun EntitiesBuilder.hashtagln(vararg parts: TextSource) = hashtag(*parts) + newLine
/**
* Add hashtag using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag]
*/
inline fun EntitiesBuilder.hashtag(hashtag: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.hashtag(hashtag))
/**
* Version of [EntitiesBuilder.hashtag] with new line at the end
*/
inline fun EntitiesBuilder.hashtagln(hashtag: String) = hashtag(hashtag) + newLine
/**
* Add italic using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.italic]
*/
inline fun EntitiesBuilder.italic(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.italic(parts))
/**
* Version of [EntitiesBuilder.italic] with new line at the end
*/
inline fun EntitiesBuilder.italicln(parts: TextSourcesList) = italic(parts) + newLine
/**
* Add italic using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.italic]
*/
inline fun EntitiesBuilder.italic(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.italic(buildEntities(init)))
/**
* Version of [EntitiesBuilder.italic] with new line at the end
*/
inline fun EntitiesBuilder.italicln(noinline init: EntitiesBuilderBody) = italic(init) + newLine
/**
* Add italic using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.italic]
*/
inline fun EntitiesBuilder.italic(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.italic(*parts))
/**
* Version of [EntitiesBuilder.italic] with new line at the end
*/
inline fun EntitiesBuilder.italicln(vararg parts: TextSource) = italic(*parts) + newLine
/**
* Add italic using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.italic]
*/
inline fun EntitiesBuilder.italic(text: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.italic(text))
/**
* Version of [EntitiesBuilder.italic] with new line at the end
*/
inline fun EntitiesBuilder.italicln(text: String) = italic(text) + newLine
/**
* Add mention using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.mention]
*/
inline fun EntitiesBuilder.mention(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(parts))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(parts: TextSourcesList) = mention(parts) + newLine
/**
* Add mention using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.mention]
*/
inline fun EntitiesBuilder.mention(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(buildEntities(init)))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(noinline init: EntitiesBuilderBody) = mention(init) + newLine
/**
* Add mention using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.mention]
*/
inline fun EntitiesBuilder.mention(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(*parts))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(vararg parts: TextSource) = mention(*parts) + newLine
/**
* Add mention using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.mention]
*/
inline fun EntitiesBuilder.mention(whoToMention: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(whoToMention))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(whoToMention: String) = mention(whoToMention) + newLine
/**
* Add mention using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.mention]
*/
inline fun EntitiesBuilder.mention(parts: TextSourcesList, user: User) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(parts, user))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(parts: TextSourcesList, user: User) = mention(parts) + newLine
inline fun EntitiesBuilder.mention(
user: User,
vararg parts: TextSource
) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(user, *parts))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(user: User, vararg parts: TextSource) = mention(user, *parts) + newLine
/**
* Add mention using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.mention]
*/
inline fun EntitiesBuilder.mention(text: String, user: User) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.mention(text, user))
/**
* Version of [EntitiesBuilder.mention] with new line at the end
*/
inline fun EntitiesBuilder.mentionln(text: String, user: User) = mention(text) + newLine
/**
* Add phone using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.phone]
*/
inline fun EntitiesBuilder.phone(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.phone(parts))
/**
* Version of [EntitiesBuilder.phone] with new line at the end
*/
inline fun EntitiesBuilder.phoneln(parts: TextSourcesList) = phone(parts) + newLine
/**
* Add phone using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.phone]
*/
inline fun EntitiesBuilder.phone(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.phone(buildEntities(init)))
/**
* Version of [EntitiesBuilder.phone] with new line at the end
*/
inline fun EntitiesBuilder.phoneln(noinline init: EntitiesBuilderBody) = phone(init) + newLine
/**
* Add phone using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.phone]
*/
inline fun EntitiesBuilder.phone(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.phone(*parts))
/**
* Version of [EntitiesBuilder.phone] with new line at the end
*/
inline fun EntitiesBuilder.phoneln(vararg parts: TextSource) = phone(*parts) + newLine
/**
* Add phone using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.phone]
*/
inline fun EntitiesBuilder.phone(number: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.phone(number))
/**
* Version of [EntitiesBuilder.phone] with new line at the end
*/
inline fun EntitiesBuilder.phoneln(number: String) = phone(number) + newLine
/**
* Add pre using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.pre]
*/
inline fun EntitiesBuilder.pre(code: String, language: String?) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.pre(code, language))
/**
* Version of [EntitiesBuilder.pre] with new line at the end
*/
inline fun EntitiesBuilder.preln(code: String, language: String?) = pre(code) + newLine
/**
* Will add simple [dev.inmo.tgbotapi.types.MessageEntity.textsources.regular] [TextSource]
*
* @see RegularTextSource
* @see dev.inmo.tgbotapi.extensions.utils.formatting.regularln
*/
inline fun EntitiesBuilder.regular(text: String) =
add(dev.inmo.tgbotapi.types.MessageEntity.textsources.regular(text))
/**
* Will add simple [dev.inmo.tgbotapi.types.MessageEntity.textsources.regular] [TextSource] and "\n" at the end
*
* @see RegularTextSource
* @see dev.inmo.tgbotapi.extensions.utils.formatting.regular
*/
inline fun EntitiesBuilder.regularln(text: String) = regular(text) + newLine
/**
* Add strikethrough using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough]
*/
inline fun EntitiesBuilder.strikethrough(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough(parts))
/**
* Version of [EntitiesBuilder.strikethrough] with new line at the end
*/
inline fun EntitiesBuilder.strikethroughln(parts: TextSourcesList) = strikethrough(parts) + newLine
/**
* Add strikethrough using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough]
*/
inline fun EntitiesBuilder.strikethrough(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough(buildEntities(init)))
/**
* Version of [EntitiesBuilder.strikethrough] with new line at the end
*/
inline fun EntitiesBuilder.strikethroughln(noinline init: EntitiesBuilderBody) = strikethrough(init) + newLine
/**
* Add strikethrough using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough]
*/
inline fun EntitiesBuilder.strikethrough(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough(*parts))
/**
* Version of [EntitiesBuilder.strikethrough] with new line at the end
*/
inline fun EntitiesBuilder.strikethroughln(vararg parts: TextSource) = strikethrough(*parts) + newLine
/**
* Add strikethrough using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough]
*/
inline fun EntitiesBuilder.strikethrough(text: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.strikethrough(text))
/**
* Version of [EntitiesBuilder.strikethrough] with new line at the end
*/
inline fun EntitiesBuilder.strikethroughln(text: String) = strikethrough(text) + newLine
/**
* Add link using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.link]
*/
inline fun EntitiesBuilder.link(text: String, url: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.link(text, url))
/**
* Version of [EntitiesBuilder.link] with new line at the end
*/
inline fun EntitiesBuilder.linkln(text: String, url: String) = link(text) + newLine
/**
* Add link using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.link]
*/
inline fun EntitiesBuilder.link(url: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.link(url))
/**
* Version of [EntitiesBuilder.link] with new line at the end
*/
inline fun EntitiesBuilder.linkln(url: String) = link(url) + newLine
/**
* Add underline using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.underline]
*/
inline fun EntitiesBuilder.underline(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.underline(parts))
/**
* Version of [EntitiesBuilder.underline] with new line at the end
*/
inline fun EntitiesBuilder.underlineln(parts: TextSourcesList) = underline(parts) + newLine
/**
* Add underline using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.underline]
*/
inline fun EntitiesBuilder.underline(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.underline(buildEntities(init)))
/**
* Version of [EntitiesBuilder.underline] with new line at the end
*/
inline fun EntitiesBuilder.underlineln(noinline init: EntitiesBuilderBody) = underline(init) + newLine
/**
* Add underline using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.underline]
*/
inline fun EntitiesBuilder.underline(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.underline(*parts))
/**
* Version of [EntitiesBuilder.underline] with new line at the end
*/
inline fun EntitiesBuilder.underlineln(vararg parts: TextSource) = underline(*parts) + newLine
/**
* Add underline using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.MessageEntity.textsources.underline]
*/
inline fun EntitiesBuilder.underline(text: String) = add(dev.inmo.tgbotapi.types.MessageEntity.textsources.underline(text))
/**
* Version of [EntitiesBuilder.underline] with new line at the end
*/
inline fun EntitiesBuilder.underlineln(text: String) = underline(text) + newLine

View File

@@ -0,0 +1,85 @@
package dev.inmo.tgbotapi.extensions.utils.formatting
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.MessageEntity.textsources.link
import dev.inmo.tgbotapi.types.chat.abstracts.*
import dev.inmo.tgbotapi.types.chat.abstracts.extended.ExtendedPublicChat
import dev.inmo.tgbotapi.types.message.abstracts.Message
private const val internalLinkBeginning = "https://t.me"
fun makeUsernameLink(username: String) = "$internalLinkBeginning/$username"
inline val Username.link
get() = makeUsernameLink(username)
inline fun makeLink(username: Username) = username.link
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("^@")
/**
* Link which can be used as by any user to get access to [Message]. Returns null in case when there are no
* known way to build link (for [PrivateChat]s, for example)
*/
fun makeLinkToMessage(
chat: Chat,
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
}
}
/**
* @see makeLinkToMessage
*/
val Message.link: String?
get() = makeLinkToMessage(
chat,
messageId
)
/**
* Link which can be used as by any user to get access to [Chat]. Returns null in case when there are no
* known way to build link
*/
val Chat.link: String?
get() {
if (this is UsernameChat) {
username ?.username ?.let { return it }
}
if (this is ExtendedPublicChat) {
inviteLink ?.let { return it }
}
if (this is PrivateChat) {
return id.link
}
return null
}
private const val stickerSetAddingLinkPrefix = "$internalLinkBeginning/addstickers"
val StickerSetName.stickerSetLink
get() = link(this, "$stickerSetAddingLinkPrefix/$this")

View File

@@ -0,0 +1,117 @@
package dev.inmo.tgbotapi.extensions.utils.formatting
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.MessageEntity.textsources.TextSourcesList
import dev.inmo.tgbotapi.types.ParseMode.*
import dev.inmo.tgbotapi.types.message.content.TextContent
fun createFormattedText(
entities: TextSourcesList,
partLength: Int = textLength.last,
mode: ParseMode = defaultParseMode
): List<String> {
val texts = mutableListOf<String>()
val textBuilder = StringBuilder(partLength)
for (entity in entities) {
val string = when (mode) {
is Markdown -> entity.markdown
is MarkdownV2 -> entity.markdownV2
is HTML -> entity.html
}
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: TextSourcesList,
partLength: Int = textLength.last
): List<String> = createFormattedText(entities, partLength, Markdown)
fun TextSourcesList.toMarkdownCaptions(): List<String> = createMarkdownText(
this,
captionLength.last
)
fun TextSourcesList.toMarkdownTexts(): List<String> = createMarkdownText(
this,
textLength.last
)
fun TextContent.toMarkdownTexts(): List<String> = textSources.toMarkdownTexts()
fun TextSourcesList.toMarkdownExplanations(): List<String> = createMarkdownText(
this,
explanationLimit.last
)
fun createMarkdownV2Text(
entities: TextSourcesList,
partLength: Int = textLength.last
): List<String> = createFormattedText(entities, partLength, MarkdownV2)
fun TextSourcesList.toMarkdownV2Captions(): List<String> = createMarkdownV2Text(
this,
captionLength.last
)
fun TextSourcesList.toMarkdownV2Texts(): List<String> = createMarkdownV2Text(
this,
textLength.last
)
fun TextContent.toMarkdownV2Texts(): List<String> = textSources.toMarkdownV2Texts()
fun TextSourcesList.toMarkdownV2Explanations(): List<String> = createMarkdownV2Text(
this,
explanationLimit.last
)
fun createHtmlText(
entities: TextSourcesList,
partLength: Int = textLength.last
): List<String> = createFormattedText(entities, partLength, HTML)
fun TextSourcesList.toHtmlCaptions(): List<String> = createHtmlText(
this,
captionLength.last
)
fun TextSourcesList.toHtmlTexts(): List<String> = createHtmlText(
this,
textLength.last
)
fun TextContent.toHtmlTexts(): List<String> = textSources.toHtmlTexts()
fun TextSourcesList.toHtmlExplanations(): List<String> = createHtmlText(
this,
explanationLimit.last
)

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
package dev.inmo.tgbotapi.extensions.utils.shortcuts
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 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.textSources.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.textSources.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.textSources
(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
}
}
}

View File

@@ -0,0 +1,123 @@
@file:Suppress("NOTHING_TO_INLINE", "unused", "EXPERIMENTAL_API_USAGE")
package dev.inmo.tgbotapi.extensions.utils.shortcuts
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.tgbotapi.types.message.ChannelEventMessage
import dev.inmo.tgbotapi.types.message.ChatEvents.*
import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.*
import dev.inmo.tgbotapi.types.message.PrivateEventMessage
import dev.inmo.tgbotapi.types.message.abstracts.*
import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent
import dev.inmo.tgbotapi.types.payments.SuccessfulPayment
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
import dev.inmo.tgbotapi.utils.RiskFeature
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
@RiskFeature("Use with caution")
inline fun FlowsUpdatesFilter.events(): Flow<ChatEventMessage<*>> {
return channelPostsFlow.mapNotNull { it.data as? ChatEventMessage<*> } + messagesFlow.mapNotNull { it.data as? ChatEventMessage<*> }
}
@RiskFeature("Use with caution")
inline fun FlowsUpdatesFilter.channelEvents(): Flow<ChannelEventMessage<*>> = channelPostsFlow.mapNotNull {
it.data as? ChannelEventMessage<*>
}
@RiskFeature("Use with caution")
inline fun FlowsUpdatesFilter.groupEvents(): Flow<GroupEventMessage<*>> = messagesFlow.mapNotNull {
it.data as? GroupEventMessage<*>
}
@RiskFeature("Use with caution")
inline fun FlowsUpdatesFilter.supergroupEvents(): Flow<SupergroupEventMessage<*>> = messagesFlow.mapNotNull {
it.data as? SupergroupEventMessage<*>
}
@RiskFeature("Use with caution")
inline fun FlowsUpdatesFilter.privateEvents(): Flow<PrivateEventMessage<*>> = messagesFlow.mapNotNull {
it.data as? PrivateEventMessage<*>
}
@RiskFeature("Use with caution")
inline fun <reified T: ChatEvent, reified O: ChatEventMessage<T>> Flow<ChatEventMessage<*>>.filterByChatEvent(): Flow<O> = mapNotNull {
if (it.chatEvent is T) it as? O else null
}
@RiskFeature("Use with caution")
inline fun <reified T : ChannelEvent> Flow<ChatEventMessage<*>>.filterChannelEvents() = filterByChatEvent<T, ChannelEventMessage<T>>()
@RiskFeature("Use with caution")
inline fun <reified T : ChannelEvent> FlowsUpdatesFilter.filterChannelEvents() = channelEvents().filterChannelEvents<T>()
inline fun Flow<ChatEventMessage<*>>.channelCreatedEvents() = filterChannelEvents<ChannelChatCreated>()
inline fun FlowsUpdatesFilter.channelCreatedEvents() = filterChannelEvents<ChannelChatCreated>()
inline fun Flow<ChatEventMessage<*>>.deletedChannelPhotoEvents() = filterChannelEvents<DeleteChatPhoto>()
inline fun FlowsUpdatesFilter.deletedChannelPhotoEvents() = filterChannelEvents<DeleteChatPhoto>()
inline fun Flow<ChatEventMessage<*>>.newChannelPhotoEvents() = filterChannelEvents<NewChatPhoto>()
inline fun FlowsUpdatesFilter.newChannelPhotoEvents() = filterChannelEvents<NewChatPhoto>()
inline fun Flow<ChatEventMessage<*>>.newChannelTitleEvents() = filterChannelEvents<NewChatTitle>()
inline fun FlowsUpdatesFilter.newChannelTitleEvents() = filterChannelEvents<NewChatTitle>()
inline fun Flow<ChatEventMessage<*>>.newChannelPinnedMessageEvents() = filterChannelEvents<PinnedMessage>()
inline fun FlowsUpdatesFilter.newChannelPinnedMessageEvents() = filterChannelEvents<PinnedMessage>()
inline fun Flow<ChatEventMessage<*>>.successfulPaymentInChannelEvents() = filterChannelEvents<SuccessfulPaymentEvent>()
inline fun FlowsUpdatesFilter.successfulPaymentInChannelEvents() = filterChannelEvents<SuccessfulPaymentEvent>()
inline fun Flow<ChatEventMessage<*>>.channelEvents() = filterChannelEvents<ChannelEvent>()
@RiskFeature("Use with caution")
inline fun <reified T : GroupEvent> Flow<ChatEventMessage<*>>.filterGroupEvents() = filterByChatEvent<T, GroupEventMessage<T>>()
@RiskFeature("Use with caution")
inline fun <reified T : GroupEvent> FlowsUpdatesFilter.filterGroupEvents() = groupEvents().filterByChatEvent<T, GroupEventMessage<T>>()
inline fun Flow<ChatEventMessage<*>>.groupCreatedEvents() = filterGroupEvents<GroupChatCreated>()
inline fun FlowsUpdatesFilter.groupCreatedEvents() = filterGroupEvents<GroupChatCreated>()
inline fun Flow<ChatEventMessage<*>>.deletedGroupPhotoEvents() = filterGroupEvents<DeleteChatPhoto>()
inline fun FlowsUpdatesFilter.deletedGroupPhotoEvents() = filterGroupEvents<DeleteChatPhoto>()
inline fun Flow<ChatEventMessage<*>>.newGroupMembersEvents() = filterGroupEvents<NewChatMembers>()
inline fun FlowsUpdatesFilter.newGroupMembersEvents() = filterGroupEvents<NewChatMembers>()
inline fun Flow<ChatEventMessage<*>>.leftGroupMemberEvents() = filterGroupEvents<LeftChatMember>()
inline fun FlowsUpdatesFilter.leftGroupMemberEvents() = filterGroupEvents<LeftChatMember>()
inline fun Flow<ChatEventMessage<*>>.newGroupPhotoEvents() = filterGroupEvents<NewChatPhoto>()
inline fun FlowsUpdatesFilter.newGroupPhotoEvents() = filterGroupEvents<NewChatPhoto>()
inline fun Flow<ChatEventMessage<*>>.newGroupTitleEvents() = filterGroupEvents<NewChatTitle>()
inline fun FlowsUpdatesFilter.newGroupTitleEvents() = filterGroupEvents<NewChatTitle>()
inline fun Flow<ChatEventMessage<*>>.newGroupPinnedMessageEvents() = filterGroupEvents<PinnedMessage>()
inline fun FlowsUpdatesFilter.newGroupPinnedMessageEvents() = filterGroupEvents<PinnedMessage>()
inline fun Flow<ChatEventMessage<*>>.proximityAlertTriggeredInGroupEvents() = filterGroupEvents<ProximityAlertTriggered>()
inline fun FlowsUpdatesFilter.proximityAlertTriggeredInGroupEvents() = filterGroupEvents<ProximityAlertTriggered>()
inline fun Flow<ChatEventMessage<*>>.successfulPaymentInGroupEvents() = filterGroupEvents<SuccessfulPaymentEvent>()
inline fun FlowsUpdatesFilter.successfulPaymentInGroupEvents() = filterGroupEvents<SuccessfulPaymentEvent>()
inline fun Flow<ChatEventMessage<*>>.groupEvents() = filterGroupEvents<GroupEvent>()
@RiskFeature("Use with caution")
inline fun <reified T : SupergroupEvent> Flow<ChatEventMessage<*>>.filterSupergroupEvents() = filterByChatEvent<T, SupergroupEventMessage<T>>()
@RiskFeature("Use with caution")
inline fun <reified T : SupergroupEvent> FlowsUpdatesFilter.filterSupergroupEvents() = supergroupEvents().filterByChatEvent<T, SupergroupEventMessage<T>>()
inline fun Flow<ChatEventMessage<*>>.supergroupCreatedEvents() = filterSupergroupEvents<SupergroupChatCreated>()
inline fun FlowsUpdatesFilter.supergroupCreatedEvents() = filterSupergroupEvents<SupergroupChatCreated>()
inline fun Flow<ChatEventMessage<*>>.deletedSupergroupPhotoEvents() = filterSupergroupEvents<DeleteChatPhoto>()
inline fun FlowsUpdatesFilter.deletedSupergroupPhotoEvents() = filterSupergroupEvents<DeleteChatPhoto>()
inline fun Flow<ChatEventMessage<*>>.newSupergroupMembersEvents() = filterSupergroupEvents<NewChatMembers>()
inline fun FlowsUpdatesFilter.newSupergroupMembersEvents() = filterSupergroupEvents<NewChatMembers>()
inline fun Flow<ChatEventMessage<*>>.leftSupergroupMemberEvents() = filterSupergroupEvents<LeftChatMember>()
inline fun FlowsUpdatesFilter.leftSupergroupMemberEvents() = filterSupergroupEvents<LeftChatMember>()
inline fun Flow<ChatEventMessage<*>>.newSupergroupPhotoEvents() = filterSupergroupEvents<NewChatPhoto>()
inline fun FlowsUpdatesFilter.newSupergroupPhotoEvents() = filterSupergroupEvents<NewChatPhoto>()
inline fun Flow<ChatEventMessage<*>>.newSupergroupTitleEvents() = filterSupergroupEvents<NewChatTitle>()
inline fun FlowsUpdatesFilter.newSupergroupTitleEvents() = filterSupergroupEvents<NewChatTitle>()
inline fun Flow<ChatEventMessage<*>>.newSupergroupPinnedMessageEvents() = filterSupergroupEvents<PinnedMessage>()
inline fun FlowsUpdatesFilter.newSupergroupPinnedMessageEvents() = filterSupergroupEvents<PinnedMessage>()
inline fun Flow<ChatEventMessage<*>>.proximityAlertTriggeredInSupergroupEvents() = filterSupergroupEvents<ProximityAlertTriggered>()
inline fun FlowsUpdatesFilter.proximityAlertTriggeredInSupergroupEvents() = filterSupergroupEvents<ProximityAlertTriggered>()
inline fun Flow<ChatEventMessage<*>>.successfulPaymentInSupergroupEvents() = filterSupergroupEvents<SuccessfulPaymentEvent>()
inline fun FlowsUpdatesFilter.successfulPaymentInSupergroupEvents() = filterSupergroupEvents<SuccessfulPaymentEvent>()
inline fun Flow<ChatEventMessage<*>>.supergroupEvents() = filterSupergroupEvents<SupergroupEvent>()
@RiskFeature("Use with caution")
inline fun <reified T : PrivateEvent> Flow<ChatEventMessage<*>>.filterPrivateEvents() = filterByChatEvent<T, PrivateEventMessage<T>>()
@RiskFeature("Use with caution")
inline fun <reified T : PrivateEvent> FlowsUpdatesFilter.filterPrivateEvents() = privateEvents().filterByChatEvent<T, PrivateEventMessage<T>>()
inline fun Flow<ChatEventMessage<*>>.successfulPaymentInPrivateEvents() = filterPrivateEvents<SuccessfulPaymentEvent>()
inline fun FlowsUpdatesFilter.successfulPaymentInPrivateEvents() = filterPrivateEvents<SuccessfulPaymentEvent>()
inline fun Flow<ChatEventMessage<*>>.newPrivatePinnedMessageEvents() = filterPrivateEvents<PinnedMessage>()
inline fun FlowsUpdatesFilter.newPrivatePinnedMessageEvents() = filterPrivateEvents<PinnedMessage>()
inline fun Flow<ChatEventMessage<*>>.privateEvents() = filterPrivateEvents<PrivateEvent>()

View File

@@ -0,0 +1,245 @@
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.*
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 dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.lowLevelRiskFeatureMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
@RiskFeature(lowLevelRiskFeatureMessage)
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")
@RiskFeature(lowLevelRiskFeatureMessage)
inline fun <reified T: MessageContent> Flow<BaseSentMessageUpdate>.filterContentMessages(
): Flow<ContentMessage<T>> = asContentMessagesFlow().mapNotNull(filterForContentMessage())
@RiskFeature("This method is low-level")
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.messagesFlow] and
* [FlowsUpdatesFilter.channelPostsFlow]. In case it is null will be used [Flow]s mapping
*/
@Suppress("UNCHECKED_CAST")
@RiskFeature(lowLevelRiskFeatureMessage)
inline fun <reified T: MessageContent> FlowsUpdatesFilter.filterContentMessages(
scopeToIncludeChannels: CoroutineScope? = null
): Flow<ContentMessage<T>> {
return (scopeToIncludeChannels ?.let { scope ->
aggregateFlows(
scope,
messagesFlow,
channelPostsFlow
)
} ?: messagesFlow).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.messagesFlow] and
* [FlowsUpdatesFilter.channelPostsFlow]. In case it is null will be used [Flow]s mapping
*/
@Suppress("UNCHECKED_CAST")
@RiskFeature(lowLevelRiskFeatureMessage)
inline fun <reified T: MediaGroupContent> FlowsUpdatesFilter.filterMediaGroupMessages(
scopeToIncludeChannels: CoroutineScope? = null
): Flow<List<CommonMessage<T>>> {
return (scopeToIncludeChannels ?.let { scope ->
aggregateFlows(
scope,
messageMediaGroupsFlow,
channelPostMediaGroupsFlow
)
} ?: messageMediaGroupsFlow).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 FlowsUpdatesFilter.audioMessagesWithMediaGroups(
scopeToIncludeChannels: CoroutineScope? = null
) = merge(
filterContentMessages<AudioContent>(scopeToIncludeChannels),
mediaGroupAudioMessages(scopeToIncludeChannels).flatMap()
)
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 FlowsUpdatesFilter.documentMessagesWithMediaGroups(
scopeToIncludeChannels: CoroutineScope? = null
) = merge(
filterContentMessages<DocumentContent>(scopeToIncludeChannels),
mediaGroupDocumentMessages(scopeToIncludeChannels).flatMap()
)
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)
fun Flow<SentMediaGroupUpdate>.mediaGroupVisualMessages() = filterMediaGroupMessages<VisualMediaGroupContent>()
fun FlowsUpdatesFilter.mediaGroupVisualMessages(
scopeToIncludeChannels: CoroutineScope? = null
) = filterMediaGroupMessages<VisualMediaGroupContent>(scopeToIncludeChannels)
fun Flow<SentMediaGroupUpdate>.mediaGroupAudioMessages() = filterMediaGroupMessages<AudioContent>()
fun FlowsUpdatesFilter.mediaGroupAudioMessages(
scopeToIncludeChannels: CoroutineScope? = null
) = filterMediaGroupMessages<AudioContent>(scopeToIncludeChannels)
fun Flow<SentMediaGroupUpdate>.mediaGroupDocumentMessages() = filterMediaGroupMessages<DocumentContent>()
fun FlowsUpdatesFilter.mediaGroupDocumentMessages(
scopeToIncludeChannels: CoroutineScope? = null
) = filterMediaGroupMessages<DocumentContent>(scopeToIncludeChannels)

View File

@@ -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<out MediaGroupContent>>.forwardInfo: ForwardInfo?
get() = firstOrNull() ?.forwardInfo
val List<CommonMessage<out MediaGroupContent>>.replyTo: Message?
get() = firstOrNull() ?.replyTo
val List<CommonMessage<out 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<MediaGroupContent>(
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
)

View File

@@ -0,0 +1,35 @@
package dev.inmo.tgbotapi.extensions.utils.shortcuts
import com.soywiz.klock.DateTime
import com.soywiz.klock.TimeSpan
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
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())

View File

@@ -0,0 +1,44 @@
package dev.inmo.tgbotapi.extensions.utils.shortcuts
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.tgbotapi.bot.RequestsExecutor
import dev.inmo.tgbotapi.requests.abstracts.Request
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext
fun <T: Any> RequestsExecutor.executeAsync(
request: Request<T>,
scope: CoroutineScope
): Deferred<T> = scope.async {
safely {
execute(request)
}
}
suspend fun <T: Any> RequestsExecutor.executeAsync(
request: Request<T>
): Deferred<T> = executeAsync(request, CoroutineScope(coroutineContext))
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 safely (
{
leftRetries--
delay(retriesDelay)
exceptions ?.add(it)
null
}
) {
execute(request)
} ?: continue
} while(leftRetries >= 0)
onAllFailed ?.invoke(exceptions ?.toTypedArray() ?: emptyArray())
return null
}

View File

@@ -0,0 +1,130 @@
package dev.inmo.tgbotapi.extensions.utils.types.buttons
import dev.inmo.tgbotapi.types.LoginURL
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.*
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.utils.MatrixBuilder
import dev.inmo.tgbotapi.utils.RowBuilder
/**
* Core DSL part of Inline Keyboard DSL. Can accept only [InlineKeyboardButton] and returns ready to use
* [InlineKeyboardMarkup] via [build] method
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
* @see InlineKeyboardRowBuilder
*/
class InlineKeyboardBuilder : MatrixBuilder<InlineKeyboardButton>() {
/**
* Creates [InlineKeyboardMarkup] using internal [matrix]
*/
fun build() = InlineKeyboardMarkup(matrix)
}
/**
* Row builder of [InlineKeyboardBuilder]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
class InlineKeyboardRowBuilder : RowBuilder<InlineKeyboardButton>()
/**
* Factory-function for [InlineKeyboardBuilder]. It will [apply] [block] to internally created [InlineKeyboardMarkup]
* and [InlineKeyboardBuilder.build] [InlineKeyboardMarkup] then
*
* @see InlineKeyboardBuilder.row
*/
inline fun inlineKeyboard(
crossinline block: InlineKeyboardBuilder.() -> Unit
) = InlineKeyboardBuilder().apply(block).build()
/**
* Creates an [InlineKeyboardRowBuilder] and [apply] [block] with this builder
*
* @see payButton
* @see dataButton
* @see gameButton
* @see loginButton
* @see inlineQueryInCurrentChatButton
* @see inlineQueryButton
* @see urlButton
*/
inline fun InlineKeyboardBuilder.row(
crossinline block: InlineKeyboardRowBuilder.() -> Unit
) = add(InlineKeyboardRowBuilder().apply(block).row)
/**
* Creates and put [PayInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.payButton(
text: String
) = add(PayInlineKeyboardButton(text))
/**
* Creates and put [CallbackDataInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.dataButton(
text: String,
data: String
) = add(CallbackDataInlineKeyboardButton(text, data))
/**
* Creates and put [CallbackGameInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.gameButton(
text: String
) = add(CallbackGameInlineKeyboardButton(text))
/**
* Creates and put [LoginURLInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.loginButton(
text: String,
loginUrl: LoginURL
) = add(LoginURLInlineKeyboardButton(text, loginUrl))
/**
* Creates and put [SwitchInlineQueryCurrentChatInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.inlineQueryInCurrentChatButton(
text: String,
data: String
) = add(SwitchInlineQueryCurrentChatInlineKeyboardButton(text, data))
/**
* Creates and put [SwitchInlineQueryInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.inlineQueryButton(
text: String,
data: String
) = add(SwitchInlineQueryInlineKeyboardButton(text, data))
/**
* Creates and put [URLInlineKeyboardButton]
*
* @see inlineKeyboard
* @see InlineKeyboardBuilder.row
*/
inline fun InlineKeyboardRowBuilder.urlButton(
text: String,
url: String
) = add(URLInlineKeyboardButton(text, url))

View File

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

View File

@@ -0,0 +1,102 @@
package dev.inmo.tgbotapi.extensions.utils.types.buttons
import dev.inmo.tgbotapi.types.buttons.*
import dev.inmo.tgbotapi.types.inputFieldPlaceholderField
import dev.inmo.tgbotapi.utils.MatrixBuilder
import dev.inmo.tgbotapi.utils.RowBuilder
import kotlinx.serialization.SerialName
/**
* Core DSL part of Keyboard DSL. Can accept only [KeyboardButton] and returns ready to use
* [ReplyKeyboardMarkup] via [build] method
*
* @see replyKeyboard
* @see ReplyKeyboardBuilder.row
* @see ReplyKeyboardRowBuilder
*/
class ReplyKeyboardBuilder : MatrixBuilder<KeyboardButton>() {
/**
* Creates [InlineKeyboardMarkup] using internal [matrix]
*/
fun build(
resizeKeyboard: Boolean? = null,
oneTimeKeyboard: Boolean? = null,
inputFieldPlaceholder: String? = null,
selective: Boolean? = null,
) = ReplyKeyboardMarkup(matrix, resizeKeyboard, oneTimeKeyboard, inputFieldPlaceholder, selective)
}
/**
* Row builder of [KeyboardButton]
*
* @see replyKeyboard
* @see ReplyKeyboardBuilder.row
*/
class ReplyKeyboardRowBuilder : RowBuilder<KeyboardButton>()
/**
* Factory-function for [ReplyKeyboardBuilder]. It will [apply] [block] to internally created [ReplyKeyboardMarkup]
* and [ReplyKeyboardBuilder.build] [ReplyKeyboardMarkup] then
*
* @see ReplyKeyboardBuilder.row
*/
inline fun replyKeyboard(
resizeKeyboard: Boolean? = null,
oneTimeKeyboard: Boolean? = null,
inputFieldPlaceholder: String? = null,
selective: Boolean? = null,
crossinline block: ReplyKeyboardBuilder.() -> Unit
) = ReplyKeyboardBuilder().apply(block).build(resizeKeyboard, oneTimeKeyboard, inputFieldPlaceholder, selective)
/**
* Creates an [ReplyKeyboardRowBuilder] and [apply] [block] with this builder
*
* @see simpleButton
* @see requestContactButton
* @see requestLocationButton
* @see requestPollButton
*/
inline fun ReplyKeyboardBuilder.row(
crossinline block: ReplyKeyboardRowBuilder.() -> Unit
) = add(ReplyKeyboardRowBuilder().apply(block).row)
/**
* Creates and put [SimpleKeyboardButton]
*
* @see replyKeyboard
* @see ReplyKeyboardBuilder.row
*/
inline fun ReplyKeyboardRowBuilder.simpleButton(
text: String
) = add(SimpleKeyboardButton(text))
/**
* Creates and put [RequestContactKeyboardButton]
*
* @see replyKeyboard
* @see ReplyKeyboardBuilder.row
*/
inline fun ReplyKeyboardRowBuilder.requestContactButton(
text: String
) = add(RequestContactKeyboardButton(text))
/**
* Creates and put [RequestLocationKeyboardButton]
*
* @see replyKeyboard
* @see ReplyKeyboardBuilder.row
*/
inline fun ReplyKeyboardRowBuilder.requestLocationButton(
text: String
) = add(RequestLocationKeyboardButton(text))
/**
* Creates and put [RequestPollKeyboardButton]
*
* @see replyKeyboard
* @see ReplyKeyboardBuilder.row
*/
inline fun ReplyKeyboardRowBuilder.requestPollButton(
text: String,
pollType: KeyboardButtonPollType
) = add(RequestPollKeyboardButton(text, pollType))

View File

@@ -0,0 +1,19 @@
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,
inputFieldPlaceholder: String? = null,
selective: Boolean? = null
): ReplyKeyboardMarkup = ReplyKeyboardMarkup(
flatMatrix { buttons.forEach { +it } },
resizeKeyboard,
oneTimeKeyboard,
inputFieldPlaceholder,
selective
)

View File

@@ -0,0 +1,38 @@
package dev.inmo.tgbotapi.extensions.utils.types.files
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.DownloadFileStream
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.get.GetFile
import dev.inmo.tgbotapi.types.files.PathedFile
import dev.inmo.tgbotapi.types.files.abstracts.TelegramMediaFile
import dev.inmo.tgbotapi.types.message.content.abstracts.MediaContent
import dev.inmo.tgbotapi.utils.*
suspend fun convertToStorageFile(
downloadStreamAllocator: ByteReadChannelAllocator,
pathedFile: PathedFile
): StorageFile {
return downloadStreamAllocator.asStorageFile(
pathedFile.fileName
)
}
suspend fun TelegramBot.convertToStorageFile(
pathedFile: PathedFile
): StorageFile = convertToStorageFile(
execute(DownloadFileStream(pathedFile.filePath)),
pathedFile
)
suspend fun TelegramBot.convertToStorageFile(
fileId: FileId
): StorageFile = convertToStorageFile(execute(GetFile(fileId)))
suspend fun TelegramBot.convertToStorageFile(
file: TelegramMediaFile
): StorageFile = convertToStorageFile(file.fileId)
suspend fun TelegramBot.convertToStorageFile(
content: MediaContent
): StorageFile = convertToStorageFile(content.media)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
package dev.inmo.tgbotapi.extensions.utils.updates
import dev.inmo.tgbotapi.extensions.utils.onlyTextContentMessages
import dev.inmo.tgbotapi.extensions.utils.shortcuts.*
import dev.inmo.tgbotapi.types.MessageEntity.textsources.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.update.abstracts.BaseSentMessageUpdate
import kotlinx.coroutines.flow.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>, TextSourcesList>> = textMessages().filterCommandsWithArgs(commandRegex)

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
@file:Suppress("unused")
package dev.inmo.tgbotapi.extensions.utils.updates
import dev.inmo.tgbotapi.types.message.PassportMessage
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<*>
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T : BaseSentMessageUpdate> Flow<T>.chatEvents() = mapNotNull {
it.data as? ChatEventMessage<*>
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T : BaseSentMessageUpdate> Flow<T>.passportMessages() = mapNotNull {
it.data as? PassportMessage
}
/**
* Will map incoming [BaseSentMessageUpdate]s to [UnknownMessageType] from [BaseSentMessageUpdate.data]
*/
fun <T : BaseSentMessageUpdate> Flow<T>.asUnknownMessagesFlow() = mapNotNull {
it.data as? UnknownMessageType
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,194 @@
package dev.inmo.tgbotapi.extensions.utils.updates.retrieving
import dev.inmo.micro_utils.coroutines.*
import dev.inmo.tgbotapi.bot.RequestsExecutor
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.bot.exceptions.GetUpdatesConflict
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 io.ktor.client.features.HttpRequestTimeoutException
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun TelegramBot.longPollingFlow(
timeoutSeconds: Seconds = 30,
exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null,
): Flow<Update> = channelFlow {
var lastUpdateIdentifier: UpdateIdentifier? = null
while (isActive) {
safely(
{ e ->
exceptionsHandler ?.invoke(e)
if (e is RequestException) {
delay(1000L)
}
if (e is GetUpdatesConflict && (exceptionsHandler == null || exceptionsHandler == defaultSafelyExceptionHandler)) {
println("Warning!!! Other bot with the same bot token requests updates with getUpdate in parallel")
}
}
) {
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
*/
/**
* 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
}
}
safelyWithResult {
for (update in updates) {
send(update)
lastUpdateIdentifier = update.lastUpdateIdentifier()
}
}.onFailure {
cancel(it as? CancellationException ?: return@onFailure)
}
}
}
}
fun TelegramBot.startGettingOfUpdatesByLongPolling(
timeoutSeconds: Seconds = 30,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null,
updatesReceiver: UpdateReceiver<Update>
): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates).subscribeSafely(
scope,
exceptionsHandler ?: defaultSafelyExceptionHandler,
updatesReceiver
)
fun TelegramBot.retrieveAccumulatedUpdates(
avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null,
updatesReceiver: UpdateReceiver<Update>
): Job = scope.launch {
safelyWithoutExceptions {
startGettingOfUpdatesByLongPolling(
0,
CoroutineScope(coroutineContext + SupervisorJob()),
{
if (it is HttpRequestTimeoutException) {
throw CancellationException("Cancel due to absence of new updates")
} else {
exceptionsHandler ?.invoke(it)
}
},
allowedUpdates
) {
when {
it is InlineQueryUpdate && avoidInlineQueries ||
it is CallbackQueryUpdate && avoidCallbackQueries -> return@startGettingOfUpdatesByLongPolling
else -> updatesReceiver(it)
}
}.join()
}
}
/**
* @return [kotlinx.coroutines.flow.Flow] which will emit updates to the collector while they will be accumulated. Works
* the same as [retrieveAccumulatedUpdates], but pass [kotlinx.coroutines.flow.FlowCollector.emit] as a callback
*/
fun TelegramBot.createAccumulatedUpdatesRetrieverFlow(
avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false,
exceptionsHandler: ExceptionHandler<Unit>? = null,
allowedUpdates: List<String>? = null
): Flow<Update> = channelFlow {
val parentContext = kotlin.coroutines.coroutineContext
channel.apply {
retrieveAccumulatedUpdates(
avoidInlineQueries,
avoidCallbackQueries,
CoroutineScope(parentContext),
exceptionsHandler,
allowedUpdates,
::send
).join()
close()
}
}
fun TelegramBot.retrieveAccumulatedUpdates(
flowsUpdatesFilter: FlowsUpdatesFilter,
avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: ExceptionHandler<Unit>? = null
) = flowsUpdatesFilter.run {
retrieveAccumulatedUpdates(avoidInlineQueries, avoidCallbackQueries, scope, exceptionsHandler, allowedUpdates, asUpdateReceiver)
}
/**
* Will [startGettingOfUpdatesByLongPolling] using incoming [flowsUpdatesFilter]. It is assumed that you ALREADY CONFIGURE
* all updates receivers, because this method will trigger getting of updates and.
*/
fun TelegramBot.longPolling(
flowsUpdatesFilter: FlowsUpdatesFilter,
timeoutSeconds: Seconds = 30,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: ExceptionHandler<Unit>? = null
): Job = flowsUpdatesFilter.run {
startGettingOfUpdatesByLongPolling(timeoutSeconds, scope, exceptionsHandler, allowedUpdates, asUpdateReceiver)
}
/**
* Will enable [longPolling] by creating [FlowsUpdatesFilter] with [flowsUpdatesFilterUpdatesKeeperCount] as an argument
* and applied [flowUpdatesPreset]. It is assumed that you WILL CONFIGURE all updates receivers in [flowUpdatesPreset],
* because of after [flowUpdatesPreset] method calling will be triggered getting of updates.
*/
@Suppress("unused")
fun TelegramBot.longPolling(
timeoutSeconds: Seconds = 30,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: ExceptionHandler<Unit>? = null,
flowsUpdatesFilterUpdatesKeeperCount: Int = 100,
flowUpdatesPreset: FlowsUpdatesFilter.() -> Unit
): Job = longPolling(FlowsUpdatesFilter(flowsUpdatesFilterUpdatesKeeperCount).apply(flowUpdatesPreset), timeoutSeconds, scope, exceptionsHandler)
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
)

View File

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

View File

@@ -0,0 +1,151 @@
package dev.inmo.tgbotapi.extensions.utils.updates.retrieving
import dev.inmo.micro_utils.coroutines.ExceptionHandler
import dev.inmo.micro_utils.coroutines.safely
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.webhook.SetWebhookRequest
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 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,
mediaGroupsDebounceTimeMillis: Long = 1000L,
block: UpdateReceiver<Update>
) {
val transformer = scope.updateHandlerWithMediaGroupsAdaptation(block, mediaGroupsDebounceTimeMillis)
post {
safely(
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,
mediaGroupsDebounceTimeMillis: Long = 1000L,
block: FlowsUpdatesFilter.() -> Unit
) = includeWebhookHandlingInRoute(
scope,
exceptionsHandler,
mediaGroupsDebounceTimeMillis,
flowsUpdatesFilter(block = block).asUpdateReceiver
)
/**
* Setting up ktor server
*
* @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()),
mediaGroupsDebounceTimeMillis: Long = 1000L,
block: UpdateReceiver<Update>
): ApplicationEngine {
val env = applicationEngineEnvironment {
module {
routing {
listenRoute ?.also {
createRouteFromPath(it).includeWebhookHandlingInRoute(scope, exceptionsHandler, mediaGroupsDebounceTimeMillis, block)
} ?: includeWebhookHandlingInRoute(scope, exceptionsHandler, mediaGroupsDebounceTimeMillis, 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)
}
}
/**
* Setting up ktor server, set webhook info via [SetWebhookRequest] 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: SetWebhookRequest,
exceptionsHandler: ExceptionHandler<Unit> = {},
listenHost: String = "0.0.0.0",
listenRoute: String = "/",
privateKeyConfig: WebhookPrivateKeyConfig? = null,
scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()),
mediaGroupsDebounceTimeMillis: Long = 1000L,
block: UpdateReceiver<Update>
): ApplicationEngine = try {
execute(setWebhookRequest)
startListenWebhooks(listenPort, engineFactory, exceptionsHandler, listenHost, listenRoute, privateKeyConfig, scope, mediaGroupsDebounceTimeMillis, block)
} catch (e: Exception) {
throw e
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.tgbotapi.types.files
import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper
import java.io.*
import java.net.URL
fun PathedFile.asStream(
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper
): InputStream = URL(this.fullUrl(telegramAPIUrlsKeeper)).openStream()