From 08b797bbc24d50e62cd66b7f46ce8057f20bbc47 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 13 Aug 2022 18:30:33 +0600 Subject: [PATCH] start integration FSM oriented plagubot version --- fsm/bot/build.gradle | 38 ++++++ fsm/bot/publish.gradle | 81 +++++++++++++ fsm/bot/publish.kpsb | 1 + fsm/bot/src/main/kotlin/App.kt | 25 ++++ fsm/bot/src/main/kotlin/FSMPlaguBot.kt | 112 ++++++++++++++++++ fsm/bot/src/main/kotlin/HelloFSMPlugin.kt | 37 ++++++ fsm/plugin/build.gradle | 21 ++++ fsm/plugin/publish.gradle | 81 +++++++++++++ fsm/plugin/publish.kpsb | 1 + fsm/plugin/src/main/kotlin/FSMPlugin.kt | 24 ++++ .../src/main/kotlin/FSMPluginSerializer.kt | 25 ++++ gradle/libs.versions.toml | 1 + settings.gradle | 13 +- 13 files changed, 456 insertions(+), 4 deletions(-) create mode 100644 fsm/bot/build.gradle create mode 100644 fsm/bot/publish.gradle create mode 100644 fsm/bot/publish.kpsb create mode 100644 fsm/bot/src/main/kotlin/App.kt create mode 100644 fsm/bot/src/main/kotlin/FSMPlaguBot.kt create mode 100644 fsm/bot/src/main/kotlin/HelloFSMPlugin.kt create mode 100644 fsm/plugin/build.gradle create mode 100644 fsm/plugin/publish.gradle create mode 100644 fsm/plugin/publish.kpsb create mode 100644 fsm/plugin/src/main/kotlin/FSMPlugin.kt create mode 100644 fsm/plugin/src/main/kotlin/FSMPluginSerializer.kt diff --git a/fsm/bot/build.gradle b/fsm/bot/build.gradle new file mode 100644 index 0000000..495dfa1 --- /dev/null +++ b/fsm/bot/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id "org.jetbrains.kotlin.plugin.serialization" + id 'application' +} + +project.group="$group" +project.version="$version" + +apply from: "publish.gradle" + +dependencies { + implementation libs.kt.stdlib + api libs.kt.coroutines + api libs.kt.serialization + api libs.jb.exposed.jdbc + + api libs.tgbotapi + api libs.microutils.repos.exposed + api libs.kslog + + api libs.sqlite + + testImplementation libs.kt.test.junit + + api project(":plagubot.bot") + api project(":plagubot.fsm.plugin") +} + +application { + mainClassName = 'dev.inmo.plagubot.AppKt' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + diff --git a/fsm/bot/publish.gradle b/fsm/bot/publish.gradle new file mode 100644 index 0000000..a65131e --- /dev/null +++ b/fsm/bot/publish.gradle @@ -0,0 +1,81 @@ +apply plugin: 'maven-publish' + +task javadocJar(type: Jar) { + from javadoc + classifier = 'javadoc' +} +task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + + artifact javadocJar + artifact sourcesJar + + pom { + resolveStrategy = Closure.DELEGATE_FIRST + + description = "Base PlaguBot project" + name = "PlaguBot Bot" + url = "https://github.com/InsanusMokrassar/PlaguBot" + + scm { + developerConnection = "scm:git:[fetch=]ssh://git@github.com/InsanusMokrassar/PlaguBot.git[push=]ssh://git@github.com/InsanusMokrassar/PlaguBot.git" + url = "ssh://git@github.com/InsanusMokrassar/PlaguBot.git" + } + + developers { + + developer { + id = "InsanusMokrassar" + name = "Aleksei Ovsiannikov" + email = "ovsyannikov.alexey95@gmail.com" + } + + } + + licenses { + + license { + name = "Apache Software License 2.0" + url = "https://github.com/InsanusMokrassar/PlaguBot/LICENSE" + } + + } + } + repositories { + if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) { + maven { + name = "sonatype" + url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER') + password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') + } + } + } + } + } + } +} + +if (project.hasProperty("signing.gnupg.keyName")) { + apply plugin: 'signing' + + signing { + useGpgCmd() + + sign publishing.publications + } + + task signAll { + tasks.withType(Sign).forEach { + dependsOn(it) + } + } +} diff --git a/fsm/bot/publish.kpsb b/fsm/bot/publish.kpsb new file mode 100644 index 0000000..b1382ea --- /dev/null +++ b/fsm/bot/publish.kpsb @@ -0,0 +1 @@ +{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/PlaguBot/LICENSE"}],"mavenConfig":{"name":"PlaguBot Bot","description":"Base PlaguBot project","url":"https://github.com/InsanusMokrassar/PlaguBot","vcsUrl":"ssh://git@github.com/InsanusMokrassar/PlaguBot.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"} \ No newline at end of file diff --git a/fsm/bot/src/main/kotlin/App.kt b/fsm/bot/src/main/kotlin/App.kt new file mode 100644 index 0000000..e23073c --- /dev/null +++ b/fsm/bot/src/main/kotlin/App.kt @@ -0,0 +1,25 @@ +package dev.inmo.plagubot + +import dev.inmo.kslog.common.KSLog +import dev.inmo.kslog.common.i +import dev.inmo.plagubot.config.Config +import dev.inmo.plagubot.config.defaultJsonFormat +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.json.jsonObject +import java.io.File + +/** + * This method by default expects one argument in [args] field: path to config + */ +@InternalSerializationApi +suspend fun main(args: Array) { + KSLog.default = KSLog("PlaguBot") + val (configPath) = args + val file = File(configPath) + KSLog.i("Start read config from ${file.absolutePath}") + val json = defaultJsonFormat.parseToJsonElement(file.readText()).jsonObject + val config = defaultJsonFormat.decodeFromJsonElement(Config.serializer(), json) + KSLog.i("Config has been read") + + FSMPlaguBot(json, config).start().join() +} diff --git a/fsm/bot/src/main/kotlin/FSMPlaguBot.kt b/fsm/bot/src/main/kotlin/FSMPlaguBot.kt new file mode 100644 index 0000000..b771485 --- /dev/null +++ b/fsm/bot/src/main/kotlin/FSMPlaguBot.kt @@ -0,0 +1,112 @@ +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.plagubot.fsm.FSMPlugin +import dev.inmo.tgbotapi.bot.ktor.telegramBot +import dev.inmo.tgbotapi.extensions.api.webhook.deleteWebhook +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling +import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonObject +import org.jetbrains.exposed.sql.Database +import org.koin.core.Koin +import org.koin.core.KoinApplication +import org.koin.core.context.GlobalContext +import org.koin.core.module.Module +import org.koin.core.scope.Scope +import org.koin.dsl.module + +val Scope.fsmPlagubot: FSMPlaguBot + get() = get() + +val Koin.fsmPlagubot: FSMPlaguBot + get() = get() + +@OptIn(Warning::class) +@Serializable +data class FSMPlaguBot( + private val json: JsonObject, + private val config: Config +) : FSMPlugin { + @Transient + private val bot = telegramBot(config.botToken) + + override fun Module.setupDI(database: Database, params: JsonObject) { + single { config } + single { config.plugins } + single { config.databaseConfig } + single { config.databaseConfig.database } + single { defaultJsonFormat } + single { this@FSMPlaguBot } + + includes( + config.plugins.mapNotNull { + runCatching { + module { + with(it) { + setupDI(database, params) + } + } + }.onFailure { e -> + logger.w("Unable to load DI part of $it", e) + }.getOrNull() + } + ) + } + + override suspend fun BehaviourContextWithFSM<*>.setupBotPlugin(koin: Koin) { + config.plugins.map { plugin -> + launch { + runCatchingSafely { + logger.i("Start loading of $plugin") + with(plugin) { + if (this is FSMPlugin) { + setupBotPlugin(koin) // use setupBotPlugin with BehaviourContextWithFSM as receiver + } else { + setupBotPlugin(koin) // use setupBotPlugin with BehaviourContext as receiver + } + } + }.onFailure { e -> + logger.w("Unable to load bot part of $plugin", e) + }.onSuccess { + logger.i("Complete loading of $plugin") + } + } + }.joinAll() + } + + /** + * This method will create an [Job] which will be the main [Job] of ran instance + */ + suspend fun start( + scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + ): Job { + logger.i("Start initialization") + val koinApp = KoinApplication.init() + koinApp.modules( + module { + setupDI(config.databaseConfig.database, json) + } + ) + logger.i("Modules loaded") + GlobalContext.startKoin(koinApp) + logger.i("Koin started") + lateinit var behaviourContext: BehaviourContext + bot.buildBehaviourWithFSM (scope = scope) { + logger.i("Start setup of bot part") + behaviourContext = this + setupBotPlugin(koinApp.koin) + deleteWebhook() + } + 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/fsm/bot/src/main/kotlin/HelloFSMPlugin.kt b/fsm/bot/src/main/kotlin/HelloFSMPlugin.kt new file mode 100644 index 0000000..baa6b5b --- /dev/null +++ b/fsm/bot/src/main/kotlin/HelloFSMPlugin.kt @@ -0,0 +1,37 @@ +package dev.inmo.plagubot + +import dev.inmo.kslog.common.* +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.behaviour_builder.triggers_handling.onCommand +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import org.jetbrains.exposed.sql.Database +import org.koin.core.Koin +import org.koin.core.module.Module + +@Serializable +@SerialName("Hello") +object HelloFSMPlugin : Plugin { + @Serializable + data class HelloPluginConfig( + val print: String + ) + + 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 } + logger.dS { getMe().toString() } + onCommand("hello_world") { + reply(it, "Hello :)") + } + } +} diff --git a/fsm/plugin/build.gradle b/fsm/plugin/build.gradle new file mode 100644 index 0000000..7d57c41 --- /dev/null +++ b/fsm/plugin/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id "org.jetbrains.kotlin.plugin.serialization" +} + +project.group="$group" +project.version="$version" + +apply from: "publish.gradle" + +dependencies { + implementation libs.kt.stdlib + + api libs.tgbotapi.behaviourBuilder.fsm + api project(":plagubot.plugin") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} diff --git a/fsm/plugin/publish.gradle b/fsm/plugin/publish.gradle new file mode 100644 index 0000000..ed05a9d --- /dev/null +++ b/fsm/plugin/publish.gradle @@ -0,0 +1,81 @@ +apply plugin: 'maven-publish' + +task javadocJar(type: Jar) { + from javadoc + classifier = 'javadoc' +} +task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + + artifact javadocJar + artifact sourcesJar + + pom { + resolveStrategy = Closure.DELEGATE_FIRST + + description = "Base dependency for whole PlaguBot project" + name = "PlaguBot Plugin" + url = "https://github.com/InsanusMokrassar/PlaguBot" + + scm { + developerConnection = "scm:git:[fetch=]ssh://git@github.com/InsanusMokrassar/PlaguBot.git[push=]ssh://git@github.com/InsanusMokrassar/PlaguBot.git" + url = "ssh://git@github.com/InsanusMokrassar/PlaguBot.git" + } + + developers { + + developer { + id = "InsanusMokrassar" + name = "Aleksei Ovsiannikov" + email = "ovsyannikov.alexey95@gmail.com" + } + + } + + licenses { + + license { + name = "Apache Software License 2.0" + url = "https://github.com/InsanusMokrassar/PlaguBot/LICENSE" + } + + } + } + repositories { + if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) { + maven { + name = "sonatype" + url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER') + password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') + } + } + } + } + } + } +} + +if (project.hasProperty("signing.gnupg.keyName")) { + apply plugin: 'signing' + + signing { + useGpgCmd() + + sign publishing.publications + } + + task signAll { + tasks.withType(Sign).forEach { + dependsOn(it) + } + } +} diff --git a/fsm/plugin/publish.kpsb b/fsm/plugin/publish.kpsb new file mode 100644 index 0000000..40f786b --- /dev/null +++ b/fsm/plugin/publish.kpsb @@ -0,0 +1 @@ +{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/PlaguBot/LICENSE"}],"mavenConfig":{"name":"PlaguBot Plugin","description":"Base dependency for whole PlaguBot project","url":"https://github.com/InsanusMokrassar/PlaguBot","vcsUrl":"ssh://git@github.com/InsanusMokrassar/PlaguBot.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"} \ No newline at end of file diff --git a/fsm/plugin/src/main/kotlin/FSMPlugin.kt b/fsm/plugin/src/main/kotlin/FSMPlugin.kt new file mode 100644 index 0000000..d089c15 --- /dev/null +++ b/fsm/plugin/src/main/kotlin/FSMPlugin.kt @@ -0,0 +1,24 @@ +package dev.inmo.plagubot.fsm + +import dev.inmo.plagubot.Plugin +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 +import org.koin.core.Koin +import org.koin.core.module.Module + +/** + * **ANY REALIZATION OF [FSMPlugin] MUST HAVE CONSTRUCTOR WITH ABSENCE OF INCOMING PARAMETERS** + * + * Use this interface for your bot. It is possible to use [kotlinx.serialization.SerialName] annotations on your plugins + * to set up short name for your plugin. Besides, simple name of your class will be used as key for deserialization + * too. + */ +@Serializable(FSMPluginSerializer::class) +interface FSMPlugin : Plugin { + suspend fun BehaviourContextWithFSM<*>.setupBotPlugin(koin: Koin) { + (this as BehaviourContext).setupBotPlugin(koin) + } +} diff --git a/fsm/plugin/src/main/kotlin/FSMPluginSerializer.kt b/fsm/plugin/src/main/kotlin/FSMPluginSerializer.kt new file mode 100644 index 0000000..40b6a51 --- /dev/null +++ b/fsm/plugin/src/main/kotlin/FSMPluginSerializer.kt @@ -0,0 +1,25 @@ +package dev.inmo.plagubot.fsm + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializer(FSMPlugin::class) +class FSMPluginSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = String.serializer().descriptor + + override fun deserialize(decoder: Decoder): FSMPlugin { + val kclass = Class.forName(decoder.decodeString()).kotlin + return (kclass.objectInstance ?: kclass.constructors.first { it.parameters.isEmpty() }.call()) as FSMPlugin + } + + override fun serialize(encoder: Encoder, value: FSMPlugin) { + encoder.encodeString( + value::class.java.canonicalName + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44f17bb..b6f45ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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/settings.gradle b/settings.gradle index ab5d8e7..24fa9bc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,13 @@ -String[] toInclude = [":bot", ":plugin"] +String[] toInclude = [":bot", ":plugin", ":fsm:bot", ":fsm: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) }