1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2024-12-22 16:47:13 +00:00

preview state of steps

This commit is contained in:
InsanusMokrassar 2021-01-06 23:06:48 +06:00
parent 6bd423dc11
commit 00873a255c
11 changed files with 450 additions and 1 deletions

View File

@ -8,5 +8,6 @@ pluginManagement {
include ":tgbotapi.core"
include ":tgbotapi.extensions.api"
include ":tgbotapi.extensions.utils"
include ":tgbotapi.extensions.steps"
include ":tgbotapi"
include ":docs"

View File

@ -19,13 +19,17 @@ interface TextedOutput : ParsableOutput, EntitiesOutput
interface TextedInput : Texted {
/**
* Not full list of entities. This list WILL NOT contain [TextPart]s with [dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource]
* Here must be full list of entities. This list must contains [TextPart]s with
* [dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource] in case if source text contains parts of
* regular text
* @see [CaptionedInput.fullEntitiesList]
*/
val textEntities: List<TextPart>
}
/**
* Full list of [TextSource] built from source[TextedInput.textEntities]
*
* @see TextedInput.textEntities
* @see justTextSources
*/

View File

@ -0,0 +1 @@
# TelegramBotAPI Steps Extensions

View File

@ -0,0 +1,48 @@
buildscript {
repositories {
mavenLocal()
jcenter()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
project.version = "$library_version"
project.group = "$library_group"
apply from: "publish.gradle"
repositories {
mavenLocal()
jcenter()
mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
kotlin {
jvm()
js(BOTH) {
browser()
nodejs()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
api project(":tgbotapi.core")
api project(":tgbotapi.extensions.utils")
api project(":tgbotapi.extensions.api")
}
}
}
}

View File

@ -0,0 +1 @@
{"bintrayConfig":{"repo":"TelegramBotAPI","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/TelegramBotAPI","autoPublish":true,"overridePublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"}],"mavenConfig":{"name":"Telegram Bot API Steps Extensions","description":"These extensions project contains tools for simple interaction with chats","url":"https://insanusmokrassar.github.io/TelegramBotAPI/tgbotapi.extensions.steps","vcsUrl":"https://github.com/insanusmokrassar/TelegramBotAPI.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]}}

View File

@ -0,0 +1,69 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
classifier = 'javadoc'
}
task sourceJar (type : Jar) {
classifier = 'sources'
}
afterEvaluate {
project.publishing.publications.all {
// rename artifacts
groupId "${project.group}"
if (it.name.contains('kotlinMultiplatform')) {
artifactId = "${project.name}"
artifact sourceJar
} else {
artifactId = "${project.name}-$name"
}
}
}
publishing {
publications.all {
artifact javadocsJar
pom {
description = "These extensions project contains tools for simple interaction with chats"
name = "Telegram Bot API Steps Extensions"
url = "https://insanusmokrassar.github.io/TelegramBotAPI/tgbotapi.extensions.steps"
scm {
developerConnection = "scm:git:[fetch=]https://github.com/insanusmokrassar/TelegramBotAPI.git[push=]https://github.com/insanusmokrassar/TelegramBotAPI.git"
url = "https://github.com/insanusmokrassar/TelegramBotAPI.git"
}
developers {
developer {
id = "InsanusMokrassar"
name = "Ovsiannikov Aleksei"
email = "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name = "Apache Software License 2.0"
url = "https://github.com/InsanusMokrassar/TelegramBotAPI/blob/master/LICENSE"
}
}
}
repositories {
maven {
name = "bintray"
url = uri("https://api.bintray.com/maven/${project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')}/TelegramBotAPI/${project.name}/;publish=1;override=1")
credentials {
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
}
}
}
}
}

View File

@ -0,0 +1,16 @@
package dev.inmo.tgbotapi.extensions.steps
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
import kotlinx.coroutines.CoroutineScope
typealias ScenarioReceiver<T> = suspend Scenario.() -> T
typealias ScenarioAndTypeReceiver<T, I> = suspend Scenario.(I) -> T
data class Scenario(
val bot: TelegramBot,
val flowsUpdatesFilter: FlowsUpdatesFilter,
val scope: CoroutineScope
)

View File

@ -0,0 +1,17 @@
package dev.inmo.tgbotapi.extensions.steps
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
import kotlinx.coroutines.CoroutineScope
suspend fun TelegramBot.buildScenarios(
scope: CoroutineScope,
flowUpdatesFilter: FlowsUpdatesFilter = FlowsUpdatesFilter(),
block: ScenarioReceiver<Unit>
) {
Scenario(
this,
flowUpdatesFilter,
scope
).block()
}

View File

@ -0,0 +1,80 @@
package dev.inmo.tgbotapi.extensions.steps.expectations
import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.steps.Scenario
import dev.inmo.tgbotapi.extensions.steps.ScenarioReceiver
import dev.inmo.tgbotapi.requests.abstracts.Request
import dev.inmo.tgbotapi.types.update.abstracts.Update
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
import dev.inmo.tgbotapi.utils.RiskFeature
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
private val cancelledByFilterException = CancellationException("Cancelled by filter precreatedException")
typealias RequestBuilder<T> = suspend (Update) -> Request<T>
typealias NullableRequestBuilder<T> = suspend (Update) -> Request<T>?
@RiskFeature("This method is not very comfortable to use and too low-level. It is recommended to use methods which already included into library")
suspend fun <T> FlowsUpdatesFilter.expectFlow(
bot: TelegramBot,
initRequest: Request<*>? = null,
count: Int? = null,
errorFactory: NullableRequestBuilder<*> = { null },
cancelRequestFactory: NullableRequestBuilder<*> = { null },
cancelTrigger: suspend (Update) -> Boolean = { cancelRequestFactory(it) != null },
filter: suspend (Update) -> T?
): Flow<T> {
val flow = allUpdatesFlow.mapNotNull {
val result = safelyWithoutExceptions { filter(it) }
if (result == null) {
if (cancelTrigger(it)) {
cancelRequestFactory(it) ?.also {
safelyWithoutExceptions { bot.execute(it) }
throw cancelledByFilterException
}
}
errorFactory(it) ?.also { errorRequest ->
safelyWithoutExceptions { bot.execute(errorRequest) }
}
null
} else {
result
}
}
val result = if (count == null) {
flow
} else {
flow.take(count)
}
initRequest ?.also { safelyWithoutExceptions { bot.execute(initRequest) } }
return result
}
suspend fun <T> Scenario.expectFlow(
initRequest: Request<*>? = null,
count: Int? = null,
errorFactory: NullableRequestBuilder<*> = { null },
cancelRequestFactory: NullableRequestBuilder<*> = { null },
cancelTrigger: suspend (Update) -> Boolean = { cancelRequestFactory(it) != null },
filter: suspend (Update) -> T?
) = flowsUpdatesFilter.expectFlow(bot, initRequest, count, errorFactory, cancelRequestFactory, cancelTrigger, filter)
@RiskFeature("This method is not very comfortable to use and too low-level. It is recommended to use methods which already included into library")
suspend fun <T> FlowsUpdatesFilter.expectOne(
bot: TelegramBot,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
cancelRequestFactory: NullableRequestBuilder<*> = { null },
cancelTrigger: suspend (Update) -> Boolean = { cancelRequestFactory(it) != null },
filter: suspend (Update) -> T?,
): T = expectFlow(bot, initRequest, 1, errorFactory, cancelRequestFactory, cancelTrigger, filter).first()
suspend fun <T> Scenario.expectOne(
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
cancelRequestFactory: NullableRequestBuilder<*> = { null },
cancelTrigger: suspend (Update) -> Boolean = { cancelRequestFactory(it) != null },
filter: suspend (Update) -> T?
) = flowsUpdatesFilter.expectOne(bot, initRequest, errorFactory, cancelRequestFactory, cancelTrigger, filter)

View File

@ -0,0 +1,175 @@
package dev.inmo.tgbotapi.extensions.steps.expectations
import dev.inmo.tgbotapi.extensions.steps.Scenario
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.requests.abstracts.Request
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.*
import dev.inmo.tgbotapi.types.message.content.abstracts.*
import dev.inmo.tgbotapi.types.message.content.media.*
import dev.inmo.tgbotapi.types.message.payments.InvoiceContent
import kotlinx.coroutines.flow.toList
typealias ContentMessageToContentMapper<T> = suspend ContentMessage<T>.() -> T?
private suspend fun <O> Scenario.waitContentMessage(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
mapper: suspend ContentMessage<MessageContent>.() -> O?
): List<O> = expectFlow(
initRequest,
count,
errorFactory
) {
it.asMessageUpdate() ?.data ?.asContentMessage() ?.mapper()
}.toList().toList()
private suspend inline fun <reified T : MessageContent> Scenario.waitContent(
count: Int = 1,
initRequest: Request<*>? = null,
noinline errorFactory: NullableRequestBuilder<*> = { null },
noinline filter: (suspend (ContentMessage<T>) -> T?)? = null
) : List<T> = waitContentMessage<T>(
count,
initRequest,
errorFactory
) {
if (content is T) {
val message = (this as ContentMessage<T>)
if (filter == null) {
message.content
} else {
filter(message)
}
} else {
null
}
}
suspend fun Scenario.waitContact(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<ContactContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitDice(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<DiceContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitGame(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<GameContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitLocation(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<LocationContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitPoll(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<PollContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitText(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<TextContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitVenue(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<VenueContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitAudioMediaGroup(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<AudioMediaGroupContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitDocumentMediaGroup(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<DocumentMediaGroupContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitMedia(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<MediaContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitMediaGroup(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<MediaGroupContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitVisualMediaGroup(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<VisualMediaGroupContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitAnimation(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<AnimationContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitAudio(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<AudioContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitDocument(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<DocumentContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitPhoto(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<PhotoContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitSticker(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<StickerContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitVideo(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<VideoContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitVideoNote(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<VideoNoteContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitVoice(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<VoiceContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)
suspend fun Scenario.waitInvoice(
count: Int = 1,
initRequest: Request<*>? = null,
errorFactory: NullableRequestBuilder<*> = { null },
filter: ContentMessageToContentMapper<InvoiceContent>? = null
) = waitContent(count, initRequest, errorFactory, filter)

View File

@ -0,0 +1,37 @@
package dev.inmo.tgbotapi.extensions.steps.triggers_handling
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.tgbotapi.CommonAbstracts.textSources
import dev.inmo.tgbotapi.extensions.steps.*
import dev.inmo.tgbotapi.extensions.steps.expectations.expectFlow
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.TextContent
suspend fun Scenario.command(
commandRegex: Regex,
requireOnlyCommandInMessage: Boolean = true,
scenarioReceiver: ScenarioAndTypeReceiver<Unit, ContentMessage<TextContent>>
) {
flowsUpdatesFilter.expectFlow(bot) {
it.asMessageUpdate() ?.data ?.asContentMessage() ?.let { message ->
message.content.asTextContent() ?.let {
val textSources = it.textSources
val sizeRequirement = if (requireOnlyCommandInMessage) {
textSources.size == 1
} else {
true
}
if (sizeRequirement && textSources.any { commandRegex.matches(it.asBotCommandTextSource() ?.command ?: return@any false) }) {
message as ContentMessage<TextContent>
} else {
null
}
}
}
}.subscribeSafelyWithoutExceptions(scope) {
scenarioReceiver(it)
}
}