diff --git a/CHANGELOG.md b/CHANGELOG.md index fee1b27..2d6f3c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 11.0.0 + +* `Versions`: + * `kotlin`: `2.3.10` + * `serialization`: `1.10.0` + * `microutils`: `0.29.1` + * `tgbotapi`: `32.0.0` + * `exposed`: `1.1.1` + * `sqlite`: `3.51.2.0` + ## 10.10.0 * `Versions`: diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt index f0cafad..26e1084 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt @@ -2,12 +2,12 @@ package dev.inmo.plagubot import dev.inmo.kslog.common.* import dev.inmo.micro_utils.common.Warning +import dev.inmo.micro_utils.coroutines.runCatchingLogging import dev.inmo.micro_utils.coroutines.runCatchingSafely 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.pagination.utils.getAll import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin import dev.inmo.plagubot.config.* import dev.inmo.tgbotapi.bot.TelegramBot @@ -24,9 +24,39 @@ import org.koin.core.module.Module import org.koin.core.scope.Scope import java.io.File +/** + * Central plugin-object that: + * - Provides DI bindings for the bot, configuration and optional database + * - Creates and configures Telegram bot client + * - Builds BehaviourBuilder with FSM support and launches long polling + * - Loads and initializes other plugins + * - Exposes several start(...) overloads to bootstrap the application from JSON or strongly-typed configs + * + * Typical usage: + * - Prepare JSON or typed configs + * - Call one of start(...) methods + * - The returned Job represents the bot lifecycle; awaiting it keeps the app running + */ @OptIn(Warning::class) @Serializable object PlaguBot : Plugin { + /** + * JSON format used for encoding/decoding PlaguBot-related configuration. + * + * Note: + * - This property is not reactive. Set it only before starting the bot to affect configuration parsing/serialization. + */ + var defaultJsonFormat = dev.inmo.plagubot.config.defaultJsonFormat + @Warning("This is not reactive set. Use this only BEFORE starting of bot") set + + /** + * Allows other plugins to customize the [KtorRequestsExecutorBuilder] used by the bot client. + * + * For each plugin in DI (excluding this object), calls its own setupBotClient, passing the same [scope] and [params]. + * + * @param scope Koin scope where the bot is being created + * @param params Raw JSON params of the bot part of the configuration + */ override fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) { scope.plugins.filter { it !== this@PlaguBot }.forEach { with(it) { @@ -35,6 +65,19 @@ object PlaguBot : Plugin { } } + /** + * Sets up application DI for the bot. + * + * Registers: + * - Decoded typed [Config] from provided [config] JSON + * - Raw [JsonObject] config itself for low-level access + * - Database-related config and database instance (if present in [Config]) + * - This [PlaguBot] plugin instance + * - All plugins declared in launcher config as DI singletons + * - Configured [TelegramBot] built via [telegramBot] and customized by all plugins' setupBotClient + * + * @param config Raw PlaguBot JSON configuration + */ override fun Module.setupDI(config: JsonObject) { single { get().decodeFromJsonElement(Config.serializer(), config) } single { config } @@ -55,15 +98,14 @@ object PlaguBot : Plugin { } /** - * 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]. + * Builds and starts the bot behaviour and FSM. * - * 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 + * Flow: + * - Collects [OnStartContextsConflictResolver], [OnUpdateContextsConflictResolver], [StatesManager] and [DefaultStatesManagerRepo] from DI (or creates defaults) + * - Builds BehaviourBuilder with FSM, logs errors, and calls [setupBotPlugin] for all plugins + * - Ensures webhook is removed before long polling with [deleteWebhook] + * - Starts long polling via [startGettingOfUpdatesByLongPolling] using the created behaviour context as scope and filter + * - Awaits the long-polling [Job] to keep the app alive */ override suspend fun startPlugin(koin: Koin) { super.startPlugin(koin) @@ -102,12 +144,18 @@ object PlaguBot : Plugin { } /** - * Initializing [Plugin]s from [koin] took by [plugins] extension. [PlaguBot] itself will be filtered out from - * list of plugins to be inited + * Initializes and loads all other [Plugin]s from DI for the bot runtime. + * + * Each plugin is: + * - Logged as loading started + * - Executed via plugin.setupBotPlugin(...) inside a safe block + * - Logged on failure (as warning) or success (as info) + * + * [PlaguBot] itself is excluded from this list. */ override suspend fun BehaviourContextWithFSM.setupBotPlugin(koin: Koin) { koin.plugins.filter { it !== this@PlaguBot }.forEach { plugin -> - runCatchingSafely { + runCatchingLogging(logger = logger) { logger.i("Start loading of $plugin") with(plugin) { setupBotPlugin(koin) @@ -121,23 +169,45 @@ object PlaguBot : Plugin { } /** - * 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] + * Starts the application using raw JSON and a launcher config, optionally overriding PlaguBot config. + * + * Behavior: + * - If [PlaguBot] is not present in [config.plugins], injects it into the plugins list + * - Optionally merges [plaguBotConfig] into [json] + * - Delegates to [StartLauncherPlugin.start] and returns the lifecycle [Job] + * + * @param json Raw JSON that may contain both launcher and PlaguBot configs + * @param config Parsed launcher configuration + * @param plaguBotConfig Optional typed PlaguBot config to merge/add + * @return Job representing the bot lifecycle */ - suspend fun start(initialJson: JsonObject): Job { - val initialConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), initialJson) - + suspend fun start( + json: JsonObject, + config: dev.inmo.micro_utils.startup.launcher.Config, + plaguBotConfig: Config? = null + ): Job { KSLog.i("Config has been read") // Adding of PlaguBot when it absent in config - val (resultJson, resultConfig) = if (PlaguBot in initialConfig.plugins) { + val (resultJson, resultConfig) = if (PlaguBot in config.plugins) { KSLog.i("Initial config contains PlaguBot, pass config as is to StartLauncherPlugin") - initialJson to initialConfig + json to config } 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 encodedPlaguBotConfig = plaguBotConfig ?.let { + defaultJsonFormat.encodeToJsonElement(Config.serializer(), it).jsonObject + } ?: JsonObject(emptyMap()) + val resultJson = JsonObject( - initialJson + Pair("plugins", JsonArray(initialJson["plugins"]!!.jsonArray + JsonPrimitive(PlaguBot::class.qualifiedName!!))) + encodedPlaguBotConfig + JsonObject( + json + Pair( + "plugins", + JsonArray( + (json["plugins"] as? JsonArray ?: JsonArray(emptyList())) + JsonPrimitive(PlaguBot::class.qualifiedName!!) + ) + ) + ) ) val resultConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), resultJson) resultJson to resultConfig @@ -151,9 +221,91 @@ object PlaguBot : Plugin { } /** - * 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 + * Starts the application using typed launcher config and typed PlaguBot config. + * + * Behavior: + * - Merges both configs into a single JSON + * - Ensures [PlaguBot] is present in plugins list + * - Delegates to [start] that accepts JSON and launcher config + * + * @param pluginsConfig Launcher configuration + * @param plaguBotConfig Typed PlaguBot configuration + * @return Job representing the bot lifecycle + */ + suspend fun start( + pluginsConfig: dev.inmo.micro_utils.startup.launcher.Config, + plaguBotConfig: Config + ): Job { + val json = JsonObject( + defaultJsonFormat.encodeToJsonElement( + dev.inmo.micro_utils.startup.launcher.Config.serializer(), + pluginsConfig + ).jsonObject + defaultJsonFormat.encodeToJsonElement( + Config.serializer(), + plaguBotConfig + ).jsonObject + ) + val pluginsConfig = if (pluginsConfig.plugins.contains(PlaguBot)) { + pluginsConfig + } else { + pluginsConfig.copy( + plugins = pluginsConfig.plugins + PlaguBot, + ) + } + + return start(json, pluginsConfig) + } + + /** + * Starts the application using raw JSON and a typed PlaguBot config. + * + * Behavior: + * - Encodes [plaguBotConfig] and merges it into [initialJson] + * - Delegates to [start] that takes a JSON only + * + * @param initialJson Initial JSON to start from + * @param plaguBotConfig Typed PlaguBot configuration that will be encoded and merged + * @return Job representing the bot lifecycle + */ + suspend fun start( + initialJson: JsonObject, + plaguBotConfig: Config + ): Job { + val encodedPlaguBotConfig = defaultJsonFormat.encodeToJsonElement(Config.serializer(), plaguBotConfig).jsonObject + + return start( + JsonObject(initialJson + encodedPlaguBotConfig) + ) + } + + /** + * Starts the plugins system using [StartLauncherPlugin.start]. + * + * Parsing notes: + * - [initialJson] is decoded into launcher config + * - If [PlaguBot] is missing from the plugins list, it will be added automatically + * + * By using this method it is guaranteed that [PlaguBot] will be included into the set of plugins to launch. + * + * @param initialJson Raw JSON that includes launcher configuration (and optionally PlaguBot config) + * @return Job representing the bot lifecycle + */ + suspend fun start(initialJson: JsonObject): Job { + val initialConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), initialJson) + + return start(initialJson, initialConfig) + } + + /** + * Starts the application from CLI arguments. + * + * Expects: + * - Exactly one argument: path to a file with JSON configuration + * + * The file is read, parsed as [JsonObject], and passed to [start(JsonObject)]. + * + * @param args Command-line arguments; first element is a path to the config file + * @return Job representing the bot lifecycle */ suspend fun start(args: Array): Job { KSLog.default = KSLog("PlaguBot") diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/DatabaseConfig.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/DatabaseConfig.kt index 5a8cd0e..7e0cb4b 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/DatabaseConfig.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/DatabaseConfig.kt @@ -5,8 +5,8 @@ import dev.inmo.kslog.common.logger import kotlinx.coroutines.delay import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.transactions.transactionManager +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.transactions.transactionManager import org.koin.core.scope.Scope import org.sqlite.JDBC import java.lang.Exception diff --git a/build.gradle b/build.gradle index 683279b..b42291e 100644 --- a/build.gradle +++ b/build.gradle @@ -34,8 +34,8 @@ allprojects { repositories { mavenCentral() mavenLocal() - maven { url 'https://jitpack.io' } maven { url "https://nexus.inmo.dev/repository/maven-releases/" } + maven { url 'https://jitpack.io' } } } diff --git a/changelog_parser.sh b/changelog_parser.sh old mode 100644 new mode 100755 diff --git a/gradle.properties b/gradle.properties index 4b61d3d..6c13cfe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ kotlin.js.generate.externals=true kotlin.incremental=true group=dev.inmo -version=10.10.0 +version=11.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67f526d..75ee4a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,24 +1,24 @@ [versions] -kt = "2.2.21" -kt-serialization = "1.9.0" +kt = "2.3.10" +kt-serialization = "1.10.0" kt-coroutines = "1.10.2" -microutils = "0.26.8" -tgbotapi = "30.0.2" +microutils = "0.29.1" +tgbotapi = "32.0.0" -ksp = "2.3.2" +ksp = "2.3.6" -jb-exposed = "0.61.0" +jb-exposed = "1.1.1" jb-dokka = "2.1.0" -sqlite = "3.50.1.0" +sqlite = "3.51.2.0" gh-release = "2.5.2" koin = "4.1.1" -nmcp = "1.3.0" +nmcp = "1.4.4" [libraries] diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/KoinDatabaseExtensions.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/KoinDatabaseExtensions.kt index d3ed49d..ee0578d 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/KoinDatabaseExtensions.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/KoinDatabaseExtensions.kt @@ -1,6 +1,6 @@ package dev.inmo.plagubot -import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.v1.jdbc.Database import org.koin.core.Koin import org.koin.core.scope.Scope diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt index 18585df..df334d9 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/KoinExtensions.kt @@ -5,7 +5,6 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.serializer -import org.jetbrains.exposed.sql.Database import org.koin.core.Koin import org.koin.core.module.Module import org.koin.core.scope.Scope diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt index 769f184..4c77132 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt @@ -28,6 +28,7 @@ interface Plugin : StartPlugin { * @param scope The scope of [org.koin.core.module.Module.single] of bot definition * @param params Params (in fact, the whole bot config) */ + @Suppress("DEPRECATION") fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) = setupBotClient() /** diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/PluginSerializer.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/PluginSerializer.kt index d3d89a4..9067412 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/PluginSerializer.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/PluginSerializer.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -@Serializer(Plugin::class) class PluginSerializer : KSerializer { override val descriptor: SerialDescriptor get() = String.serializer().descriptor