1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2025-11-17 20:40:20 +00:00

Compare commits

...

40 Commits

Author SHA1 Message Date
99f16e33a6 Update CHANGELOG.md 2022-08-26 12:44:11 +06:00
e029b29f7f fixes 2022-08-26 12:20:20 +06:00
c04f795fdd migrate onto 3.2.0 from 3.1.2 2022-08-26 11:00:36 +06:00
b873898100 wrap client request exception into exception handling 2022-08-25 13:35:15 +06:00
61ac9df5e3 BotException 2022-08-25 13:32:12 +06:00
b1931900e7 Update BotCommandTextSource.kt 2022-08-25 02:11:04 +06:00
fab3af48d6 Update WaitCommandsMessages.kt 2022-08-25 02:04:19 +06:00
8d7563b6e4 Update WaitCommandsMessages.kt 2022-08-25 02:02:45 +06:00
05112afe0c add deeplinks triggers 2022-08-24 15:32:34 +06:00
9ea06de27c add waitCommands* expectations 2022-08-24 15:03:54 +06:00
fff05a40d9 revert adding of mention waiters 2022-08-24 13:29:57 +06:00
5c6428f220 update microutils 2022-08-23 13:13:06 +06:00
766b7ca205 add waiters for mentions 2022-08-23 13:12:22 +06:00
41e6c52369 allowedUpdates is ALL_UPDATES_LIST by default everywhere 2022-08-23 11:55:28 +06:00
06013f624f update dependencies 2022-08-23 11:48:55 +06:00
d8bba89f3f start 3.1.2 2022-08-23 11:47:31 +06:00
141281f96d Merge pull request #643 from InsanusMokrassar/3.1.1
3.1.1
2022-08-15 01:31:59 +06:00
be7aaa7845 fixes and improvements in webapps 2022-08-15 01:29:24 +06:00
78c224ffa8 fix on asUser fun 2022-08-15 01:21:10 +06:00
a08d07f7b3 fixes in popup params and buttons 2022-08-15 01:20:29 +06:00
69dde19543 Update CHANGELOG.md 2022-08-14 22:13:03 +06:00
e8a3b93831 Update Extended.kt 2022-08-14 22:11:55 +06:00
e10caa3171 add support of has_restricted_voice_and_video_messages 2022-08-14 21:56:25 +06:00
d4fe4e09fc add support of Bot API 6.2 in webapp parts 2022-08-14 21:52:26 +06:00
ad453afba2 start 3.1.1 2022-08-14 18:55:08 +06:00
a0daca28b1 Merge pull request #642 from InsanusMokrassar/3.1.0
3.1.0
2022-08-13 14:44:00 +06:00
cd62a9ef3c fixes 2022-08-13 14:40:00 +06:00
3c084d70e5 fix of #588 2022-08-13 13:13:07 +06:00
2611d4ecc1 fill changelog with dependnecies updates 2022-08-13 13:06:18 +06:00
b4d853dfa0 update bot api support info 2022-08-13 13:03:34 +06:00
5044075adf add support of sticker_type for new sticker sets and rework create new sticker set requests 2022-08-13 13:01:09 +06:00
5f2660b804 fill raw sticker data 2022-08-13 12:20:28 +06:00
c72dccc0f9 rework of stickers and stickersets 2022-08-13 11:58:42 +06:00
3fc1058491 start rework of stickers and stickersets 2022-08-13 00:02:48 +06:00
c3668e978b add support of custom emojis 2022-08-12 23:21:53 +06:00
5a4a6d5710 start 3.1.0 2022-08-12 22:42:47 +06:00
ed1d8beb2e Update libs.versions.toml 2022-08-12 13:28:45 +06:00
881ede7d2a start 3.0.3 2022-08-12 13:25:58 +06:00
60d3d279e7 Update gradle-wrapper.properties 2022-08-06 10:19:22 +06:00
4b84518dbb Merge pull request #637 from InsanusMokrassar/3.0.2
3.0.2
2022-08-06 00:40:46 +06:00
55 changed files with 2107 additions and 266 deletions

View File

@@ -1,5 +1,35 @@
# TelegramBotAPI changelog # TelegramBotAPI changelog
## 3.2.0
**Since this update, `RequestsExecutor#execute` may throw only `BotException`. In case you wish to handle some exceptions from `execute` you must catch `BotException` and handle its `cause`**
* `Versions`:
* `Serialization`: `1.4.0-RC` -> `1.4.0`
* `MicroUtils`: `0.12.1` -> `0.12.4`
* `Core`:
* `SetWebhook#allowedUpdates` now is `ALL_UPDATES_LIST` by default instead of `null`
* `API`:
* Extension `TelegramBot#setWebhook` parameter `allowedUpdates` now is `ALL_UPDATES_LIST` by default instead of `null`
* `Utils`:
* All related to long polling extensions parameters `allowedUpdates` now are `ALL_UPDATES_LIST` by default instead of `null`
## 3.1.1
* `Common`:
* Complete Bot API 6.2 implementation
## 3.1.0
**This update contains including of Bot API 6.2**
* `Versions`:
* `Ktor`: `2.0.3` -> `2.1.0`
* `MicroUtils`: `0.12.0` -> `0.12.1`
* `Core`:
* Add support of `custom emoji`s
* Add support of `sticker_type`
## 3.0.2 ## 3.0.2
**ALL OLD DEPRECATIONS HAVE BEEN REMOVED** **ALL OLD DEPRECATIONS HAVE BEEN REMOVED**

View File

@@ -1,4 +1,4 @@
# TelegramBotAPI [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi) [![Supported version](https://img.shields.io/badge/Telegram%20Bot%20API-6.1-blue)](https://core.telegram.org/bots/api-changelog#june-20-2022) # TelegramBotAPI [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi) [![Supported version](https://img.shields.io/badge/Telegram%20Bot%20API-6.2-blue)](https://core.telegram.org/bots/api-changelog#august-12-2022)
| Docs | [![KDocs](https://img.shields.io/static/v1?label=Dokka&message=KDocs&color=blue&logo=kotlin)](https://tgbotapi.inmo.dev/index.html) [![Mini tutorial](https://img.shields.io/static/v1?label=Bookstack&message=Tutorial&color=blue&logo=bookstack)](https://bookstack.inmo.dev/books/telegrambotapi/chapter/introduction-tutorial) | | Docs | [![KDocs](https://img.shields.io/static/v1?label=Dokka&message=KDocs&color=blue&logo=kotlin)](https://tgbotapi.inmo.dev/index.html) [![Mini tutorial](https://img.shields.io/static/v1?label=Bookstack&message=Tutorial&color=blue&logo=bookstack)](https://bookstack.inmo.dev/books/telegrambotapi/chapter/introduction-tutorial) |
|:---:|:---:| |:---:|:---:|

View File

@@ -6,4 +6,4 @@ kotlin.incremental=true
kotlin.incremental.js=true kotlin.incremental.js=true
library_group=dev.inmo library_group=dev.inmo
library_version=3.0.2 library_version=3.2.0

View File

@@ -1,19 +1,19 @@
[versions] [versions]
kotlin = "1.7.10" kotlin = "1.7.10"
kotlin-serialization = "1.4.0-RC" kotlin-serialization = "1.4.0"
kotlin-coroutines = "1.6.4" kotlin-coroutines = "1.6.4"
javax-activation = "1.1.1" javax-activation = "1.1.1"
korlibs = "3.0.0" korlibs = "3.0.0"
uuid = "0.5.0" uuid = "0.5.0"
ktor = "2.0.3" ktor = "2.1.0"
ksp = "1.7.10-1.0.6" ksp = "1.7.10-1.0.6"
kotlin-poet = "1.12.0" kotlin-poet = "1.12.0"
microutils = "0.12.0" microutils = "0.12.4"
github-release-plugin = "2.4.1" github-release-plugin = "2.4.1"

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip

View File

@@ -0,0 +1,37 @@
package dev.inmo.tgbotapi.extensions.api.get
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.get.GetCustomEmojiStickers
import dev.inmo.tgbotapi.requests.get.GetStickerSet
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.files.Sticker
import kotlin.js.JsName
import kotlin.jvm.JvmName
suspend fun TelegramBot.getCustomEmojiStickers(
customEmojiIds: List<CustomEmojiId>
) = execute(
GetCustomEmojiStickers(customEmojiIds)
)
@JvmName("getCustomEmojiStickersWithStringsList")
@JsName("getCustomEmojiStickersWithStringsList")
suspend fun TelegramBot.getCustomEmojiStickers(
customEmojiIds: List<String>
) = getCustomEmojiStickers(customEmojiIds.map(::CustomEmojiId))
suspend fun TelegramBot.getCustomEmojiStickerOrNull(
customEmojiId: CustomEmojiId
) = getCustomEmojiStickers(listOf(customEmojiId)).firstOrNull()
suspend fun TelegramBot.getCustomEmojiStickerOrThrow(
customEmojiId: CustomEmojiId
) = getCustomEmojiStickers(listOf(customEmojiId)).first()
suspend fun TelegramBot.getCustomEmojiStickerOrNull(
customEmojiId: String
) = getCustomEmojiStickerOrNull(CustomEmojiId(customEmojiId))
suspend fun TelegramBot.getCustomEmojiStickerOrThrow(
customEmojiId: String
) = getCustomEmojiStickerOrThrow(CustomEmojiId(customEmojiId))

View File

@@ -0,0 +1,54 @@
package dev.inmo.tgbotapi.extensions.api.stickers
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.stickers.*
import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
suspend fun TelegramBot.createNewMaskAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: FileId,
emojis: String,
maskPosition: MaskPosition
) = execute(
CreateNewMaskAnimatedStickerSet(userId, name, title, sticker, emojis, maskPosition)
)
suspend fun TelegramBot.createNewMaskAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: MultipartFile,
emojis: String,
maskPosition: MaskPosition
) = execute(
CreateNewMaskAnimatedStickerSet(userId, name, title, sticker, emojis, maskPosition)
)
suspend fun TelegramBot.createNewMaskAnimatedStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: FileId,
emojis: String,
maskPosition: MaskPosition
) = createNewMaskAnimatedStickerSet(
user.id, name, title, sticker, emojis, maskPosition
)
suspend fun TelegramBot.createNewMaskAnimatedStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: MultipartFile,
emojis: String,
maskPosition: MaskPosition
) = createNewMaskAnimatedStickerSet(
user.id, name, title, sticker, emojis, maskPosition
)

View File

@@ -0,0 +1,54 @@
package dev.inmo.tgbotapi.extensions.api.stickers
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.stickers.*
import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
suspend fun TelegramBot.createNewMaskStickerSet(
userId: UserId,
name: String,
title: String,
sticker: FileId,
emojis: String,
maskPosition: MaskPosition
) = execute(
CreateNewMaskStickerSet(userId, name, title, sticker, emojis, maskPosition)
)
suspend fun TelegramBot.createNewMaskStickerSet(
userId: UserId,
name: String,
title: String,
sticker: MultipartFile,
emojis: String,
maskPosition: MaskPosition
) = execute(
CreateNewMaskStickerSet(userId, name, title, sticker, emojis, maskPosition)
)
suspend fun TelegramBot.createNewMaskStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: FileId,
emojis: String,
maskPosition: MaskPosition
) = createNewMaskStickerSet(
user.id, name, title, sticker, emojis, maskPosition
)
suspend fun TelegramBot.createNewMaskStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: MultipartFile,
emojis: String,
maskPosition: MaskPosition
) = createNewMaskStickerSet(
user.id, name, title, sticker, emojis, maskPosition
)

View File

@@ -0,0 +1,54 @@
package dev.inmo.tgbotapi.extensions.api.stickers
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.stickers.*
import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
suspend fun TelegramBot.createNewMaskVideoStickerSet(
userId: UserId,
name: String,
title: String,
sticker: FileId,
emojis: String,
maskPosition: MaskPosition
) = execute(
CreateNewMaskVideoStickerSet(userId, name, title, sticker, emojis, maskPosition)
)
suspend fun TelegramBot.createNewMaskVideoStickerSet(
userId: UserId,
name: String,
title: String,
sticker: MultipartFile,
emojis: String,
maskPosition: MaskPosition
) = execute(
CreateNewMaskVideoStickerSet(userId, name, title, sticker, emojis, maskPosition)
)
suspend fun TelegramBot.createNewMaskVideoStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: FileId,
emojis: String,
maskPosition: MaskPosition
) = createNewMaskVideoStickerSet(
user.id, name, title, sticker, emojis, maskPosition
)
suspend fun TelegramBot.createNewMaskVideoStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: MultipartFile,
emojis: String,
maskPosition: MaskPosition
) = createNewMaskVideoStickerSet(
user.id, name, title, sticker, emojis, maskPosition
)

View File

@@ -0,0 +1,50 @@
package dev.inmo.tgbotapi.extensions.api.stickers
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.stickers.*
import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
suspend fun TelegramBot.createNewRegularAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: FileId,
emojis: String
) = execute(
CreateNewRegularAnimatedStickerSet(userId, name, title, sticker, emojis)
)
suspend fun TelegramBot.createNewRegularAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: MultipartFile,
emojis: String
) = execute(
CreateNewRegularAnimatedStickerSet(userId, name, title, sticker, emojis)
)
suspend fun TelegramBot.createNewRegularAnimatedStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: FileId,
emojis: String
) = createNewRegularAnimatedStickerSet(
user.id, name, title, sticker, emojis
)
suspend fun TelegramBot.createNewRegularAnimatedStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: MultipartFile,
emojis: String
) = createNewRegularAnimatedStickerSet(
user.id, name, title, sticker, emojis
)

