diff --git a/CHANGELOG.md b/CHANGELOG.md index 63acdb2..4861d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2.1.0 + +* `Versions`: + * `tgbotapi`: `3.1.1` + * `ktor`: `2.1.0` + * `microutils`: `0.12.1` +* `Plugins`: + * New fum of `Plugin` with `BehaviourContextWithFSM` receiver +* `Bot`: + * Now bot uses `buildBehaviourWithFSM` to be able to setup bot with FSM + ## 2.0.0 * `Versions`: diff --git a/README.md b/README.md index b867809..4386187 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,28 @@ You can create your bot using That is a set of libraries for plagubots. Look at the [PlaguBot Plugin template](https://insanusmokrassar.github.io/PlaguBotPluginTemplate/) to find how to create your bot. + +### Technical help + +In this bot has been used variant with FSM. That means that you may use all the [Behaviour Builder with FSM](https://bookstack.inmo.dev/books/telegrambotapi/page/behaviour-builder-with-fsm) functionality. In case you wish to setup states repo, you should use the next code in the `setupDI` of your plugin: + +```kotlin +single> { + // setup your manager and return here + // Default is: + DefaultStatesManager( + InMemoryDefaultStatesManagerRepo() + ) +} +``` + +Besides, you may setup handling errors lambda in the same function: + +```kotlin +single> { + { state, e -> + logger.eS(e) { "Unable to handle state $state" } // logging by default + null // you should return new state or null, default callback will return null + } +} +``` diff --git a/bot/build.gradle b/bot/build.gradle index fe93273..473692a 100644 --- a/bot/build.gradle +++ b/bot/build.gradle @@ -16,6 +16,7 @@ dependencies { api libs.jb.exposed.jdbc api libs.tgbotapi + api libs.tgbotapi.behaviourBuilder.fsm api libs.microutils.repos.exposed api libs.kslog diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt b/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt index 89cb216..085722e 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt @@ -1,10 +1,18 @@ package dev.inmo.plagubot import dev.inmo.kslog.common.* +import dev.inmo.micro_utils.fsm.common.State +import dev.inmo.plagubot.HelloPlugin.setupBotPlugin import dev.inmo.tgbotapi.extensions.api.bot.getMe import dev.inmo.tgbotapi.extensions.api.send.reply -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitText +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitTextMessage import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onUnhandledCommand +import dev.inmo.tgbotapi.types.ChatId +import kotlinx.coroutines.flow.first import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -24,14 +32,34 @@ object HelloPlugin : Plugin { override fun Module.setupDI(database: Database, params: JsonObject) { single { get().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null) + + } } - override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { - logger.d { koin.get().print } + private sealed interface InternalFSMState : State { + override val context: ChatId + data class DidntSaidHello(override val context: ChatId) : InternalFSMState + data class SaidHelloOnce(override val context: ChatId) : InternalFSMState + } + + override suspend fun BehaviourContextWithFSM.setupBotPlugin(koin: Koin) { + val toPrint = koin.getOrNull() ?.print ?: "Hello :)" + logger.d { toPrint } logger.dS { getMe().toString() } onCommand("hello_world") { - reply(it, "Hello :)") + startChain(InternalFSMState.DidntSaidHello(it.chat.id)) + } + + strictlyOn { state: InternalFSMState.DidntSaidHello -> + sendMessage(state.context, toPrint) + InternalFSMState.SaidHelloOnce(state.context) + } + + strictlyOn { state: InternalFSMState.SaidHelloOnce -> + val message = waitTextMessage().first() + reply(message, "Sorry, I can answer only this: $toPrint") + InternalFSMState.SaidHelloOnce(state.context) } } } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt index b850b49..756aac8 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt @@ -3,12 +3,13 @@ package dev.inmo.plagubot import dev.inmo.kslog.common.* import dev.inmo.micro_utils.common.Warning import dev.inmo.micro_utils.coroutines.runCatchingSafely -import dev.inmo.plagubot.config.Config -import dev.inmo.plagubot.config.defaultJsonFormat +import dev.inmo.micro_utils.fsm.common.State +import dev.inmo.micro_utils.fsm.common.StatesManager +import dev.inmo.micro_utils.fsm.common.managers.* +import dev.inmo.plagubot.config.* import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.webhook.deleteWebhook -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviour +import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling import kotlinx.coroutines.* import kotlinx.serialization.Serializable @@ -60,7 +61,7 @@ data class PlaguBot( ) } - override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { + override suspend fun BehaviourContextWithFSM.setupBotPlugin(koin: Koin) { config.plugins.map { plugin -> launch { runCatchingSafely { @@ -94,12 +95,25 @@ data class PlaguBot( GlobalContext.startKoin(koinApp) logger.i("Koin started") lateinit var behaviourContext: BehaviourContext - bot.buildBehaviour(scope = scope) { + bot.buildBehaviourWithFSM( + scope = scope, + defaultExceptionsHandler = { + logger.e("Something went wrong", it) + }, + statesManager = koinApp.koin.getOrNull>() ?: DefaultStatesManager( + koinApp.koin.getOrNull>() ?: InMemoryDefaultStatesManagerRepo(), + onStartContextsConflictResolver = { _, _ -> false } + ), + onStateHandlingErrorHandler = koinApp.koin.getOrNull>() ?: { state, e -> + logger.eS(e) { "Unable to handle state $state" } + null + } + ) { logger.i("Start setup of bot part") behaviourContext = this setupBotPlugin(koinApp.koin) deleteWebhook() - } + }.start() logger.i("Behaviour builder has been setup") return bot.startGettingOfUpdatesByLongPolling(scope = behaviourContext, updatesFilter = behaviourContext).also { logger.i("Long polling has been started") diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/StateHandlingErrorHandler.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/StateHandlingErrorHandler.kt new file mode 100644 index 0000000..14b136d --- /dev/null +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/StateHandlingErrorHandler.kt @@ -0,0 +1,3 @@ +package dev.inmo.plagubot.config + +typealias StateHandlingErrorHandler = suspend (T, Throwable) -> T? diff --git a/gradle.properties b/gradle.properties index c22cb52..0644318 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ kotlin.js.generate.externals=true kotlin.incremental=true group=dev.inmo -version=2.0.0 +version=2.1.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 044a050..16974bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,8 +4,8 @@ kt = "1.7.10" kt-serialization = "1.4.0-RC" kt-coroutines = "1.6.4" -microutils = "0.12.0" -tgbotapi = "3.0.2" +microutils = "0.12.1" +tgbotapi = "3.1.1" kslog = "0.5.0" jb-exposed = "0.39.2" @@ -16,7 +16,7 @@ sqlite = "3.36.0.3" klock = "3.0.0" uuid = "0.5.0" -ktor = "2.0.3" +ktor = "2.1.0" gh-release = "2.4.1" @@ -32,6 +32,7 @@ kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", vers kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" } tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" } +tgbotapi-behaviourBuilder-fsm = { module = "dev.inmo:tgbotapi.behaviour_builder.fsm", version.ref = "tgbotapi" } microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" } kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fc..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt index 87cfdee..83d6ad6 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt @@ -1,6 +1,8 @@ package dev.inmo.plagubot +import dev.inmo.micro_utils.fsm.common.State import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject import org.jetbrains.exposed.sql.Database @@ -23,7 +25,20 @@ interface Plugin { database: Database, params: JsonObject ) {} + + /** + * Override this method in cases when you want to declare common bot behaviour. In case you wish to use FSM, you + * should override the method with receiver [BehaviourContextWithFSM] + */ suspend fun BehaviourContext.setupBotPlugin( koin: Koin ) {} + /** + * Override this method in cases when you want to declare full behaviour of the plugin. It is recommended to declare + * common logic of plugin in the [setupBotPlugin] with [BehaviourContext] receiver and use override this one + * for the FSM configuration + */ + suspend fun BehaviourContextWithFSM.setupBotPlugin(koin: Koin) { + (this as BehaviourContext).setupBotPlugin(koin) + } } diff --git a/settings.gradle b/settings.gradle index ab5d8e7..2e81cdb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,12 @@ String[] toInclude = [":bot", ":plugin"] rootProject.name = 'plagubot' -toInclude.each { - include (it) - project(it).name = "${rootProject.name}${it.replace(":", ".")}" +toInclude.each { originalName -> + String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replace(":", File.separator)}" + String projectName = "${rootProject.name}${originalName.replace(":", ".")}" + String projectIdentifier = ":${projectName}" + include projectIdentifier + ProjectDescriptor project = project(projectIdentifier) + project.name = projectName + project.projectDir = new File(projectDirectory) }