From d928db7e748f65ba13d2d81ff7436eb54d74e2ae Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 22 Sep 2024 15:29:17 +0600 Subject: [PATCH] rework of plagubot system --- CHANGELOG.md | 20 +- bot/src/main/kotlin/dev/inmo/plagubot/App.kt | 16 +- .../dev/inmo/plagubot/KoinExtensions.kt | 28 +++ .../main/kotlin/dev/inmo/plagubot/PlaguBot.kt | 198 +++++++++--------- .../kotlin/dev/inmo/plagubot/config/Config.kt | 5 +- .../dev/inmo/plagubot/config/JsonFormat.kt | 2 + .../kotlin/dev/inmo/plagubot/HelloPlugin.kt | 8 +- .../dev/inmo/plagubot/KoinExtensions.kt | 11 + .../main/kotlin/dev/inmo/plagubot/Plugin.kt | 18 +- 9 files changed, 161 insertions(+), 145 deletions(-) create mode 100644 bot/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt rename {bot => plugin}/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt (85%) create mode 100644 plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 09199a9..0f9f86d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,26 @@ ## 10.0.0 +**OVERALL LOGIC OF PLAGUBOT INITIALIZATION AND WORK HAS BEEN CHANGED** + +First of all, since this update `PlaguBot` will use default `StartPlugin` logic and will be built on top of it. +All special methods of `Plugin` will be called from one of `PlaguBot` initialization phases: + +* `setupBotClient` will be called from `single` initialization of `telegramBot` (in `setupDI` phase) +* `setupBotPlugin` will be called from `startPlugin` method in time of `buildBehaviourWithFSM` initialization + * `Plugin`: - * `Plugin#setupBotPlugin` now will call start plugin by default + * Extension `Module.setupDI(Database,JsonObject)` has been dropped. Use `database` extension in `Module.setupDI(JsonObject)` +* `Bot`: + * `dev.inmo.plagubot.config.Config` lost its `plugins` section. Now you may retrieve plugins from `Koin` only + * `defaultJsonFormat` became `Warning` feature due to the fact of its fully default nature + * `PlaguBot` lost old `start` method and took two new: with `args` as `Array` and `initialConfig` as `JsonObject` + +**Migration:** + +* If you are running bot and doing it using `StartPlugin` launcher, add `dev.inmo.plagubot.PlaguBot` explicitly +* In plugins: replace your `setupDI` overrides with `Database` as argument by the same one, but `database` will be +available as extension in `single` or `factory` calls (as extension to `Scope` and `Koin`) ## 9.3.0 diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt index 6f28c13..bb246a0 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt @@ -1,25 +1,11 @@ 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") - - PlaguBot(json, config).start().join() + PlaguBot.start(args).join() } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt b/bot/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt new file mode 100644 index 0000000..47004e8 --- /dev/null +++ b/bot/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt @@ -0,0 +1,28 @@ +package dev.inmo.plagubot + +import org.koin.core.Koin +import org.koin.core.definition.Definition +import org.koin.core.module.Module +import org.koin.core.qualifier.Qualifier +import org.koin.core.qualifier.StringQualifier +import org.koin.core.scope.Scope + + +val Scope.plagubot: PlaguBot + get() = get() + +val Koin.plagubot: PlaguBot + get() = get() + +private val pluginsQualifier = StringQualifier("plagubotPlugins") +internal fun Module.singlePlugins( + createdAtStart: Boolean = false, + definition: Definition> +) = single(pluginsQualifier, createdAtStart, definition) +val Scope.plugins: List + get() = get(pluginsQualifier) + +val Koin.plugins: List + get() = get(pluginsQualifier) + + diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt index 1258264..2d99565 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt @@ -7,6 +7,7 @@ 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.micro_utils.koin.getAllDistinct +import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin import dev.inmo.plagubot.config.* import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder @@ -16,49 +17,30 @@ 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.json.JsonObject -import org.jetbrains.exposed.sql.Database +import kotlinx.serialization.json.* 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.plagubot: PlaguBot - get() = get() - -val Koin.plagubot: PlaguBot - get() = get() +import java.io.File @OptIn(Warning::class) @Serializable -data class PlaguBot( - private val json: JsonObject, - private val config: Config -) : Plugin { +object PlaguBot : Plugin { override fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) { - config.botPlugins.forEach { + scope.plugins.filter { it !== this@PlaguBot }.forEach { with(it) { setupBotClient(scope, params) } } } - override fun KtorRequestsExecutorBuilder.setupBotClient() { - config.botPlugins.forEach { - with(it) { - setupBotClient() - } - } - } override fun Module.setupDI(config: JsonObject) { - single { this@PlaguBot.config } - single { this@PlaguBot.config.plugins } - single { this@PlaguBot.config.databaseConfig } - single { this@PlaguBot.config.databaseConfig.database } - single { defaultJsonFormat } + single { get().decodeFromJsonElement(Config.serializer(), config) } + single { config } + single { get().databaseConfig } + single { get().databaseConfig.database } single { this@PlaguBot } + singlePlugins { get().plugins.filterIsInstance() } single { val config = get() telegramBot( @@ -66,48 +48,63 @@ data class PlaguBot( testServer = config.testServer, apiUrl = config.botApiServer ) { - setupBotClient(this@single, json) + setupBotClient(this@single, get()) } } } - override fun Module.setupDI(database: Database, params: JsonObject) { - setupDI(params) - - includes( - config.botPlugins.mapNotNull { - runCatching { - module { - with(it) { - setupDI(database, params) - } - } - }.onFailure { e -> - logger.w(e) { "Unable to load DI part of $it" } - }.getOrNull() - } - ) - } - + /** + * Getting all [OnStartContextsConflictResolver], [OnUpdateContextsConflictResolver], [StatesManager] and [DefaultStatesManagerRepo] + * and pass them into [buildBehaviourWithFSM] on top of [TelegramBot] took from [koin]. In time of + * [buildBehaviourWithFSM] configuration will call [setupBotPlugin] and [deleteWebhook]. + * + * After all preparation, the result of [buildBehaviourWithFSM] will be passed to [startGettingOfUpdatesByLongPolling] + * as [CoroutineScope] and [UpdatesFilter]. + * + * The [Job] took from [startGettingOfUpdatesByLongPolling] will be used to prevent app stopping by calling [Job.join] + * on it + */ override suspend fun startPlugin(koin: Koin) { super.startPlugin(koin) + val scope = koin.get() - config.plugins.forEach { plugin -> - runCatchingSafely { - logger.i { "Starting of $plugin common logic" } - with(plugin) { - startPlugin(koin) - } - }.onFailure { e -> - logger.w(e) { "Unable to load common logic of $plugin" } - }.onSuccess { - logger.i { "Complete loading of $plugin common logic" } + lateinit var behaviourContext: BehaviourContext + val onStartContextsConflictResolver by lazy { koin.getAllDistinct() } + val onUpdateContextsConflictResolver by lazy { koin.getAllDistinct() } + val bot = koin.get() + bot.buildBehaviourWithFSM( + scope = scope, + defaultExceptionsHandler = { + logger.e("Something went wrong", it) + }, + statesManager = koin.getOrNull>() ?: DefaultStatesManager( + koin.getOrNull>() ?: InMemoryDefaultStatesManagerRepo(), + onStartContextsConflictResolver = { old, new -> onStartContextsConflictResolver.firstNotNullOfOrNull { it(old, new) } ?: false }, + onUpdateContextsConflictResolver = { old, new, currentNew -> onUpdateContextsConflictResolver.firstNotNullOfOrNull { it(old, new, currentNew) } ?: false } + ), + onStateHandlingErrorHandler = koin.getOrNull>() ?: { state, e -> + logger.eS(e) { "Unable to handle state $state" } + null } - } + ) { + logger.i("Start setup of bot part") + behaviourContext = this + setupBotPlugin(koin) + deleteWebhook() + }.start() + logger.i("Behaviour builder has been setup") + + bot.startGettingOfUpdatesByLongPolling(scope = behaviourContext, updatesFilter = behaviourContext).also { + logger.i("Long polling has been started") + }.join() } + /** + * Initializing [Plugin]s from [koin] took by [plugins] extension. [PlaguBot] itself will be filtered out from + * list of plugins to be inited + */ override suspend fun BehaviourContextWithFSM.setupBotPlugin(koin: Koin) { - config.botPlugins.forEach { plugin -> + koin.plugins.filter { it !== this@PlaguBot }.forEach { plugin -> runCatchingSafely { logger.i("Start loading of $plugin") with(plugin) { @@ -122,50 +119,47 @@ data class PlaguBot( } /** - * This method will create an [Job] which will be the main [Job] of ran instance + * Starting plugins system using [StartLauncherPlugin.start]. In time of parsing [initialJson] [PlaguBot] may + * add itself in its `plugins` section in case of its absence there. So, by launching this [start] it is guaranteed + * that [PlaguBot] will be in list of plugins to be loaded by [StartLauncherPlugin] */ - suspend fun start( - scope: CoroutineScope = CoroutineScope(Dispatchers.Default) - ): Job { - logger.i("Start initialization") - val koinApp = KoinApplication.init() - koinApp.modules( - module { - setupDI(config.databaseConfig.database, json) - } - ) - logger.i("Modules loaded. Starting koin") - GlobalContext.startKoin(koinApp) - logger.i("Koin started. Starting plugins common logic") - startPlugin(koinApp.koin) - logger.i("Plugins common logic started. Starting setup of bot logic part") - lateinit var behaviourContext: BehaviourContext - val onStartContextsConflictResolver by lazy { koinApp.koin.getAllDistinct() } - val onUpdateContextsConflictResolver by lazy { koinApp.koin.getAllDistinct() } - val bot = koinApp.koin.get() - bot.buildBehaviourWithFSM( - scope = scope, - defaultExceptionsHandler = { - logger.e("Something went wrong", it) - }, - statesManager = koinApp.koin.getOrNull>() ?: DefaultStatesManager( - koinApp.koin.getOrNull>() ?: InMemoryDefaultStatesManagerRepo(), - onStartContextsConflictResolver = { old, new -> onStartContextsConflictResolver.firstNotNullOfOrNull { it(old, new) } ?: false }, - onUpdateContextsConflictResolver = { old, new, currentNew -> onUpdateContextsConflictResolver.firstNotNullOfOrNull { it(old, new, currentNew) } ?: 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") + suspend fun start(initialJson: JsonObject): Job { + val initialConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), initialJson) + + KSLog.i("Config has been read") + + // Adding of PlaguBot when it absent in config + val (resultJson, resultConfig) = if (PlaguBot in initialConfig.plugins) { + KSLog.i("Initial config contains PlaguBot, pass config as is to StartLauncherPlugin") + initialJson to initialConfig + } else { + KSLog.i("Start fixing of PlaguBot absence. If PlaguBot has been skipped by some reason, use dev.inmo.micro_utils.startup.launcher.main as startup point or StartLauncherPlugin directly") + val resultJson = JsonObject( + initialJson + Pair("plugins", JsonArray(initialJson["plugins"]!!.jsonArray + JsonPrimitive(PlaguBot::class.qualifiedName!!))) + ) + val resultConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), resultJson) + resultJson to resultConfig } + + KSLog.i("Config initialization done. Passing config to StartLauncherPlugin") + return StartLauncherPlugin.start( + resultConfig, + resultJson + ).koin.get().coroutineContext.job + } + + /** + * Accepts single argument in [args] which will be interpreted as [File] path with [StartLauncherPlugin] + * configuration content. After reading of that file as [JsonObject] will pass it in [start] with [JsonObject] as + * argument + */ + suspend fun start(args: Array): Job { + KSLog.default = KSLog("PlaguBot") + val (configPath) = args + val file = File(configPath) + KSLog.i("Start read config from ${file.absolutePath}") + + val initialJson = defaultJsonFormat.parseToJsonElement(file.readText()).jsonObject + return start(initialJson) } } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt index 2344e20..8b82cd1 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt @@ -11,11 +11,8 @@ import kotlinx.serialization.Serializable @Serializable data class Config( val botToken: String, - val plugins: List, @SerialName("database") val databaseConfig: DatabaseConfig = DatabaseConfig(), val botApiServer: String = telegramBotAPIDefaultUrl, val testServer: Boolean = false -) { - val botPlugins = plugins.filterIsInstance() -} +) diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt index 4202346..10879e1 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt @@ -1,7 +1,9 @@ package dev.inmo.plagubot.config +import dev.inmo.micro_utils.common.Warning import kotlinx.serialization.json.Json +@Warning("This format will not be configured throw StartPlugin system. Use it will caution, it has no any configured things") val defaultJsonFormat = Json { ignoreUnknownKeys = true } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt similarity index 85% rename from bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt rename to plugin/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt index a2bf9d4..ecb53f1 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt @@ -2,22 +2,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.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.IdChatIdentifier import kotlinx.coroutines.flow.first 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 @@ -29,9 +25,9 @@ object HelloPlugin : Plugin { val print: String ) - override fun Module.setupDI(database: Database, params: JsonObject) { + override fun Module.setupDI(config: JsonObject) { single { - get().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null) + get().decodeFromJsonElement(HelloPluginConfig.serializer(), config["helloPlugin"] ?: return@single null) } } diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt new file mode 100644 index 0000000..d3ed49d --- /dev/null +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt @@ -0,0 +1,11 @@ +package dev.inmo.plagubot + +import org.jetbrains.exposed.sql.Database +import org.koin.core.Koin +import org.koin.core.scope.Scope + +val Scope.database: Database + get() = get() + +val Koin.database: Database + get() = get() diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt index 7a8ed97..769f184 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt @@ -7,9 +7,7 @@ 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 import org.koin.core.scope.Scope /** @@ -32,27 +30,13 @@ interface Plugin : StartPlugin { */ fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) = setupBotClient() - /** - * This method will be called when this plugin should configure di module based on the incoming params - */ - fun Module.setupDI( - database: Database, - params: JsonObject - ) { - setupDI(params) - } - /** * 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] - * - * Besides, this method by default will call [startPlugin] */ suspend fun BehaviourContext.setupBotPlugin( koin: Koin - ) { - startPlugin(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