View File

@@ -0,0 +1,50 @@
package dev.inmo.tgbotapi.extensions.api.stickers
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.stickers.*
import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
suspend fun TelegramBot.createNewRegularStickerSet(
userId: UserId,
name: String,
title: String,
sticker: FileId,
emojis: String
) = execute(
CreateNewRegularStickerSet(userId, name, title, sticker, emojis)
)
suspend fun TelegramBot.createNewRegularStickerSet(
userId: UserId,
name: String,
title: String,
sticker: MultipartFile,
emojis: String
) = execute(
CreateNewRegularStickerSet(userId, name, title, sticker, emojis)
)
suspend fun TelegramBot.createNewRegularStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: FileId,
emojis: String
) = createNewRegularStickerSet(
user.id, name, title, sticker, emojis
)
suspend fun TelegramBot.createNewRegularStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: MultipartFile,
emojis: String
) = createNewRegularStickerSet(
user.id, name, title, sticker, emojis
)

View File

@@ -0,0 +1,50 @@
package dev.inmo.tgbotapi.extensions.api.stickers
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.stickers.*
import dev.inmo.tgbotapi.types.chat.CommonUser
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
suspend fun TelegramBot.createNewRegularVideoStickerSet(
userId: UserId,
name: String,
title: String,
sticker: FileId,
emojis: String
) = execute(
CreateNewRegularVideoStickerSet(userId, name, title, sticker, emojis)
)
suspend fun TelegramBot.createNewRegularVideoStickerSet(
userId: UserId,
name: String,
title: String,
sticker: MultipartFile,
emojis: String
) = execute(
CreateNewRegularVideoStickerSet(userId, name, title, sticker, emojis)
)
suspend fun TelegramBot.createNewRegularVideoStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: FileId,
emojis: String
) = createNewRegularVideoStickerSet(
user.id, name, title, sticker, emojis
)
suspend fun TelegramBot.createNewRegularVideoStickerSet(
user: CommonUser,
name: String,
title: String,
sticker: MultipartFile,
emojis: String
) = createNewRegularVideoStickerSet(
user.id, name, title, sticker, emojis
)

View File

@@ -4,6 +4,7 @@ import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.webhook.SetWebhook import dev.inmo.tgbotapi.requests.webhook.SetWebhook
import dev.inmo.tgbotapi.types.ALL_UPDATES_LIST
/** /**
* Use this method to send information about webhook (like [url]) * Use this method to send information about webhook (like [url])
@@ -12,7 +13,7 @@ suspend fun TelegramBot.setWebhookInfo(
url: String, url: String,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = execute( ) = execute(
@@ -29,7 +30,7 @@ suspend fun TelegramBot.setWebhookInfo(
certificate: FileId, certificate: FileId,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = execute( ) = execute(
@@ -46,7 +47,7 @@ suspend fun TelegramBot.setWebhookInfo(
certificate: MultipartFile, certificate: MultipartFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = execute( ) = execute(

View File

@@ -0,0 +1,119 @@
package dev.inmo.tgbotapi.extensions.behaviour_builder.expectations
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar.doWithRegistration
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.requests.abstracts.Request
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
import dev.inmo.tgbotapi.types.message.textsources.TextSource
import kotlinx.coroutines.flow.*
/**
* Will filter all the messages and include required commands with [commandRegex].
*
* * In case you wish to get only the commands at the start of message, use [requireCommandAtStart]
* * In case you wish to exclude messages with more than one command, you may use [requireSingleCommand]
* * In case you wish to exclude messages with commands params, you may use [requireCommandsWithoutParams]
*/
suspend fun BehaviourContext.waitCommandMessage(
commandRegex: Regex,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null }
) = channelFlow {
triggersHolder.handleableCommandsHolder.doWithRegistration(
commandRegex
) {
waitTextMessage(initRequest, errorFactory).filter {
it.content.textSources.any { it.botCommandTextSourceOrNull() ?.command ?.matches(commandRegex) == true }
}.collect {
send(it)
}
}
}
suspend fun BehaviourContext.waitCommandMessage(
command: String,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null }
) = waitCommandMessage(Regex(command), initRequest, errorFactory)
fun Flow<CommonMessage<TextContent>>.requireCommandAtStart() = filter {
it.content.textSources.firstOrNull() is BotCommandTextSource
}
/**
* Subsequent [Flow] will retrieve only messages with ONE [BotCommandTextSource]. It does not guarantee that this
* [BotCommandTextSource] will be at the start of the message
*
* @see requireCommandAtStart
*/
fun Flow<CommonMessage<TextContent>>.requireSingleCommand() = filter {
var count = 0
it.content.textSources.forEach {
if (it is BotCommandTextSource) {
count++
if (count > 1) {
return@filter false
}
}
}
count == 1
}
/**
* Subsequent [Flow] will retrieve only messages without [TextContent.textSources] which are not [BotCommandTextSource]
*/
fun Flow<CommonMessage<TextContent>>.requireCommandsWithoutParams() = filter {
it.content.textSources.none { it !is BotCommandTextSource }
}
/**
* Map the commands with their arguments and source messages
*/
fun Flow<CommonMessage<TextContent>>.commandsWithParams(): Flow<Pair<CommonMessage<TextContent>, List<Pair<BotCommandTextSource, Array<TextSource>>>>> = mapNotNull {
var currentCommandTextSource: BotCommandTextSource? = null
val currentArgs = mutableListOf<TextSource>()
val result = mutableListOf<Pair<BotCommandTextSource, Array<TextSource>>>()
fun addCurrentCommandToResult() {
currentCommandTextSource ?.let {
result.add(it to currentArgs.toTypedArray())
currentArgs.clear()
}
}
it.content.textSources.forEach {
it.ifBotCommandTextSource {
addCurrentCommandToResult()
currentCommandTextSource = it
return@forEach
}
currentArgs.add(it)
}
addCurrentCommandToResult()
result.toList().takeIf { it.isNotEmpty() } ?.let { result ->
it to result
}
}
/**
* Flat [commandsWithParams]. Each [Pair] of [BotCommandTextSource] and its [Array] of arg text sources will
* be associated with its source message
*/
fun Flow<CommonMessage<TextContent>>.flattenCommandsWithParams() = commandsWithParams().flatMapConcat { (message, commandsWithParams) ->
commandsWithParams.map {
message to it
}.asFlow()
}
/**
* Use [flattenCommandsWithParams] and filter out the commands which do not [matches] to [commandRegex]
*/
fun Flow<CommonMessage<TextContent>>.commandParams(commandRegex: Regex) = flattenCommandsWithParams().filter { (_, commandWithParams) ->
commandWithParams.first.command.matches(commandRegex)
}

View File

@@ -0,0 +1,23 @@
package dev.inmo.tgbotapi.extensions.behaviour_builder.expectations
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.utils.regularTextSourceOrNull
import dev.inmo.tgbotapi.requests.abstracts.Request
import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.RegularTextSource
import kotlinx.coroutines.flow.*
suspend fun BehaviourContext.waitDeepLinks(
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
): Flow<Pair<CommonMessage<TextContent>, String>> = waitCommandMessage(
"start",
initRequest,
errorFactory
)
.requireSingleCommand()
.requireCommandAtStart()
.flattenCommandsWithParams().mapNotNull {
it.first to (it.second.second.singleOrNull() ?.regularTextSourceOrNull() ?.source ?.removePrefix(" ") ?: return@mapNotNull null)
}

View File

@@ -0,0 +1,48 @@
@file:Suppress("unused")
package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling
import dev.inmo.micro_utils.coroutines.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.marker_factories.ByChatMessageMarkerFactory
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.marker_factories.MarkerFactory
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.times
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithParams
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.content.TextMessage
import dev.inmo.tgbotapi.types.message.textsources.RegularTextSource
import dev.inmo.tgbotapi.types.update.abstracts.Update
import io.ktor.http.decodeURLQueryComponent
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
private val startRegex = Regex("start")
suspend fun <BC : BehaviourContext> BC.onDeepLink(
initialFilter: SimpleFilter<Pair<TextMessage, String>>? = null,
subcontextUpdatesFilter: CustomBehaviourContextAndTwoTypesReceiver<BC, Boolean, Pair<TextMessage, String>, Update> = { (message, _), update -> MessageFilterByChat(this, message, update) },
markerFactory: MarkerFactory<Pair<TextMessage, String>, Any> = MarkerFactory { (message, _) -> ByChatMessageMarkerFactory(message) },
scenarioReceiver: CustomBehaviourContextAndTypeReceiver<BC, Unit, Pair<TextMessage, String>>
): Job = on(
markerFactory,
SimpleFilter<Pair<TextMessage, String>> { (message, _) ->
message.content.textSources.size == 2
&& message.content.textSources.firstOrNull() ?.asBotCommandTextSource() ?.command == "start"
&& message.content.textSources.getOrNull(1) is RegularTextSource
} * initialFilter,
subcontextUpdatesFilter,
scenarioReceiver,
) {
(it.messageUpdateOrNull()) ?.data ?.commonMessageOrNull() ?.withContentOrNull<TextContent>() ?.let { message ->
message to message.content.textSources[1].source.removePrefix(" ").decodeURLQueryComponent()
} ?.let(::listOfNotNull)
}.also {
triggersHolder.handleableCommandsHolder.registerHandleable(startRegex)
it.invokeOnCompletion {
this@onDeepLink.launchSafelyWithoutExceptions { triggersHolder.handleableCommandsHolder.unregisterHandleable(startRegex) }
}
}

View File

@@ -4,6 +4,7 @@ fun interface SimpleFilter<in T> {
suspend operator fun invoke(o: T): Boolean suspend operator fun invoke(o: T): Boolean
} }
val TrueSimpleFilter = SimpleFilter<Any?> { true } val TrueSimpleFilter = SimpleFilter<Any?> { true }
val FalseSimpleFilter = SimpleFilter<Any?> { false }
/** /**
* @return [SimpleFilter] which will return true in case when all the items in incoming data passed [this] filter * @return [SimpleFilter] which will return true in case when all the items in incoming data passed [this] filter
@@ -28,21 +29,29 @@ fun <T> SimpleFilter<T>.listNone() = SimpleFilter<Iterable<T>> {
/** /**
* Makes an AND (&&) operation between [this] and [other] * Makes an AND (&&) operation between [this] and [other]
*
* * When both arguments are null, [TrueSimpleFilter] will be returned
*/ */
operator fun <T> SimpleFilter<T>?.times(other: SimpleFilter<T>): SimpleFilter<T> = this ?.let { infix operator fun <T> SimpleFilter<T>?.times(other: SimpleFilter<T>?): SimpleFilter<T> = this ?.let {
SimpleFilter { other ?.let {
this(it) && other(it) SimpleFilter {
} this(it) && other(it)
} ?: other }
} ?: it
} ?: other ?: TrueSimpleFilter
/** /**
* Makes an OR (||) operation between [this] and [other] * Makes an OR (||) operation between [this] and [other]
*
* * When both arguments are null, [TrueSimpleFilter] will be returned
*/ */
operator fun <T> SimpleFilter<T>?.plus(other: SimpleFilter<T>): SimpleFilter<T> = this ?.let { infix operator fun <T> SimpleFilter<T>?.plus(other: SimpleFilter<T>?): SimpleFilter<T> = this ?.let {
SimpleFilter { other ?.let {
this(it) || other(it) SimpleFilter {
} this(it) || other(it)
} ?: other }
} ?: it
} ?: other ?: TrueSimpleFilter
/** /**
* Reverse results of [this] * Reverse results of [this]

View File

@@ -1,5 +1,6 @@
package dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar package dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -7,6 +8,7 @@ open class HandleableTriggersHolder<T>(
preset: List<T> = emptyList() preset: List<T> = emptyList()
) { ) {
protected val commandsMutex = Mutex() protected val commandsMutex = Mutex()
protected val handleableCounts = mutableMapOf<T, Int>()
protected val _handleable = mutableListOf<T>().also { protected val _handleable = mutableListOf<T>().also {
it.addAll(preset) it.addAll(preset)
} }
@@ -16,12 +18,31 @@ open class HandleableTriggersHolder<T>(
suspend fun registerHandleable(data: T) { suspend fun registerHandleable(data: T) {
commandsMutex.withLock { commandsMutex.withLock {
_handleable.add(data) _handleable.add(data)
handleableCounts[data] = (handleableCounts[data] ?: 0) + 1
} }
} }
suspend fun unregisterHandleable(data: T) { suspend fun unregisterHandleable(data: T) {
commandsMutex.withLock { commandsMutex.withLock {
_handleable.remove(data) val newHandleableCount = (handleableCounts[data] ?: 0) - 1
if (newHandleableCount > 0) {
handleableCounts[data] = newHandleableCount
} else {
handleableCounts.remove(data)
_handleable.remove(data)
}
} }
} }
} }
suspend fun <T, R> HandleableTriggersHolder<T>.doWithRegistration(
data: T,
block: suspend () -> R
): R {
registerHandleable(data)
val result = runCatchingSafely {
block()
}
unregisterHandleable(data)
return result.getOrThrow()
}

View File

@@ -35,13 +35,18 @@ fun newRequestException(
} }
} ?: CommonRequestException(response, plainAnswer, message, cause) } ?: CommonRequestException(response, plainAnswer, message, cause)
sealed class BotException(message: String = "Something went wrong", cause: Throwable? = null) : IOException(message, cause)
class CommonBotException(message: String = "Something went wrong", cause: Throwable? = null) : BotException(message, cause)
sealed class RequestException constructor( sealed class RequestException constructor(
val response: Response, val response: Response,
val plainAnswer: String, val plainAnswer: String,
message: String? = null, message: String? = null,
override val cause: Throwable? = null cause: Throwable? = null
) : IOException( ) : BotException(
message ?: "Something went wrong" message ?: "Something went wrong",
cause
) )
class CommonRequestException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) : class CommonRequestException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) :

View File

@@ -1,9 +1,10 @@
package dev.inmo.tgbotapi.bot.ktor package dev.inmo.tgbotapi.bot.ktor
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.safely import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.tgbotapi.bot.BaseRequestsExecutor import dev.inmo.tgbotapi.bot.BaseRequestsExecutor
import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.bot.exceptions.newRequestException import dev.inmo.tgbotapi.bot.exceptions.*
import dev.inmo.tgbotapi.bot.ktor.base.* import dev.inmo.tgbotapi.bot.ktor.base.*
import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter
import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter
@@ -48,12 +49,36 @@ class KtorRequestsExecutor(
} }
override suspend fun <T : Any> execute(request: Request<T>): T { override suspend fun <T : Any> execute(request: Request<T>): T {
return runCatching { return runCatchingSafely {
safely( pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories)
{ e -> requestsLimiter.limit {
pipelineStepsHolder.onRequestException(request, e) ?.let { return@safely it } var result: T? = null
lateinit var factoryHandledRequest: KtorCallFactory
for (potentialFactory in callsFactories) {
pipelineStepsHolder.onBeforeCallFactoryMakeCall(request, potentialFactory)
result = potentialFactory.makeCall(
client,
telegramAPIUrlsKeeper,
request,
jsonFormatter
)
result = pipelineStepsHolder.onAfterCallFactoryMakeCall(result, request, potentialFactory)
if (result != null) {
factoryHandledRequest = potentialFactory
break
}
}
throw if (e is ClientRequestException) { result ?.let {
pipelineStepsHolder.onRequestResultPresented(it, request, factoryHandledRequest, callsFactories)
} ?: pipelineStepsHolder.onRequestResultAbsent(request, callsFactories) ?: error("Can't execute request: $request")
}
}.let {
val result = it.exceptionOrNull() ?.let { e ->
pipelineStepsHolder.onRequestException(request, e) ?.let { return@let it }
if (e is ClientRequestException) {
val exceptionResult = runCatchingSafely {
val content = e.response.bodyAsText() val content = e.response.bodyAsText()
val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content)
newRequestException( newRequestException(
@@ -61,38 +86,15 @@ class KtorRequestsExecutor(
content, content,
"Can't get result object from $content" "Can't get result object from $content"
) )
} else {
e
} }
exceptionResult.exceptionOrNull() ?.let {
CommonBotException(cause = e)
} ?: exceptionResult.getOrThrow()
} else {
CommonBotException(cause = e)
} }
) { } ?.let { Result.failure(it) } ?: it
pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) pipelineStepsHolder.onRequestReturnResult(result, request, callsFactories)
requestsLimiter.limit {
var result: T? = null
lateinit var factoryHandledRequest: KtorCallFactory
for (potentialFactory in callsFactories) {
pipelineStepsHolder.onBeforeCallFactoryMakeCall(request, potentialFactory)
result = potentialFactory.makeCall(
client,
telegramAPIUrlsKeeper,
request,
jsonFormatter
)
result = pipelineStepsHolder.onAfterCallFactoryMakeCall(result, request, potentialFactory)
if (result != null) {
factoryHandledRequest = potentialFactory
break
}
}
result ?.let {
pipelineStepsHolder.onRequestResultPresented(it, request, factoryHandledRequest, callsFactories)
} ?: pipelineStepsHolder.onRequestResultAbsent(request, callsFactories) ?: error("Can't execute request: $request")
}
}
}.let {
pipelineStepsHolder.onRequestReturnResult(it, request, callsFactories)
} }
} }

View File

@@ -0,0 +1,22 @@
package dev.inmo.tgbotapi.requests.get
import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.files.CustomEmojiSticker
import dev.inmo.tgbotapi.types.files.StickerSerializer
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
internal val getCustomEmojiStickersResultSerializer = ListSerializer(StickerSerializer) as DeserializationStrategy<List<CustomEmojiSticker>>
@Serializable
data class GetCustomEmojiStickers(
@SerialName(customEmojiIdsField)
val customEmojiIds: List<CustomEmojiId>
): SimpleRequest<List<CustomEmojiSticker>> {
override fun method(): String = "getCustomEmojiStickers"
override val resultDeserializer: DeserializationStrategy<List<CustomEmojiSticker>>
get() = getCustomEmojiStickersResultSerializer
override val requestSerializer: SerializationStrategy<*>
get() = serializer()
}

View File

@@ -1,13 +1,14 @@
package dev.inmo.tgbotapi.requests.get package dev.inmo.tgbotapi.requests.get
import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest
import dev.inmo.tgbotapi.types.nameField
import dev.inmo.tgbotapi.types.stickerSetNameField import dev.inmo.tgbotapi.types.stickerSetNameField
import dev.inmo.tgbotapi.types.stickers.StickerSet import dev.inmo.tgbotapi.types.stickers.StickerSet
import kotlinx.serialization.* import kotlinx.serialization.*
@Serializable @Serializable
data class GetStickerSet( data class GetStickerSet(
@SerialName(stickerSetNameField) @SerialName(nameField)
val name: String val name: String
): SimpleRequest<StickerSet> { ): SimpleRequest<StickerSet> {
override fun method(): String = "getStickerSet" override fun method(): String = "getStickerSet"

View File

@@ -8,26 +8,8 @@ import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.stickers.MaskPosition import dev.inmo.tgbotapi.types.stickers.MaskPosition
import kotlinx.serialization.* import kotlinx.serialization.*
fun CreateNewAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> {
val data = CreateNewAnimatedStickerSet(userId, name, title, emojis, sticker as? FileId, containsMasks, maskPosition)
return when (sticker) {
is MultipartFile -> CommonMultipartFileRequest(
data,
mapOf(tgsStickerField to sticker)
)
is FileId -> data
}
}
@Serializable @Serializable
@Deprecated("Use CreateNewStickerSet class instead")
data class CreateNewAnimatedStickerSet internal constructor( data class CreateNewAnimatedStickerSet internal constructor(
@SerialName(userIdField) @SerialName(userIdField)
override val userId: UserId, override val userId: UserId,
@@ -40,6 +22,7 @@ data class CreateNewAnimatedStickerSet internal constructor(
@SerialName(tgsStickerField) @SerialName(tgsStickerField)
val sticker: FileId? = null, val sticker: FileId? = null,
@SerialName(containsMasksField) @SerialName(containsMasksField)
@Deprecated("Will be removed soon due to its redundancy")
val containsMasks: Boolean? = null, val containsMasks: Boolean? = null,
@SerialName(maskPositionField) @SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null override val maskPosition: MaskPosition? = null

View File

@@ -8,36 +8,8 @@ import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.stickers.MaskPosition import dev.inmo.tgbotapi.types.stickers.MaskPosition
import kotlinx.serialization.* import kotlinx.serialization.*
fun CreateNewStaticStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> {
val data = CreateNewStaticStickerSet(userId, name, title, emojis, sticker as? FileId, containsMasks, maskPosition)
return when (sticker) {
is MultipartFile -> CommonMultipartFileRequest(
data,
mapOf(pngStickerField to sticker)
)
is FileId -> data
}
}
fun CreateNewStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> = CreateNewStaticStickerSet(userId, name, title, sticker, emojis, containsMasks, maskPosition)
@Serializable @Serializable
@Deprecated("Use CreateNewStickerSet class instead")
data class CreateNewStaticStickerSet internal constructor( data class CreateNewStaticStickerSet internal constructor(
@SerialName(userIdField) @SerialName(userIdField)
override val userId: UserId, override val userId: UserId,
@@ -50,6 +22,7 @@ data class CreateNewStaticStickerSet internal constructor(
@SerialName(pngStickerField) @SerialName(pngStickerField)
val sticker: FileId? = null, val sticker: FileId? = null,
@SerialName(containsMasksField) @SerialName(containsMasksField)
@Deprecated("Will be removed soon due to its redundancy")
val containsMasks: Boolean? = null, val containsMasks: Boolean? = null,
@SerialName(maskPositionField) @SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null override val maskPosition: MaskPosition? = null

View File

@@ -0,0 +1,77 @@
package dev.inmo.tgbotapi.requests.stickers
import dev.inmo.tgbotapi.requests.abstracts.*
import dev.inmo.tgbotapi.requests.common.CommonMultipartFileRequest
import dev.inmo.tgbotapi.requests.stickers.abstracts.CreateStickerSetAction
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.stickers.MaskPosition
import kotlinx.serialization.*
internal fun CreateNewStickerSet(
userId: UserId,
name: String,
title: String,
emojis: String,
stickerType: StickerType = StickerType.Regular,
pngSticker: InputFile? = null,
tgsSticker: InputFile? = null,
webmSticker: InputFile? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> {
val data = CreateNewStickerSet(
userId,
name,
title,
emojis,
stickerType,
pngSticker as? FileId,
tgsSticker as? FileId,
webmSticker as? FileId,
maskPosition
)
return if (pngSticker is MultipartFile || tgsSticker is MultipartFile || webmSticker is MultipartFile) {
CommonMultipartFileRequest(
data,
listOfNotNull(
(pngSticker as? MultipartFile) ?.let { pngStickerField to it },
(tgsSticker as? MultipartFile) ?.let { tgsStickerField to it },
(webmSticker as? MultipartFile) ?.let { webmStickerField to it },
).toMap()
)
} else {
data
}
}
@Serializable
data class CreateNewStickerSet internal constructor(
@SerialName(userIdField)
override val userId: UserId,
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(emojisField)
override val emojis: String,
@SerialName(stickerTypeField)
val stickerType: StickerType = StickerType.Regular,
@SerialName(pngStickerField)
val pngSticker: FileId? = null,
@SerialName(tgsStickerField)
val tgsSticker: FileId? = null,
@SerialName(webmStickerField)
val webmSticker: FileId? = null,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null
) : CreateStickerSetAction {
init {
if(emojis.isEmpty()) {
throw IllegalArgumentException("Emojis must not be empty")
}
}
override val requestSerializer: SerializationStrategy<*>
get() = serializer()
override fun method(): String = "createNewStickerSet"
}

View File

@@ -8,26 +8,8 @@ import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.stickers.MaskPosition import dev.inmo.tgbotapi.types.stickers.MaskPosition
import kotlinx.serialization.* import kotlinx.serialization.*
fun CreateNewVideoStickerSet(
userId: UserId,
linkName: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> {
val data = CreateNewVideoStickerSet(userId, linkName, title, emojis, sticker as? FileId, containsMasks, maskPosition)
return when (sticker) {
is MultipartFile -> CommonMultipartFileRequest(
data,
mapOf(webmStickerField to sticker)
)
is FileId -> data
}
}
@Serializable @Serializable
@Deprecated("Use CreateNewStickerSet class instead")
data class CreateNewVideoStickerSet internal constructor( data class CreateNewVideoStickerSet internal constructor(
@SerialName(userIdField) @SerialName(userIdField)
override val userId: UserId, override val userId: UserId,
@@ -40,6 +22,7 @@ data class CreateNewVideoStickerSet internal constructor(
@SerialName(webmStickerField) @SerialName(webmStickerField)
val sticker: FileId? = null, val sticker: FileId? = null,
@SerialName(containsMasksField) @SerialName(containsMasksField)
@Deprecated("Will be removed soon due to its redundancy")
val containsMasks: Boolean? = null, val containsMasks: Boolean? = null,
@SerialName(maskPositionField) @SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null override val maskPosition: MaskPosition? = null

View File

@@ -0,0 +1,105 @@
package dev.inmo.tgbotapi.requests.stickers
import dev.inmo.tgbotapi.requests.abstracts.InputFile
import dev.inmo.tgbotapi.requests.abstracts.Request
import dev.inmo.tgbotapi.types.StickerType
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
fun CreateNewRegularStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
StickerType.Regular,
pngSticker = sticker
)
fun CreateNewRegularVideoStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
StickerType.Regular,
webmSticker = sticker
)
fun CreateNewRegularAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
StickerType.Regular,
tgsSticker = sticker
)
fun CreateNewMaskStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
maskPosition: MaskPosition
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
StickerType.Mask,
pngSticker = sticker,
maskPosition = maskPosition
)
fun CreateNewMaskVideoStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
maskPosition: MaskPosition
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
StickerType.Mask,
webmSticker = sticker,
maskPosition = maskPosition
)
fun CreateNewMaskAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
maskPosition: MaskPosition
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
StickerType.Mask,
tgsSticker = sticker,
maskPosition = maskPosition
)

View File

@@ -0,0 +1,80 @@
package dev.inmo.tgbotapi.requests.stickers
import dev.inmo.tgbotapi.requests.abstracts.InputFile
import dev.inmo.tgbotapi.requests.abstracts.Request
import dev.inmo.tgbotapi.types.StickerType
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.stickers.MaskPosition
fun CreateNewVideoStickerSet(
userId: UserId,
linkName: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> = CreateNewStickerSet(
userId,
linkName,
title,
emojis,
if (containsMasks == true) StickerType.Mask else StickerType.Regular,
webmSticker = sticker,
maskPosition = maskPosition
)
fun CreateNewStaticStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
if (containsMasks == true) StickerType.Mask else StickerType.Regular,
pngSticker = sticker,
maskPosition = maskPosition
)
fun CreateNewStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
if (containsMasks == true) StickerType.Mask else StickerType.Regular,
pngSticker = sticker,
maskPosition = maskPosition
)
fun CreateNewAnimatedStickerSet(
userId: UserId,
name: String,
title: String,
sticker: InputFile,
emojis: String,
containsMasks: Boolean? = null,
maskPosition: MaskPosition? = null
): Request<Boolean> = CreateNewStickerSet(
userId,
name,
title,
emojis,
if (containsMasks == true) StickerType.Mask else StickerType.Regular,
tgsSticker = sticker,
maskPosition = maskPosition
)

View File

@@ -19,7 +19,7 @@ class MultipartSetWebhookRequest(
certificate: MultipartFile, certificate: MultipartFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) : SetWebhookRequest(), MultipartRequest<Boolean> by MultipartRequestImpl( ) : SetWebhookRequest(), MultipartRequest<Boolean> by MultipartRequestImpl(
@@ -40,7 +40,7 @@ fun SetWebhook(
certificate: MultipartFile, certificate: MultipartFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
): MultipartSetWebhookRequest = MultipartSetWebhookRequest( ): MultipartSetWebhookRequest = MultipartSetWebhookRequest(
@@ -58,7 +58,7 @@ fun SetWebhook(
certificate: FileId, certificate: FileId,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
): SetWebhook = SetWebhook( ): SetWebhook = SetWebhook(
@@ -84,7 +84,7 @@ fun SetWebhook(
certificate: InputFile, certificate: InputFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = when (certificate) { ) = when (certificate) {
@@ -104,7 +104,7 @@ fun SetWebhook(
url: String, url: String,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = SetWebhook( ) = SetWebhook(
@@ -135,7 +135,7 @@ data class SetWebhook internal constructor(
@SerialName(maxAllowedConnectionsField) @SerialName(maxAllowedConnectionsField)
val maxAllowedConnections: Int? = null, val maxAllowedConnections: Int? = null,
@SerialName(allowedUpdatesField) @SerialName(allowedUpdatesField)
val allowedUpdates: List<String>? = null, val allowedUpdates: List<String>? = ALL_UPDATES_LIST,
@SerialName(dropPendingUpdatesField) @SerialName(dropPendingUpdatesField)
val dropPendingUpdates: Boolean? = null, val dropPendingUpdates: Boolean? = null,
@SerialName(secretTokenField) @SerialName(secretTokenField)

View File

@@ -1,6 +1,13 @@
package dev.inmo.tgbotapi.types package dev.inmo.tgbotapi.types
import dev.inmo.tgbotapi.utils.BuiltinMimeTypes import dev.inmo.tgbotapi.utils.BuiltinMimeTypes
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.jvm.JvmInline
typealias Identifier = Long typealias Identifier = Long
typealias MessageIdentifier = Long typealias MessageIdentifier = Long
@@ -28,6 +35,11 @@ typealias GooglePlaceId = String
typealias GooglePlaceType = String typealias GooglePlaceType = String
typealias MembersLimit = Int typealias MembersLimit = Int
typealias WebAppQueryId = String typealias WebAppQueryId = String
@Serializable
@JvmInline
value class CustomEmojiId(
val string: String
)
typealias Seconds = Int typealias Seconds = Int
typealias MilliSeconds = Long typealias MilliSeconds = Long
@@ -37,6 +49,40 @@ typealias UnixTimeStamp = LongSeconds
typealias Meters = Float typealias Meters = Float
typealias Degrees = Int typealias Degrees = Int
@Serializable(StickerType.Serializer::class)
sealed interface StickerType {
val type: String
@Serializable
object Regular : StickerType { override val type: String = "regular" }
@Serializable
object Mask : StickerType { override val type: String = "mask" }
@Serializable
object CustomEmoji : StickerType { override val type: String = "custom_emoji" }
@Serializable
data class Unknown(override val type: String = "custom_emoji") : StickerType
object Serializer : KSerializer<StickerType> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): StickerType {
return when (val type = decoder.decodeString()) {
Regular.type -> Regular
Mask.type -> Mask
CustomEmoji.type -> CustomEmoji
else -> Unknown(type)
}
}
override fun serialize(encoder: Encoder, value: StickerType) {
encoder.encodeString(value.type)
}
}
}
val usernameRegex = Regex("@[\\w\\d_]+")
val degreesLimit = 1 .. 360 val degreesLimit = 1 .. 360
val horizontalAccuracyLimit = 0F .. 1500F val horizontalAccuracyLimit = 0F .. 1500F
@@ -120,12 +166,15 @@ const val languageCodeField = "language_code"
const val addedToAttachmentMenuField = "added_to_attachment_menu" const val addedToAttachmentMenuField = "added_to_attachment_menu"
const val isPremiumField = "is_premium" const val isPremiumField = "is_premium"
const val hasPrivateForwardsField = "has_private_forwards" const val hasPrivateForwardsField = "has_private_forwards"
const val hasRestrictedVoiceAndVideoMessagesField = "has_restricted_voice_and_video_messages"
const val canJoinGroupsField = "can_join_groups" const val canJoinGroupsField = "can_join_groups"
const val canReadAllGroupMessagesField = "can_read_all_group_messages" const val canReadAllGroupMessagesField = "can_read_all_group_messages"
const val supportInlineQueriesField = "supports_inline_queries" const val supportInlineQueriesField = "supports_inline_queries"
const val textEntitiesField = "text_entities" const val textEntitiesField = "text_entities"
const val entitiesField = "entities" const val entitiesField = "entities"
const val stickerSetNameField = "set_name" const val stickerSetNameField = "set_name"
const val customEmojiIdField = "custom_emoji_id"
const val customEmojiIdsField = "custom_emoji_ids"
const val premiumAnimationField = "premium_animation" const val premiumAnimationField = "premium_animation"
const val stickerSetNameFullField = "sticker_set_name" const val stickerSetNameFullField = "sticker_set_name"
const val slowModeDelayField = "slow_mode_delay" const val slowModeDelayField = "slow_mode_delay"
@@ -290,6 +339,7 @@ const val tgsStickerField = "tgs_sticker"
const val webmStickerField = "webm_sticker" const val webmStickerField = "webm_sticker"
const val oldChatMemberField = "old_chat_member" const val oldChatMemberField = "old_chat_member"
const val newChatMemberField = "new_chat_member" const val newChatMemberField = "new_chat_member"
const val stickerTypeField = "sticker_type"
const val okField = "ok" const val okField = "ok"
const val captionField = "caption" const val captionField = "caption"

View File

@@ -62,7 +62,9 @@ data class ExtendedPrivateChatImpl(
@SerialName(bioField) @SerialName(bioField)
override val bio: String = "", override val bio: String = "",
@SerialName(hasPrivateForwardsField) @SerialName(hasPrivateForwardsField)
override val hasPrivateForwards: Boolean = false override val hasPrivateForwards: Boolean = false,
@SerialName(hasRestrictedVoiceAndVideoMessagesField)
override val hasRestrictedVoiceAndVideoMessages: Boolean = false
) : ExtendedPrivateChat ) : ExtendedPrivateChat
typealias ExtendedUser = ExtendedPrivateChatImpl typealias ExtendedUser = ExtendedPrivateChatImpl

View File

@@ -19,6 +19,7 @@ sealed interface ExtendedGroupChat : GroupChat, ExtendedPublicChat {
sealed interface ExtendedPrivateChat : PrivateChat, ExtendedChat { sealed interface ExtendedPrivateChat : PrivateChat, ExtendedChat {
val bio: String val bio: String
val hasPrivateForwards: Boolean val hasPrivateForwards: Boolean
val hasRestrictedVoiceAndVideoMessages: Boolean
val allowCreateUserIdLink: Boolean val allowCreateUserIdLink: Boolean
get() = hasPrivateForwards get() = hasPrivateForwards

View File

@@ -4,16 +4,19 @@ import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.stickers.MaskPosition import dev.inmo.tgbotapi.types.stickers.MaskPosition
import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.nonstrictJsonFormat
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonElement
@Serializable @Serializable
@RiskFeature("This class is used for serialization/deserialization of Sticker interface") @RiskFeature("This class is used for serialization/deserialization of Sticker interface")
data class StickerSurrogate( data class StickerSurrogate(
val file_id: FileId, val file_id: FileId,
val file_unique_id: FileUniqueId, val file_unique_id: FileUniqueId,
val type: StickerType,
val width: Int, val width: Int,
val height: Int, val height: Int,
val is_animated: Boolean? = null, val is_animated: Boolean? = null,
@@ -21,33 +24,138 @@ data class StickerSurrogate(
val thumb: PhotoSize? = null, val thumb: PhotoSize? = null,
val emoji: String? = null, val emoji: String? = null,
val set_name: StickerSetName? = null, val set_name: StickerSetName? = null,
val premium_animation: File? = null,
val mask_position: MaskPosition? = null, val mask_position: MaskPosition? = null,
val file_size: Long? = null, val custom_emoji_id: CustomEmojiId? = null,
val premium_animation: File? = null val file_size: Long? = null
) )
// TODO:: Serializer // TODO:: Serializer
@Serializable(StickerSerializer::class) @Serializable(StickerSerializer::class)
sealed interface Sticker : TelegramMediaFile, SizedMediaFile, ThumbedMediaFile { sealed interface Sticker : TelegramMediaFile, SizedMediaFile, ThumbedMediaFile {
val emoji: String? val emoji: String?
val maskPosition: MaskPosition?
val stickerSetName: StickerSetName? val stickerSetName: StickerSetName?
val premiumAnimationFile: File?
val isAnimated val isAnimated
get() = this is AnimatedSticker get() = false
val isVideo val isVideo
get() = this is VideoSticker get() = false
} }
object StickerSerializer : KSerializer<Sticker> { object StickerSerializer : KSerializer<Sticker> {
override val descriptor: SerialDescriptor = StickerSurrogate.serializer().descriptor override val descriptor: SerialDescriptor = StickerSurrogate.serializer().descriptor
override fun deserialize(decoder: Decoder): Sticker { override fun deserialize(decoder: Decoder): Sticker {
val surrogate = StickerSurrogate.serializer().deserialize(decoder) val json = JsonElement.serializer().deserialize(decoder)
val surrogate = nonstrictJsonFormat.decodeFromJsonElement(StickerSurrogate.serializer(), json)
return when { return when (surrogate.type) {
surrogate.is_animated == true -> AnimatedSticker( StickerType.Regular -> when {
surrogate.is_animated == true -> RegularAnimatedSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.premium_animation,
surrogate.file_size
)
surrogate.is_video == true -> RegularVideoSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.premium_animation,
surrogate.file_size
)
else -> RegularSimpleSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.premium_animation,
surrogate.file_size
)
}
StickerType.Mask -> when {
surrogate.is_animated == true -> MaskAnimatedSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.mask_position ?: error("For mask stickers field mask_position should be presented"),
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.file_size
)
surrogate.is_video == true -> MaskVideoSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.mask_position ?: error("For mask stickers field mask_position should be presented"),
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.file_size
)
else -> MaskSimpleSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.mask_position ?: error("For mask stickers field mask_position should be presented"),
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.file_size
)
}
StickerType.CustomEmoji -> when {
surrogate.is_animated == true -> CustomEmojiAnimatedSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.custom_emoji_id ?: error("For mask stickers field mask_position should be presented"),
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.file_size
)
surrogate.is_video == true -> CustomEmojiVideoSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.custom_emoji_id ?: error("For mask stickers field mask_position should be presented"),
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.file_size
)
else -> CustomEmojiSimpleSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.custom_emoji_id ?: error("For mask stickers field mask_position should be presented"),
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.file_size
)
}
is StickerType.Unknown -> UnknownSticker(
surrogate.file_id, surrogate.file_id,
surrogate.file_unique_id, surrogate.file_unique_id,
surrogate.width, surrogate.width,
@@ -55,33 +163,8 @@ object StickerSerializer : KSerializer<Sticker> {
surrogate.thumb, surrogate.thumb,
surrogate.emoji, surrogate.emoji,
surrogate.set_name, surrogate.set_name,
surrogate.premium_animation, surrogate.file_size,
surrogate.mask_position, json
surrogate.file_size
)
surrogate.is_video == true -> VideoSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.premium_animation,
surrogate.mask_position,
surrogate.file_size
)
else -> SimpleSticker(
surrogate.file_id,
surrogate.file_unique_id,
surrogate.width,
surrogate.height,
surrogate.thumb,
surrogate.emoji,
surrogate.set_name,
surrogate.premium_animation,
surrogate.mask_position,
surrogate.file_size
) )
} }
} }
@@ -93,53 +176,23 @@ object StickerSerializer : KSerializer<Sticker> {
} }
@Serializable @Serializable
data class SimpleSticker( sealed interface VideoSticker : Sticker {
@SerialName(fileIdField) override val isVideo: Boolean
override val fileId: FileId, get() = true
@SerialName(fileUniqueIdField) }
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(premiumAnimationField)
override val premiumAnimationFile: File?,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : Sticker
@Serializable @Serializable
data class AnimatedSticker( sealed interface AnimatedSticker : Sticker {
@SerialName(fileIdField) override val isAnimated: Boolean
override val fileId: FileId, get() = true
@SerialName(fileUniqueIdField) }
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(premiumAnimationField)
override val premiumAnimationFile: File?,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : Sticker
@Serializable @Serializable
data class VideoSticker( sealed interface RegularSticker : Sticker {
val premiumAnimationFile: File?
}
@Serializable
data class RegularSimpleSticker(
@SerialName(fileIdField) @SerialName(fileIdField)
override val fileId: FileId, override val fileId: FileId,
@SerialName(fileUniqueIdField) @SerialName(fileUniqueIdField)
@@ -156,8 +209,209 @@ data class VideoSticker(
override val stickerSetName: StickerSetName? = null, override val stickerSetName: StickerSetName? = null,
@SerialName(premiumAnimationField) @SerialName(premiumAnimationField)
override val premiumAnimationFile: File?, override val premiumAnimationFile: File?,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition? = null,
@SerialName(fileSizeField) @SerialName(fileSizeField)
override val fileSize: Long? = null, override val fileSize: Long? = null,
) : RegularSticker
@Deprecated("Renamed", ReplaceWith("SimpleRegularSticker", "dev.inmo.tgbotapi.types.files.SimpleRegularSticker"))
typealias SimpleSticker = RegularSimpleSticker
@Serializable
data class RegularAnimatedSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(premiumAnimationField)
override val premiumAnimationFile: File?,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : RegularSticker, AnimatedSticker
@Serializable
data class RegularVideoSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(premiumAnimationField)
override val premiumAnimationFile: File?,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : RegularSticker, VideoSticker
@Serializable
sealed interface MaskSticker : Sticker {
val maskPosition: MaskPosition
}
@Serializable
data class MaskSimpleSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : MaskSticker
@Serializable
data class MaskAnimatedSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : MaskSticker, AnimatedSticker
@Serializable
data class MaskVideoSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(maskPositionField)
override val maskPosition: MaskPosition,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : MaskSticker, VideoSticker
@Serializable
sealed interface CustomEmojiSticker : Sticker {
val customEmojiId: CustomEmojiId
}
@Serializable
data class CustomEmojiSimpleSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(customEmojiIdField)
override val customEmojiId: CustomEmojiId,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : CustomEmojiSticker
@Serializable
data class CustomEmojiAnimatedSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(customEmojiIdField)
override val customEmojiId: CustomEmojiId,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : CustomEmojiSticker, AnimatedSticker
@Serializable
data class CustomEmojiVideoSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(customEmojiIdField)
override val customEmojiId: CustomEmojiId,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
) : CustomEmojiSticker, VideoSticker
@Serializable
data class UnknownSticker(
@SerialName(fileIdField)
override val fileId: FileId,
@SerialName(fileUniqueIdField)
override val fileUniqueId: FileUniqueId,
@SerialName(widthField)
override val width: Int,
@SerialName(heightField)
override val height: Int,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
@SerialName(emojiField)
override val emoji: String? = null,
@SerialName(stickerSetNameField)
override val stickerSetName: StickerSetName? = null,
@SerialName(fileSizeField)
override val fileSize: Long? = null,
val raw: JsonElement
) : Sticker ) : Sticker

View File

@@ -1,5 +1,6 @@
package dev.inmo.tgbotapi.types.message package dev.inmo.tgbotapi.types.message
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.chat.User import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.types.message.textsources.* import dev.inmo.tgbotapi.types.message.textsources.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -11,7 +12,8 @@ internal data class RawMessageEntity(
val length: Int, val length: Int,
val url: String? = null, val url: String? = null,
val user: User? = null, val user: User? = null,
val language: String? = null val language: String? = null,
val custom_emoji_id: CustomEmojiId? = null
) { ) {
internal val range by lazy { internal val range by lazy {
offset until (offset + length) offset until (offset + length)
@@ -50,6 +52,7 @@ internal fun RawMessageEntity.asTextSource(
"underline" -> UnderlineTextSource(sourceSubstring, subPartsWithRegulars) "underline" -> UnderlineTextSource(sourceSubstring, subPartsWithRegulars)
"strikethrough" -> StrikethroughTextSource(sourceSubstring, subPartsWithRegulars) "strikethrough" -> StrikethroughTextSource(sourceSubstring, subPartsWithRegulars)
"spoiler" -> SpoilerTextSource(sourceSubstring, subPartsWithRegulars) "spoiler" -> SpoilerTextSource(sourceSubstring, subPartsWithRegulars)
"custom_emoji" -> CustomEmojiTextSource(sourceSubstring, custom_emoji_id ?: error("For custom emoji custom_emoji_id should exists"), subPartsWithRegulars)
else -> RegularTextSource(sourceSubstring) else -> RegularTextSource(sourceSubstring)
} }
} }
@@ -158,7 +161,8 @@ internal fun TextSource.toRawMessageEntities(offset: Int = 0): List<RawMessageEn
is UnderlineTextSource -> RawMessageEntity("underline", offset, length) is UnderlineTextSource -> RawMessageEntity("underline", offset, length)
is StrikethroughTextSource -> RawMessageEntity("strikethrough", offset, length) is StrikethroughTextSource -> RawMessageEntity("strikethrough", offset, length)
is SpoilerTextSource -> RawMessageEntity("spoiler", offset, length) is SpoilerTextSource -> RawMessageEntity("spoiler", offset, length)
else -> null is CustomEmojiTextSource -> RawMessageEntity("custom_emoji", offset, length, custom_emoji_id = customEmojiId)
is RegularTextSource -> null
} }
) + if (this is MultilevelTextSource) { ) + if (this is MultilevelTextSource) {
subsources.toRawMessageEntities(offset) subsources.toRawMessageEntities(offset)

View File

@@ -1,5 +1,7 @@
package dev.inmo.tgbotapi.types.message.textsources package dev.inmo.tgbotapi.types.message.textsources
import dev.inmo.tgbotapi.types.usernameRegex
import dev.inmo.tgbotapi.types.Username
import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.internal.* import dev.inmo.tgbotapi.utils.internal.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -16,6 +18,9 @@ data class BotCommandTextSource @RiskFeature(DirectInvocationOfTextSourceConstru
val command: String by lazy { val command: String by lazy {
commandRegex.find(source) ?.value ?.substring(1) ?: source.substring(1)// skip first symbol like "/" or "!" commandRegex.find(source) ?.value ?.substring(1) ?: source.substring(1)// skip first symbol like "/" or "!"
} }
val username: Username? by lazy {
Username(usernameRegex.find(source) ?.value ?: return@lazy null)
}
override val markdown: String by lazy { source.commandMarkdown() } override val markdown: String by lazy { source.commandMarkdown() }
override val markdownV2: String by lazy { source.commandMarkdownV2() } override val markdownV2: String by lazy { source.commandMarkdownV2() }

View File

@@ -0,0 +1,31 @@
package dev.inmo.tgbotapi.types.message.textsources
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.utils.RiskFeature
import dev.inmo.tgbotapi.utils.extensions.makeString
import dev.inmo.tgbotapi.utils.internal.*
import kotlinx.serialization.Serializable
/**
* @see customEmoji
*/
@Serializable
data class CustomEmojiTextSource @RiskFeature(DirectInvocationOfTextSourceConstructor) constructor (
override val source: String,
val customEmojiId: CustomEmojiId,
override val subsources: TextSourcesList
) : MultilevelTextSource {
override val markdown: String by lazy { source.customEmojiMarkdown() }
override val markdownV2: String by lazy { source.customEmojiMarkdownV2() }
override val html: String by lazy { source.customEmojiHTML() }
}
@Suppress("NOTHING_TO_INLINE", "EXPERIMENTAL_API_USAGE")
inline fun customEmoji(emojiId: CustomEmojiId, parts: TextSourcesList) = CustomEmojiTextSource(parts.makeString(), emojiId, parts)
@Suppress("NOTHING_TO_INLINE")
inline fun customEmoji(emojiId: CustomEmojiId, vararg parts: TextSource) = customEmoji(emojiId, parts.toList())
/**
* Without sharp (#)
*/
@Suppress("NOTHING_TO_INLINE")
inline fun customEmoji(emojiId: CustomEmojiId, text: String) = customEmoji(emojiId, regular(text))

View File

@@ -36,7 +36,7 @@ sealed interface MultilevelTextSource : TextSource {
val subsources: List<TextSource> val subsources: List<TextSource>
} }
fun List<TextSource>.separateForMessage(limit: IntRange, numberOfParts: Int? = null): List<List<TextSource>> { fun List<TextSource>.splitForMessage(limit: IntRange, numberOfParts: Int? = null): List<List<TextSource>> {
if (isEmpty()) { if (isEmpty()) {
return emptyList() return emptyList()
} }
@@ -70,13 +70,27 @@ fun List<TextSource>.separateForMessage(limit: IntRange, numberOfParts: Int? = n
* This method will prepare [TextSource]s list for messages. Remember, that first part will be separated with * This method will prepare [TextSource]s list for messages. Remember, that first part will be separated with
* [captionLength] and all others with * [captionLength] and all others with
*/ */
fun List<TextSource>.separateForCaption(): List<List<TextSource>> { fun List<TextSource>.splitForCaption(): List<List<TextSource>> {
val captionPart = separateForMessage(captionLength, 1).first() val captionPart = splitForMessage(captionLength, 1).first()
return listOf(captionPart) + minus(captionPart).separateForMessage(textLength) return listOf(captionPart) + minus(captionPart).splitForMessage(textLength)
} }
/** /**
* This method will prepare [TextSource]s list for messages with [textLength] * This method will prepare [TextSource]s list for messages with [textLength]
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun List<TextSource>.separateForText(): List<List<TextSource>> = separateForMessage(textLength) inline fun List<TextSource>.splitForText(): List<List<TextSource>> = splitForMessage(textLength)
fun List<TextSource>.separateForMessage(limit: IntRange, numberOfParts: Int? = null): List<List<TextSource>> = splitForMessage(limit, numberOfParts)
/**
* This method will prepare [TextSource]s list for messages. Remember, that first part will be separated with
* [captionLength] and all others with
*/
fun List<TextSource>.separateForCaption(): List<List<TextSource>> = splitForCaption()
/**
* This method will prepare [TextSource]s list for messages with [textLength]
*/
@Suppress("NOTHING_TO_INLINE")
inline fun List<TextSource>.separateForText(): List<List<TextSource>> = splitForText()

View File

@@ -21,6 +21,7 @@ private val baseSerializers: Map<String, KSerializer<out TextSource>> = mapOf(
"hashtag" to HashTagTextSource.serializer(), "hashtag" to HashTagTextSource.serializer(),
"cashtag" to CashTagTextSource.serializer(), "cashtag" to CashTagTextSource.serializer(),
"spoiler" to SpoilerTextSource.serializer(), "spoiler" to SpoilerTextSource.serializer(),
"custom_emoji" to CustomEmojiTextSource.serializer(),
) )
object TextSourceSerializer : TypedSerializer<TextSource>(TextSource::class, baseSerializers) { object TextSourceSerializer : TypedSerializer<TextSource>(TextSource::class, baseSerializers) {

View File

@@ -1,25 +1,298 @@
package dev.inmo.tgbotapi.types.stickers package dev.inmo.tgbotapi.types.stickers
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.files.PhotoSize import dev.inmo.tgbotapi.types.files.*
import dev.inmo.tgbotapi.types.files.Sticker import dev.inmo.tgbotapi.utils.nonstrictJsonFormat
import kotlinx.serialization.SerialName import kotlinx.serialization.*
import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@Serializable @Serializable
data class StickerSet( data class SurrogateStickerSet(
@SerialName(nameField)
val name: String, val name: String,
@SerialName(titleField)
val title: String, val title: String,
@SerialName(stickersField) val sticker_type: StickerType,
val stickers: List<Sticker>, val is_animated: Boolean? = false,
@SerialName(isAnimatedField) val is_video: Boolean? = false,
val isAnimated: Boolean = false, val stickers: List<@Serializable(StickerSerializer::class) Sticker> = emptyList(),
@SerialName(isVideoField)
val isVideo: Boolean = false,
@SerialName(containsMasksField)
val containsMasks: Boolean = false,
@SerialName(thumbField)
val thumb: PhotoSize? = null val thumb: PhotoSize? = null
) )
@Serializable(StickerSet.Serializer::class)
sealed interface StickerSet {
val name: String
val title: String
val stickerType: StickerType
val stickers: List<Sticker>
val isAnimated: Boolean
get() = false
val isVideo: Boolean
get() = false
val thumb: PhotoSize?
@Deprecated("Will be removed soon due to its redundancy")
val containsMasks: Boolean
get() = this is MaskStickerSet
object Serializer : KSerializer<StickerSet> {
override val descriptor: SerialDescriptor = JsonElement.serializer().descriptor
override fun deserialize(decoder: Decoder): StickerSet {
val json = JsonElement.serializer().deserialize(decoder)
val surrogate = nonstrictJsonFormat.decodeFromJsonElement(SurrogateStickerSet.serializer(), json)
return when (surrogate.sticker_type) {
StickerType.CustomEmoji -> when {
surrogate.is_animated == true -> CustomEmojiAnimatedStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<CustomEmojiAnimatedSticker>(),
surrogate.thumb
)
surrogate.is_video == true -> CustomEmojiVideoStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<CustomEmojiVideoSticker>(),
surrogate.thumb
)
else -> CustomEmojiSimpleStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<CustomEmojiSimpleSticker>(),
surrogate.thumb
)
}
StickerType.Mask -> when {
surrogate.is_animated == true -> MaskAnimatedStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<MaskAnimatedSticker>(),
surrogate.thumb
)
surrogate.is_video == true -> MaskVideoStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<MaskVideoSticker>(),
surrogate.thumb
)
else -> MaskSimpleStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<MaskSimpleSticker>(),
surrogate.thumb
)
}
StickerType.Regular -> when {
surrogate.is_animated == true -> RegularAnimatedStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<RegularAnimatedSticker>(),
surrogate.thumb
)
surrogate.is_video == true -> RegularVideoStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<RegularVideoSticker>(),
surrogate.thumb
)
else -> RegularSimpleStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<RegularSimpleSticker>(),
surrogate.thumb
)
}
is StickerType.Unknown -> UnknownStickerSet(
surrogate.name,
surrogate.title,
surrogate.stickers.filterIsInstance<RegularSimpleSticker>(),
surrogate.sticker_type,
surrogate.thumb,
json
)
}
}
override fun serialize(encoder: Encoder, value: StickerSet) {
TODO("Not yet implemented")
}
}
}
@Serializable
sealed interface AnimatedStickerSet : StickerSet {
override val isAnimated: Boolean
get() = true
}
@Serializable
sealed interface VideoStickerSet : StickerSet {
override val isVideo: Boolean
get() = true
}
@Serializable
sealed interface RegularStickerSet : StickerSet
@Serializable
sealed interface MaskStickerSet : StickerSet
@Serializable
sealed interface CustomEmojiStickerSet : StickerSet
@Serializable
data class RegularSimpleStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<RegularSimpleSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : RegularStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.Regular
}
@Serializable
data class RegularAnimatedStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<RegularAnimatedSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : RegularStickerSet, AnimatedStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.Regular
}
@Serializable
data class RegularVideoStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<RegularVideoSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : RegularStickerSet, VideoStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.Regular
}
@Serializable
data class MaskSimpleStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<MaskSimpleSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : MaskStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.Mask
}
@Serializable
data class MaskAnimatedStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<MaskAnimatedSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : MaskStickerSet, AnimatedStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.Mask
}
@Serializable
data class MaskVideoStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<MaskVideoSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : MaskStickerSet, VideoStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.Mask
}
@Serializable
data class CustomEmojiSimpleStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<CustomEmojiSimpleSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : CustomEmojiStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.CustomEmoji
}
@Serializable
data class CustomEmojiAnimatedStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<CustomEmojiAnimatedSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : CustomEmojiStickerSet, AnimatedStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.CustomEmoji
}
@Serializable
data class CustomEmojiVideoStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<CustomEmojiVideoSticker>,
@SerialName(thumbField)
override val thumb: PhotoSize? = null
) : CustomEmojiStickerSet, VideoStickerSet {
@SerialName(stickerTypeField)
@EncodeDefault
override val stickerType: StickerType = StickerType.CustomEmoji
}
@Serializable
data class UnknownStickerSet(
@SerialName(nameField)
override val name: String,
@SerialName(titleField)
override val title: String,
@SerialName(stickersField)
override val stickers: List<Sticker>,
@SerialName(stickerTypeField)
override val stickerType: StickerType,
@SerialName(thumbField)
override val thumb: PhotoSize? = null,
val raw: JsonElement
) : CustomEmojiStickerSet, VideoStickerSet

View File

@@ -119,6 +119,10 @@ internal fun String.commandMarkdown(): String = command(String::toMarkdown)
internal fun String.commandMarkdownV2(): String = command(String::escapeMarkdownV2Common) internal fun String.commandMarkdownV2(): String = command(String::escapeMarkdownV2Common)
internal fun String.commandHTML(): String = command(String::toHtml) internal fun String.commandHTML(): String = command(String::toHtml)
internal fun String.customEmojiMarkdown(): String = toMarkdown()
internal fun String.customEmojiMarkdownV2(): String = escapeMarkdownV2Common()
internal fun String.customEmojiHTML(): String = toHtml()
internal fun String.regularMarkdown(): String = toMarkdown() internal fun String.regularMarkdown(): String = toMarkdown()
internal fun String.regularMarkdownV2(): String = escapeMarkdownV2Common() internal fun String.regularMarkdownV2(): String = escapeMarkdownV2Common()
internal fun String.regularHtml(): String = toHtml() internal fun String.regularHtml(): String = toHtml()

View File

@@ -167,18 +167,30 @@ import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType
import dev.inmo.tgbotapi.types.files.AnimatedSticker import dev.inmo.tgbotapi.types.files.AnimatedSticker
import dev.inmo.tgbotapi.types.files.AnimationFile import dev.inmo.tgbotapi.types.files.AnimationFile
import dev.inmo.tgbotapi.types.files.AudioFile import dev.inmo.tgbotapi.types.files.AudioFile
import dev.inmo.tgbotapi.types.files.CustomEmojiAnimatedSticker
import dev.inmo.tgbotapi.types.files.CustomEmojiSimpleSticker
import dev.inmo.tgbotapi.types.files.CustomEmojiSticker
import dev.inmo.tgbotapi.types.files.CustomEmojiVideoSticker
import dev.inmo.tgbotapi.types.files.DocumentFile import dev.inmo.tgbotapi.types.files.DocumentFile
import dev.inmo.tgbotapi.types.files.File import dev.inmo.tgbotapi.types.files.File
import dev.inmo.tgbotapi.types.files.MaskAnimatedSticker
import dev.inmo.tgbotapi.types.files.MaskSimpleSticker
import dev.inmo.tgbotapi.types.files.MaskSticker
import dev.inmo.tgbotapi.types.files.MaskVideoSticker
import dev.inmo.tgbotapi.types.files.MimedMediaFile import dev.inmo.tgbotapi.types.files.MimedMediaFile
import dev.inmo.tgbotapi.types.files.PassportFile import dev.inmo.tgbotapi.types.files.PassportFile
import dev.inmo.tgbotapi.types.files.PathedFile import dev.inmo.tgbotapi.types.files.PathedFile
import dev.inmo.tgbotapi.types.files.PhotoSize import dev.inmo.tgbotapi.types.files.PhotoSize
import dev.inmo.tgbotapi.types.files.PlayableMediaFile import dev.inmo.tgbotapi.types.files.PlayableMediaFile
import dev.inmo.tgbotapi.types.files.SimpleSticker import dev.inmo.tgbotapi.types.files.RegularAnimatedSticker
import dev.inmo.tgbotapi.types.files.RegularSimpleSticker
import dev.inmo.tgbotapi.types.files.RegularSticker
import dev.inmo.tgbotapi.types.files.RegularVideoSticker
import dev.inmo.tgbotapi.types.files.SizedMediaFile import dev.inmo.tgbotapi.types.files.SizedMediaFile
import dev.inmo.tgbotapi.types.files.Sticker import dev.inmo.tgbotapi.types.files.Sticker
import dev.inmo.tgbotapi.types.files.TelegramMediaFile import dev.inmo.tgbotapi.types.files.TelegramMediaFile
import dev.inmo.tgbotapi.types.files.ThumbedMediaFile import dev.inmo.tgbotapi.types.files.ThumbedMediaFile
import dev.inmo.tgbotapi.types.files.UnknownSticker
import dev.inmo.tgbotapi.types.files.VideoFile import dev.inmo.tgbotapi.types.files.VideoFile
import dev.inmo.tgbotapi.types.files.VideoNoteFile import dev.inmo.tgbotapi.types.files.VideoNoteFile
import dev.inmo.tgbotapi.types.files.VideoSticker import dev.inmo.tgbotapi.types.files.VideoSticker
@@ -295,6 +307,7 @@ import dev.inmo.tgbotapi.types.message.textsources.BoldTextSource
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
import dev.inmo.tgbotapi.types.message.textsources.CashTagTextSource import dev.inmo.tgbotapi.types.message.textsources.CashTagTextSource
import dev.inmo.tgbotapi.types.message.textsources.CodeTextSource import dev.inmo.tgbotapi.types.message.textsources.CodeTextSource
import dev.inmo.tgbotapi.types.message.textsources.CustomEmojiTextSource
import dev.inmo.tgbotapi.types.message.textsources.EMailTextSource import dev.inmo.tgbotapi.types.message.textsources.EMailTextSource
import dev.inmo.tgbotapi.types.message.textsources.HashTagTextSource import dev.inmo.tgbotapi.types.message.textsources.HashTagTextSource
import dev.inmo.tgbotapi.types.message.textsources.ItalicTextSource import dev.inmo.tgbotapi.types.message.textsources.ItalicTextSource
@@ -2266,14 +2279,14 @@ public inline fun TelegramMediaFile.stickerOrThrow(): Sticker = this as
public inline fun <T> TelegramMediaFile.ifSticker(block: (Sticker) -> T): T? = stickerOrNull() public inline fun <T> TelegramMediaFile.ifSticker(block: (Sticker) -> T): T? = stickerOrNull()
?.let(block) ?.let(block)
public inline fun TelegramMediaFile.simpleStickerOrNull(): SimpleSticker? = this as? public inline fun TelegramMediaFile.videoStickerOrNull(): VideoSticker? = this as?
dev.inmo.tgbotapi.types.files.SimpleSticker dev.inmo.tgbotapi.types.files.VideoSticker
public inline fun TelegramMediaFile.simpleStickerOrThrow(): SimpleSticker = this as public inline fun TelegramMediaFile.videoStickerOrThrow(): VideoSticker = this as
dev.inmo.tgbotapi.types.files.SimpleSticker dev.inmo.tgbotapi.types.files.VideoSticker
public inline fun <T> TelegramMediaFile.ifSimpleSticker(block: (SimpleSticker) -> T): T? = public inline fun <T> TelegramMediaFile.ifVideoSticker(block: (VideoSticker) -> T): T? =
simpleStickerOrNull() ?.let(block) videoStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.animatedStickerOrNull(): AnimatedSticker? = this as? public inline fun TelegramMediaFile.animatedStickerOrNull(): AnimatedSticker? = this as?
dev.inmo.tgbotapi.types.files.AnimatedSticker dev.inmo.tgbotapi.types.files.AnimatedSticker
@@ -2284,14 +2297,126 @@ public inline fun TelegramMediaFile.animatedStickerOrThrow(): AnimatedSticker =
public inline fun <T> TelegramMediaFile.ifAnimatedSticker(block: (AnimatedSticker) -> T): T? = public inline fun <T> TelegramMediaFile.ifAnimatedSticker(block: (AnimatedSticker) -> T): T? =
animatedStickerOrNull() ?.let(block) animatedStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.videoStickerOrNull(): VideoSticker? = this as? public inline fun TelegramMediaFile.regularStickerOrNull(): RegularSticker? = this as?
dev.inmo.tgbotapi.types.files.VideoSticker dev.inmo.tgbotapi.types.files.RegularSticker
public inline fun TelegramMediaFile.videoStickerOrThrow(): VideoSticker = this as public inline fun TelegramMediaFile.regularStickerOrThrow(): RegularSticker = this as
dev.inmo.tgbotapi.types.files.VideoSticker dev.inmo.tgbotapi.types.files.RegularSticker
public inline fun <T> TelegramMediaFile.ifVideoSticker(block: (VideoSticker) -> T): T? = public inline fun <T> TelegramMediaFile.ifRegularSticker(block: (RegularSticker) -> T): T? =
videoStickerOrNull() ?.let(block) regularStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.regularSimpleStickerOrNull(): RegularSimpleSticker? = this as?
dev.inmo.tgbotapi.types.files.RegularSimpleSticker
public inline fun TelegramMediaFile.regularSimpleStickerOrThrow(): RegularSimpleSticker = this as
dev.inmo.tgbotapi.types.files.RegularSimpleSticker
public inline fun <T> TelegramMediaFile.ifRegularSimpleSticker(block: (RegularSimpleSticker) -> T):
T? = regularSimpleStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.regularAnimatedStickerOrNull(): RegularAnimatedSticker? = this
as? dev.inmo.tgbotapi.types.files.RegularAnimatedSticker
public inline fun TelegramMediaFile.regularAnimatedStickerOrThrow(): RegularAnimatedSticker = this
as dev.inmo.tgbotapi.types.files.RegularAnimatedSticker
public inline fun <T>
TelegramMediaFile.ifRegularAnimatedSticker(block: (RegularAnimatedSticker) -> T): T? =
regularAnimatedStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.regularVideoStickerOrNull(): RegularVideoSticker? = this as?
dev.inmo.tgbotapi.types.files.RegularVideoSticker
public inline fun TelegramMediaFile.regularVideoStickerOrThrow(): RegularVideoSticker = this as
dev.inmo.tgbotapi.types.files.RegularVideoSticker
public inline fun <T> TelegramMediaFile.ifRegularVideoSticker(block: (RegularVideoSticker) -> T): T?
= regularVideoStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.maskStickerOrNull(): MaskSticker? = this as?
dev.inmo.tgbotapi.types.files.MaskSticker
public inline fun TelegramMediaFile.maskStickerOrThrow(): MaskSticker = this as
dev.inmo.tgbotapi.types.files.MaskSticker
public inline fun <T> TelegramMediaFile.ifMaskSticker(block: (MaskSticker) -> T): T? =
maskStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.maskSimpleStickerOrNull(): MaskSimpleSticker? = this as?
dev.inmo.tgbotapi.types.files.MaskSimpleSticker
public inline fun TelegramMediaFile.maskSimpleStickerOrThrow(): MaskSimpleSticker = this as
dev.inmo.tgbotapi.types.files.MaskSimpleSticker
public inline fun <T> TelegramMediaFile.ifMaskSimpleSticker(block: (MaskSimpleSticker) -> T): T? =
maskSimpleStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.maskAnimatedStickerOrNull(): MaskAnimatedSticker? = this as?
dev.inmo.tgbotapi.types.files.MaskAnimatedSticker
public inline fun TelegramMediaFile.maskAnimatedStickerOrThrow(): MaskAnimatedSticker = this as
dev.inmo.tgbotapi.types.files.MaskAnimatedSticker
public inline fun <T> TelegramMediaFile.ifMaskAnimatedSticker(block: (MaskAnimatedSticker) -> T): T?
= maskAnimatedStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.maskVideoStickerOrNull(): MaskVideoSticker? = this as?
dev.inmo.tgbotapi.types.files.MaskVideoSticker
public inline fun TelegramMediaFile.maskVideoStickerOrThrow(): MaskVideoSticker = this as
dev.inmo.tgbotapi.types.files.MaskVideoSticker
public inline fun <T> TelegramMediaFile.ifMaskVideoSticker(block: (MaskVideoSticker) -> T): T? =
maskVideoStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.customEmojiStickerOrNull(): CustomEmojiSticker? = this as?
dev.inmo.tgbotapi.types.files.CustomEmojiSticker
public inline fun TelegramMediaFile.customEmojiStickerOrThrow(): CustomEmojiSticker = this as
dev.inmo.tgbotapi.types.files.CustomEmojiSticker
public inline fun <T> TelegramMediaFile.ifCustomEmojiSticker(block: (CustomEmojiSticker) -> T): T? =
customEmojiStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.customEmojiSimpleStickerOrNull(): CustomEmojiSimpleSticker? =
this as? dev.inmo.tgbotapi.types.files.CustomEmojiSimpleSticker
public inline fun TelegramMediaFile.customEmojiSimpleStickerOrThrow(): CustomEmojiSimpleSticker =
this as dev.inmo.tgbotapi.types.files.CustomEmojiSimpleSticker
public inline fun <T>
TelegramMediaFile.ifCustomEmojiSimpleSticker(block: (CustomEmojiSimpleSticker) -> T): T? =
customEmojiSimpleStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.customEmojiAnimatedStickerOrNull(): CustomEmojiAnimatedSticker?
= this as? dev.inmo.tgbotapi.types.files.CustomEmojiAnimatedSticker
public inline fun TelegramMediaFile.customEmojiAnimatedStickerOrThrow(): CustomEmojiAnimatedSticker
= this as dev.inmo.tgbotapi.types.files.CustomEmojiAnimatedSticker
public inline fun <T>
TelegramMediaFile.ifCustomEmojiAnimatedSticker(block: (CustomEmojiAnimatedSticker) -> T): T? =
customEmojiAnimatedStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.customEmojiVideoStickerOrNull(): CustomEmojiVideoSticker? = this
as? dev.inmo.tgbotapi.types.files.CustomEmojiVideoSticker
public inline fun TelegramMediaFile.customEmojiVideoStickerOrThrow(): CustomEmojiVideoSticker = this
as dev.inmo.tgbotapi.types.files.CustomEmojiVideoSticker
public inline fun <T>
TelegramMediaFile.ifCustomEmojiVideoSticker(block: (CustomEmojiVideoSticker) -> T): T? =
customEmojiVideoStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.unknownStickerOrNull(): UnknownSticker? = this as?
dev.inmo.tgbotapi.types.files.UnknownSticker
public inline fun TelegramMediaFile.unknownStickerOrThrow(): UnknownSticker = this as
dev.inmo.tgbotapi.types.files.UnknownSticker
public inline fun <T> TelegramMediaFile.ifUnknownSticker(block: (UnknownSticker) -> T): T? =
unknownStickerOrNull() ?.let(block)
public inline fun TelegramMediaFile.thumbedMediaFileOrNull(): ThumbedMediaFile? = this as? public inline fun TelegramMediaFile.thumbedMediaFileOrNull(): ThumbedMediaFile? = this as?
dev.inmo.tgbotapi.types.files.ThumbedMediaFile dev.inmo.tgbotapi.types.files.ThumbedMediaFile
@@ -3427,6 +3552,15 @@ public inline fun TextSource.codeTextSourceOrThrow(): CodeTextSource = this as
public inline fun <T> TextSource.ifCodeTextSource(block: (CodeTextSource) -> T): T? = public inline fun <T> TextSource.ifCodeTextSource(block: (CodeTextSource) -> T): T? =
codeTextSourceOrNull() ?.let(block) codeTextSourceOrNull() ?.let(block)
public inline fun TextSource.customEmojiTextSourceOrNull(): CustomEmojiTextSource? = this as?
dev.inmo.tgbotapi.types.message.textsources.CustomEmojiTextSource
public inline fun TextSource.customEmojiTextSourceOrThrow(): CustomEmojiTextSource = this as
dev.inmo.tgbotapi.types.message.textsources.CustomEmojiTextSource
public inline fun <T> TextSource.ifCustomEmojiTextSource(block: (CustomEmojiTextSource) -> T): T? =
customEmojiTextSourceOrNull() ?.let(block)
public inline fun TextSource.eMailTextSourceOrNull(): EMailTextSource? = this as? public inline fun TextSource.eMailTextSourceOrNull(): EMailTextSource? = this as?
dev.inmo.tgbotapi.types.message.textsources.EMailTextSource dev.inmo.tgbotapi.types.message.textsources.EMailTextSource

View File

@@ -30,7 +30,7 @@ fun <T> Flow<Iterable<T>>.flatten(): Flow<T> = flow {
} }
} }
fun <T, R> Flow<T>.flatMap(mapper: (T) -> Iterable<R>): Flow<R> = flow { fun <T, R> Flow<T>.flatMap(mapper: suspend (T) -> Iterable<R>): Flow<R> = flow {
collect { collect {
mapper(it).forEach { mapper(it).forEach {
emit(it) emit(it)

View File

@@ -1,8 +1,8 @@
package dev.inmo.tgbotapi.extensions.utils.extensions.raw package dev.inmo.tgbotapi.extensions.utils.extensions.raw
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.requests.abstracts.FileId import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.types.FileUniqueId import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.StickerSetName
import dev.inmo.tgbotapi.types.files.* import dev.inmo.tgbotapi.types.files.*
import dev.inmo.tgbotapi.types.stickers.MaskPosition import dev.inmo.tgbotapi.types.stickers.MaskPosition
@@ -17,8 +17,10 @@ inline val Sticker.is_video: Boolean
inline val Sticker.set_name: StickerSetName? inline val Sticker.set_name: StickerSetName?
get() = stickerSetName get() = stickerSetName
inline val Sticker.mask_position: MaskPosition? inline val Sticker.mask_position: MaskPosition?
get() = maskPosition get() = maskStickerOrNull() ?.maskPosition
inline val Sticker.file_size: Long? inline val Sticker.file_size: Long?
get() = fileSize get() = fileSize
inline val Sticker.premium_animation: File? inline val Sticker.premium_animation: File?
get() = premiumAnimationFile get() = regularStickerOrNull() ?.premiumAnimationFile
inline val Sticker.custom_emoji_id: CustomEmojiId?
get() = customEmojiStickerOrNull() ?.customEmojiId

View File

@@ -3,6 +3,7 @@
package dev.inmo.tgbotapi.extensions.utils.formatting package dev.inmo.tgbotapi.extensions.utils.formatting
import dev.inmo.micro_utils.common.joinTo import dev.inmo.micro_utils.common.joinTo
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.chat.User import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.types.message.textsources.* import dev.inmo.tgbotapi.types.message.textsources.*
import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.RiskFeature
@@ -510,3 +511,39 @@ inline fun EntitiesBuilder.underline(text: String) = add(dev.inmo.tgbotapi.types
* Version of [EntitiesBuilder.underline] with new line at the end * Version of [EntitiesBuilder.underline] with new line at the end
*/ */
inline fun EntitiesBuilder.underlineln(text: String) = underline(text) + newLine inline fun EntitiesBuilder.underlineln(text: String) = underline(text) + newLine
/**
* Add customEmoji using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.customEmoji]
*/
inline fun EntitiesBuilder.customEmoji(customEmojiId: CustomEmojiId, parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.message.textsources.customEmoji(customEmojiId, parts))
/**
* Version of [EntitiesBuilder.customEmoji] with new line at the end
*/
inline fun EntitiesBuilder.customEmojiln(customEmojiId: CustomEmojiId, parts: TextSourcesList) = customEmoji(customEmojiId, parts) + newLine
/**
* Add customEmoji using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.customEmoji].
* Will reuse separator config from [buildEntities]
*/
inline fun EntitiesBuilder.customEmoji(customEmojiId: CustomEmojiId, noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.message.textsources.customEmoji(customEmojiId, buildEntities(separator, init)))
/**
* Version of [EntitiesBuilder.customEmoji] with new line at the end.
* Will reuse separator config from [buildEntities]
*/
inline fun EntitiesBuilder.customEmojiln(customEmojiId: CustomEmojiId, noinline init: EntitiesBuilderBody) = customEmoji(customEmojiId, init) + newLine
/**
* Add customEmoji using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.customEmoji]
*/
inline fun EntitiesBuilder.customEmoji(customEmojiId: CustomEmojiId, vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.message.textsources.customEmoji(customEmojiId, *parts))
/**
* Version of [EntitiesBuilder.customEmoji] with new line at the end
*/
inline fun EntitiesBuilder.customEmojiln(customEmojiId: CustomEmojiId, vararg parts: TextSource) = customEmoji(customEmojiId, *parts) + newLine
/**
* Add customEmoji using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.customEmoji]
*/
inline fun EntitiesBuilder.customEmoji(customEmojiId: CustomEmojiId, text: String) = add(dev.inmo.tgbotapi.types.message.textsources.customEmoji(customEmojiId, text))
/**
* Version of [EntitiesBuilder.customEmoji] with new line at the end
*/
inline fun EntitiesBuilder.customEmojiln(customEmojiId: CustomEmojiId, text: String) = customEmoji(customEmojiId, text) + newLine

View File

@@ -4,17 +4,25 @@ import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.* import dev.inmo.tgbotapi.types.chat.*
import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.abstracts.Message
import dev.inmo.tgbotapi.types.message.textsources.link import dev.inmo.tgbotapi.types.message.textsources.link
import io.ktor.http.encodeURLQueryComponent
fun makeUsernameLink(username: String) = "$internalLinkBeginning/$username" fun makeUsernameLink(username: String) = "$internalLinkBeginning/$username"
fun makeUsernameDeepLinkPrefix(username: String) = "${makeUsernameLink(username)}?start=" fun makeUsernameDeepLinkPrefix(username: String) = "${makeUsernameLink(username)}?start="
fun makeUsernameStartattachPrefix(username: String) = "$internalLinkBeginning/$username?startattach"
fun makeUsernameStartattachLink(username: String, data: String? = null) = "${makeUsernameStartattachPrefix(username)}${data?.let { "=$it" } ?: ""}"
inline val Username.link inline val Username.link
get() = makeUsernameLink(usernameWithoutAt) get() = makeUsernameLink(usernameWithoutAt)
inline val Username.deepLinkPrefix inline val Username.deepLinkPrefix
get() = makeUsernameDeepLinkPrefix(usernameWithoutAt) get() = makeUsernameDeepLinkPrefix(usernameWithoutAt)
inline val Username.startattachPrefix
get() = makeUsernameStartattachPrefix(usernameWithoutAt)
inline fun makeLink(username: Username) = username.link inline fun makeLink(username: Username) = username.link
inline fun makeTelegramDeepLink(username: String, startParameter: String) = "${makeUsernameDeepLinkPrefix(username)}$startParameter" inline fun makeTelegramDeepLink(username: String, startParameter: String) = "${makeUsernameDeepLinkPrefix(username)}$startParameter".encodeURLQueryComponent()
inline fun makeDeepLink(username: Username, startParameter: String) = "${username.deepLinkPrefix}$startParameter" inline fun makeTelegramStartattach(username: String, data: String? = null) = makeUsernameStartattachLink(username, data)
inline fun makeDeepLink(username: Username, startParameter: String) = makeTelegramDeepLink(username.usernameWithoutAt, startParameter)
inline fun makeTelegramDeepLink(username: Username, startParameter: String) = makeDeepLink(username, startParameter) inline fun makeTelegramDeepLink(username: Username, startParameter: String) = makeDeepLink(username, startParameter)
inline fun makeTelegramStartattach(username: Username, data: String? = null) = makeTelegramStartattach(username.usernameWithoutAt, data)
fun makeLinkToMessage( fun makeLinkToMessage(
username: String, username: String,

View File

@@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.*
fun TelegramBot.longPollingFlow( fun TelegramBot.longPollingFlow(
timeoutSeconds: Seconds = 30, timeoutSeconds: Seconds = 30,
exceptionsHandler: (ExceptionHandler<Unit>)? = null, exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
): Flow<Update> = channelFlow { ): Flow<Update> = channelFlow {
var lastUpdateIdentifier: UpdateIdentifier? = null var lastUpdateIdentifier: UpdateIdentifier? = null
@@ -81,7 +81,7 @@ fun TelegramBot.startGettingOfUpdatesByLongPolling(
timeoutSeconds: Seconds = 30, timeoutSeconds: Seconds = 30,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: (ExceptionHandler<Unit>)? = null, exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
updatesReceiver: UpdateReceiver<Update> updatesReceiver: UpdateReceiver<Update>
): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates).subscribeSafely( ): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates).subscribeSafely(
scope, scope,
@@ -97,7 +97,7 @@ fun TelegramBot.createAccumulatedUpdatesRetrieverFlow(
avoidInlineQueries: Boolean = false, avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false, avoidCallbackQueries: Boolean = false,
exceptionsHandler: ExceptionHandler<Unit>? = null, exceptionsHandler: ExceptionHandler<Unit>? = null,
allowedUpdates: List<String>? = null allowedUpdates: List<String>? = ALL_UPDATES_LIST
): Flow<Update> = longPollingFlow( ): Flow<Update> = longPollingFlow(
timeoutSeconds = 0, timeoutSeconds = 0,
exceptionsHandler = { exceptionsHandler = {
@@ -117,7 +117,7 @@ fun TelegramBot.retrieveAccumulatedUpdates(
avoidCallbackQueries: Boolean = false, avoidCallbackQueries: Boolean = false,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: (ExceptionHandler<Unit>)? = null, exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
updatesReceiver: UpdateReceiver<Update> updatesReceiver: UpdateReceiver<Update>
): Job = createAccumulatedUpdatesRetrieverFlow( ): Job = createAccumulatedUpdatesRetrieverFlow(
avoidInlineQueries, avoidInlineQueries,
@@ -149,7 +149,7 @@ suspend fun TelegramBot.flushAccumulatedUpdates(
avoidInlineQueries: Boolean = false, avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false, avoidCallbackQueries: Boolean = false,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
exceptionsHandler: ExceptionHandler<Unit>? = null, exceptionsHandler: ExceptionHandler<Unit>? = null,
updatesReceiver: UpdateReceiver<Update> = {} updatesReceiver: UpdateReceiver<Update> = {}
) = retrieveAccumulatedUpdates( ) = retrieveAccumulatedUpdates(

View File

@@ -0,0 +1,3 @@
package dev.inmo.tgbotapi.webapps
typealias AlertCallback = () -> Unit

View File

@@ -0,0 +1,3 @@
package dev.inmo.tgbotapi.webapps
typealias ConfirmCallback = (confirmed: Boolean) -> Unit

View File

@@ -5,3 +5,4 @@ import dev.inmo.tgbotapi.webapps.invoice.InvoiceClosedInfo
typealias EventHandler = WebApp.() -> Unit typealias EventHandler = WebApp.() -> Unit
typealias ViewportChangedEventHandler = WebApp.(ViewportChangedData) -> Unit typealias ViewportChangedEventHandler = WebApp.(ViewportChangedData) -> Unit
typealias InvoiceClosedEventHandler = WebApp.(InvoiceClosedInfo) -> Unit typealias InvoiceClosedEventHandler = WebApp.(InvoiceClosedInfo) -> Unit
typealias PopupClosedEventHandler = WebApp.(String?) -> Unit

View File

@@ -7,4 +7,5 @@ sealed class EventType(val typeName: String) {
object BackButtonClicked : EventType("backButtonClicked") object BackButtonClicked : EventType("backButtonClicked")
object SettingsButtonClicked : EventType("settingsButtonClicked") object SettingsButtonClicked : EventType("settingsButtonClicked")
object InvoiceClosed : EventType("invoiceClosed") object InvoiceClosed : EventType("invoiceClosed")
object PopupClosed : EventType("popupClosed")
} }

View File

@@ -3,6 +3,7 @@ package dev.inmo.tgbotapi.webapps
import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper
import dev.inmo.tgbotapi.webapps.haptic.HapticFeedback import dev.inmo.tgbotapi.webapps.haptic.HapticFeedback
import dev.inmo.tgbotapi.webapps.invoice.InvoiceClosedInfo import dev.inmo.tgbotapi.webapps.invoice.InvoiceClosedInfo
import dev.inmo.tgbotapi.webapps.popup.*
external class WebApp { external class WebApp {
val version: String val version: String
@@ -24,6 +25,15 @@ external class WebApp {
val viewportHeight: Float val viewportHeight: Float
val viewportStableHeight: Float val viewportStableHeight: Float
val isClosingConfirmationEnabled: Boolean
fun enableClosingConfirmation()
fun disableClosingConfirmation()
fun showPopup(params: PopupParams, callback: ClosePopupCallback? = definedExternally)
fun showAlert(message: String, callback: AlertCallback? = definedExternally)
fun showConfirm(message: String, callback: ConfirmCallback? = definedExternally)
@JsName("MainButton") @JsName("MainButton")
val mainButton: MainButton val mainButton: MainButton
@@ -38,6 +48,8 @@ external class WebApp {
internal fun onEventWithViewportChangedData(type: String, callback: (ViewportChangedData) -> Unit) internal fun onEventWithViewportChangedData(type: String, callback: (ViewportChangedData) -> Unit)
@JsName("onEvent") @JsName("onEvent")
internal fun onEventWithInvoiceClosedInfo(type: String, callback: (InvoiceClosedInfo) -> Unit) internal fun onEventWithInvoiceClosedInfo(type: String, callback: (InvoiceClosedInfo) -> Unit)
@JsName("onEvent")
internal fun onEventWithPopupClosedInfo(type: String, callback: (String?) -> Unit)
fun offEvent(type: String, callback: () -> Unit) fun offEvent(type: String, callback: () -> Unit)
@JsName("offEvent") @JsName("offEvent")
@@ -100,6 +112,18 @@ fun WebApp.onEvent(type: EventType.InvoiceClosed, eventHandler: InvoiceClosedEve
) )
} }
/**
* @return The callback which should be used in case you want to turn off events handling
*/
fun WebApp.onEvent(type: EventType.PopupClosed, eventHandler: PopupClosedEventHandler) = { it: String? ->
eventHandler(js("this").unsafeCast<WebApp>(), it)
}.also {
onEventWithPopupClosedInfo(
type.typeName,
callback = it
)
}
/** /**
* @return The callback which should be used in case you want to turn off events handling * @return The callback which should be used in case you want to turn off events handling
*/ */
@@ -124,8 +148,55 @@ fun WebApp.onSettingsButtonClicked(eventHandler: EventHandler) = onEvent(EventTy
* @return The callback which should be used in case you want to turn off events handling * @return The callback which should be used in case you want to turn off events handling
*/ */
fun WebApp.onInvoiceClosed(eventHandler: InvoiceClosedEventHandler) = onEvent(EventType.InvoiceClosed, eventHandler) fun WebApp.onInvoiceClosed(eventHandler: InvoiceClosedEventHandler) = onEvent(EventType.InvoiceClosed, eventHandler)
/**
* @return The callback which should be used in case you want to turn off events handling
*/
fun WebApp.onPopupClosed(eventHandler: PopupClosedEventHandler) = onEvent(EventType.PopupClosed, eventHandler)
fun WebApp.isInitDataSafe(botToken: String) = TelegramAPIUrlsKeeper(botToken).checkWebAppData( fun WebApp.isInitDataSafe(botToken: String) = TelegramAPIUrlsKeeper(botToken).checkWebAppData(
initData, initData,
initDataUnsafe.hash initDataUnsafe.hash
) )
fun WebApp.showPopup(
message: String,
title: String?,
buttons: Array<PopupButton>,
callback: ClosePopupCallback? = null
) = showPopup(
PopupParams(
message,
title,
buttons
),
callback
)
fun WebApp.showPopup(
message: String,
title: String?,
firstButton: PopupButton,
vararg otherButtons: PopupButton,
callback: ClosePopupCallback? = null
) = showPopup(
PopupParams(
message,
title,
arrayOf(firstButton, *otherButtons)
),
callback
)
var WebApp.requireClosingConfirmation
get() = isClosingConfirmationEnabled
set(value) {
if (value) {
enableClosingConfirmation()
} else {
disableClosingConfirmation()
}
}
fun WebApp.toggleClosingConfirmation() {
requireClosingConfirmation = !requireClosingConfirmation
}

View File

@@ -17,10 +17,14 @@ external interface WebAppUser {
val username: String? val username: String?
@JsName(languageCodeField) @JsName(languageCodeField)
val languageCode: String? val languageCode: String?
val is_premium: Boolean?
@JsName(photoUrlField) @JsName(photoUrlField)
val photoUrl: String? val photoUrl: String?
} }
val WebAppUser.isPremium
get() = is_premium == true
fun WebAppUser.asUser() = if (isBot == true) { fun WebAppUser.asUser() = if (isBot == true) {
CommonBot( CommonBot(
UserId(id), UserId(id),
@@ -34,6 +38,7 @@ fun WebAppUser.asUser() = if (isBot == true) {
firstName, firstName,
lastName ?: "", lastName ?: "",
username ?.let(::Username), username ?.let(::Username),
languageCode ?.let(::IetfLanguageCode) languageCode ?.let(::IetfLanguageCode),
isPremium = isPremium
) )
} }

View File

@@ -0,0 +1,3 @@
package dev.inmo.tgbotapi.webapps.popup
typealias ClosePopupCallback = (id: String) -> Unit

View File

@@ -0,0 +1,55 @@
package dev.inmo.tgbotapi.webapps.popup
import kotlin.js.json
external interface PopupButton {
val id: String
val type: PopupButtonType
val text: String?
}
fun PopupButton(
id: String,
type: PopupButtonType,
text: String? = null
) = json(
*listOfNotNull(
"id" to id,
"type" to type.typeName,
("text" to text).takeIf { text != null }
).toTypedArray()
).unsafeCast<PopupButton>()
value class PopupButtonType(
val typeName: String
) {
companion object {
val Default = PopupButtonType("default")
val Ok = PopupButtonType("ok")
val Close = PopupButtonType("close")
val Cancel = PopupButtonType("cancel")
val Destructive = PopupButtonType("destructive")
}
}
fun DefaultPopupButton(
id: String,
text: String
) = PopupButton(id, PopupButtonType.Default, text)
fun OkPopupButton(
id: String
) = PopupButton(id, PopupButtonType.Ok)
fun ClosePopupButton(
id: String
) = PopupButton(id, PopupButtonType.Close)
fun CancelPopupButton(
id: String
) = PopupButton(id, PopupButtonType.Cancel)
fun DestructivePopupButton(
id: String,
text: String
) = PopupButton(id, PopupButtonType.Destructive, text)

View File

@@ -0,0 +1,48 @@
package dev.inmo.tgbotapi.webapps.popup
import kotlin.js.json
external interface PopupParams {
val message: String
val title: String?
val buttons: Array<PopupButton>
}
fun PopupParams(
message: String,
title: String?,
buttons: Array<PopupButton>
) = json(
*listOfNotNull(
"message" to message,
"buttons" to buttons,
("title" to title).takeIf { title != null }
).toTypedArray()
).unsafeCast<PopupParams>()
fun PopupParams(
message: String,
firstButton: PopupButton,
vararg otherButtons: PopupButton
) = PopupParams(
message,
null,
arrayOf(
firstButton,
*otherButtons
)
)
fun PopupParams(
title: String,
message: String,
firstButton: PopupButton,
vararg otherButtons: PopupButton
) = PopupParams(
message,
title,
arrayOf(
firstButton,
*otherButtons
)
)