Merge pull request #106 from InsanusMokrassar/10.0.0

10.0.0
This commit is contained in:
InsanusMokrassar 2024-09-22 19:40:02 +06:00 committed by GitHub
commit f551f471d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 221 additions and 143 deletions

View File

@ -1,5 +1,28 @@
# Changelog # Changelog
## 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`:
* 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<String>` 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 ## 9.3.0
* `Bot`: * `Bot`:

View File

@ -1,25 +1,11 @@
package dev.inmo.plagubot 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.InternalSerializationApi
import kotlinx.serialization.json.jsonObject
import java.io.File
/** /**
* This method by default expects one argument in [args] field: path to config * This method by default expects one argument in [args] field: path to config
*/ */
@InternalSerializationApi @InternalSerializationApi
suspend fun main(args: Array<String>) { suspend fun main(args: Array<String>) {
KSLog.default = KSLog("PlaguBot") PlaguBot.start(args).join()
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()
} }

View File

@ -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<List<Plugin>>
) = single(pluginsQualifier, createdAtStart, definition)
val Scope.plugins: List<Plugin>
get() = get(pluginsQualifier)
val Koin.plugins: List<Plugin>
get() = get(pluginsQualifier)

View File

@ -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.StatesManager
import dev.inmo.micro_utils.fsm.common.managers.* import dev.inmo.micro_utils.fsm.common.managers.*
import dev.inmo.micro_utils.koin.getAllDistinct import dev.inmo.micro_utils.koin.getAllDistinct
import dev.inmo.micro_utils.startup.launcher.StartLauncherPlugin
import dev.inmo.plagubot.config.* import dev.inmo.plagubot.config.*
import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder 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 dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin 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.module.Module
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.dsl.module import java.io.File
val Scope.plagubot: PlaguBot
get() = get()
val Koin.plagubot: PlaguBot
get() = get()
@OptIn(Warning::class) @OptIn(Warning::class)
@Serializable @Serializable
data class PlaguBot( object PlaguBot : Plugin {
private val json: JsonObject,
private val config: Config
) : Plugin {
override fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) { override fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) {
config.botPlugins.forEach { scope.plugins.filter { it !== this@PlaguBot }.forEach {
with(it) { with(it) {
setupBotClient(scope, params) setupBotClient(scope, params)
} }
} }
} }
override fun KtorRequestsExecutorBuilder.setupBotClient() {
config.botPlugins.forEach {
with(it) {
setupBotClient()
}
}
}
override fun Module.setupDI(config: JsonObject) { override fun Module.setupDI(config: JsonObject) {
single { this@PlaguBot.config } single { get<Json>().decodeFromJsonElement(Config.serializer(), config) }
single { this@PlaguBot.config.plugins } single { config }
single { this@PlaguBot.config.databaseConfig } single { get<Config>().databaseConfig }
single { this@PlaguBot.config.databaseConfig.database } single { get<Config>().databaseConfig.database }
single { defaultJsonFormat }
single { this@PlaguBot } single { this@PlaguBot }
singlePlugins { get<dev.inmo.micro_utils.startup.launcher.Config>().plugins.filterIsInstance<Plugin>() }
single { single {
val config = get<Config>() val config = get<Config>()
telegramBot( telegramBot(
@ -66,48 +48,63 @@ data class PlaguBot(
testServer = config.testServer, testServer = config.testServer,
apiUrl = config.botApiServer apiUrl = config.botApiServer
) { ) {
setupBotClient(this@single, json) setupBotClient(this@single, get<JsonObject>())
} }
} }
} }
override fun Module.setupDI(database: Database, params: JsonObject) { /**
setupDI(params) * Getting all [OnStartContextsConflictResolver], [OnUpdateContextsConflictResolver], [StatesManager] and [DefaultStatesManagerRepo]
* and pass them into [buildBehaviourWithFSM] on top of [TelegramBot] took from [koin]. In time of
includes( * [buildBehaviourWithFSM] configuration will call [setupBotPlugin] and [deleteWebhook].
config.botPlugins.mapNotNull { *
runCatching { * After all preparation, the result of [buildBehaviourWithFSM] will be passed to [startGettingOfUpdatesByLongPolling]
module { * as [CoroutineScope] and [UpdatesFilter].
with(it) { *
setupDI(database, params) * The [Job] took from [startGettingOfUpdatesByLongPolling] will be used to prevent app stopping by calling [Job.join]
} * on it
} */
}.onFailure { e ->
logger.w(e) { "Unable to load DI part of $it" }
}.getOrNull()
}
)
}
override suspend fun startPlugin(koin: Koin) { override suspend fun startPlugin(koin: Koin) {
super.startPlugin(koin) super.startPlugin(koin)
val scope = koin.get<CoroutineScope>()
config.plugins.forEach { plugin -> lateinit var behaviourContext: BehaviourContext
runCatchingSafely { val onStartContextsConflictResolver by lazy { koin.getAllDistinct<OnStartContextsConflictResolver>() }
logger.i { "Starting of $plugin common logic" } val onUpdateContextsConflictResolver by lazy { koin.getAllDistinct<OnUpdateContextsConflictResolver>() }
with(plugin) { val bot = koin.get<TelegramBot>()
startPlugin(koin) bot.buildBehaviourWithFSM(
} scope = scope,
}.onFailure { e -> defaultExceptionsHandler = {
logger.w(e) { "Unable to load common logic of $plugin" } logger.e("Something went wrong", it)
}.onSuccess { },
logger.i { "Complete loading of $plugin common logic" } statesManager = koin.getOrNull<StatesManager<State>>() ?: DefaultStatesManager(
koin.getOrNull<DefaultStatesManagerRepo<State>>() ?: InMemoryDefaultStatesManagerRepo<State>(),
onStartContextsConflictResolver = { old, new -> onStartContextsConflictResolver.firstNotNullOfOrNull { it(old, new) } ?: false },
onUpdateContextsConflictResolver = { old, new, currentNew -> onUpdateContextsConflictResolver.firstNotNullOfOrNull { it(old, new, currentNew) } ?: false }
),
onStateHandlingErrorHandler = koin.getOrNull<StateHandlingErrorHandler<State>>() ?: { 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<State>.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
config.botPlugins.forEach { plugin -> koin.plugins.filter { it !== this@PlaguBot }.forEach { plugin ->
runCatchingSafely { runCatchingSafely {
logger.i("Start loading of $plugin") logger.i("Start loading of $plugin")
with(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( suspend fun start(initialJson: JsonObject): Job {
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) val initialConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), initialJson)
): Job {
logger.i("Start initialization") KSLog.i("Config has been read")
val koinApp = KoinApplication.init()
koinApp.modules( // Adding of PlaguBot when it absent in config
module { val (resultJson, resultConfig) = if (PlaguBot in initialConfig.plugins) {
setupDI(config.databaseConfig.database, json) KSLog.i("Initial config contains PlaguBot, pass config as is to StartLauncherPlugin")
} initialJson to initialConfig
) } else {
logger.i("Modules loaded. Starting koin") 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")
GlobalContext.startKoin(koinApp) val resultJson = JsonObject(
logger.i("Koin started. Starting plugins common logic") initialJson + Pair("plugins", JsonArray(initialJson["plugins"]!!.jsonArray + JsonPrimitive(PlaguBot::class.qualifiedName!!)))
startPlugin(koinApp.koin) )
logger.i("Plugins common logic started. Starting setup of bot logic part") val resultConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), resultJson)
lateinit var behaviourContext: BehaviourContext resultJson to resultConfig
val onStartContextsConflictResolver by lazy { koinApp.koin.getAllDistinct<OnStartContextsConflictResolver>() }
val onUpdateContextsConflictResolver by lazy { koinApp.koin.getAllDistinct<OnUpdateContextsConflictResolver>() }
val bot = koinApp.koin.get<TelegramBot>()
bot.buildBehaviourWithFSM(
scope = scope,
defaultExceptionsHandler = {
logger.e("Something went wrong", it)
},
statesManager = koinApp.koin.getOrNull<StatesManager<State>>() ?: DefaultStatesManager(
koinApp.koin.getOrNull<DefaultStatesManagerRepo<State>>() ?: InMemoryDefaultStatesManagerRepo<State>(),
onStartContextsConflictResolver = { old, new -> onStartContextsConflictResolver.firstNotNullOfOrNull { it(old, new) } ?: false },
onUpdateContextsConflictResolver = { old, new, currentNew -> onUpdateContextsConflictResolver.firstNotNullOfOrNull { it(old, new, currentNew) } ?: false }
),
onStateHandlingErrorHandler = koinApp.koin.getOrNull<StateHandlingErrorHandler<State>>() ?: { 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")
} }
KSLog.i("Config initialization done. Passing config to StartLauncherPlugin")
return StartLauncherPlugin.start(
resultConfig,
resultJson
).koin.get<CoroutineScope>().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<String>): 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)
} }
} }

View File

@ -11,11 +11,8 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Config( data class Config(
val botToken: String, val botToken: String,
val plugins: List<StartPlugin>,
@SerialName("database") @SerialName("database")
val databaseConfig: DatabaseConfig = DatabaseConfig(), val databaseConfig: DatabaseConfig = DatabaseConfig(),
val botApiServer: String = telegramBotAPIDefaultUrl, val botApiServer: String = telegramBotAPIDefaultUrl,
val testServer: Boolean = false val testServer: Boolean = false
) { )
val botPlugins = plugins.filterIsInstance<Plugin>()
}

View File

@ -1,7 +1,9 @@
package dev.inmo.plagubot.config package dev.inmo.plagubot.config
import dev.inmo.micro_utils.common.Warning
import kotlinx.serialization.json.Json 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 { val defaultJsonFormat = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }

View File

@ -5,4 +5,4 @@ kotlin.js.generate.externals=true
kotlin.incremental=true kotlin.incremental=true
group=dev.inmo group=dev.inmo
version=9.3.0 version=10.0.0

View File

@ -2,22 +2,18 @@ package dev.inmo.plagubot
import dev.inmo.kslog.common.* import dev.inmo.kslog.common.*
import dev.inmo.micro_utils.fsm.common.State 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.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.sendMessage import dev.inmo.tgbotapi.extensions.api.send.sendMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.* 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.expectations.waitTextMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand 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 dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
@ -29,10 +25,8 @@ object HelloPlugin : Plugin {
val print: String val print: String
) )
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(config: JsonObject) {
single { registerConfig<HelloPluginConfig>("helloPlugin") { null }
get<Json>().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null)
}
} }
private sealed interface InternalFSMState : State { private sealed interface InternalFSMState : State {
@ -47,7 +41,7 @@ object HelloPlugin : Plugin {
} }
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
val toPrint = koin.getOrNull<HelloPluginConfig>() ?.print ?: "Hello :)" val toPrint = koin.configOrNull<HelloPluginConfig>() ?.print ?: "Hello :)"
logger.d { toPrint } logger.d { toPrint }
logger.dS { getMe().toString() } logger.dS { getMe().toString() }
onCommand("hello_world") { onCommand("hello_world") {

View File

@ -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()

View File

@ -0,0 +1,55 @@
package dev.inmo.plagubot
import kotlinx.serialization.InternalSerializationApi
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
import kotlin.reflect.KClass
/**
* Using [single] to register `T` with serializer [configSerializer]
*
* @param default Will be used if [field] is absent as an alternative way of config allocation. If null passed, error
* will be thrown
*/
inline fun <reified T> Module.registerConfig(configSerializer: KSerializer<T>, field: String?, noinline default: (Scope.(JsonObject) -> T?)? = null) {
single {
val fieldValue = get<JsonObject>().let {
if (field == null) {
it
} else {
it[field] ?: default ?.let { _ ->
return@single default(it)
} ?: error("Unable to take field $field from config")
}
}
get<Json>().decodeFromJsonElement(configSerializer, fieldValue)
}
}
/**
* Using [single] to register config with getting of [serializer] from [kClass]
*
* @param default Will be used if [field] is absent as an alternative way of config allocation. If null passed, error
* will be thrown
*/
@OptIn(InternalSerializationApi::class)
inline fun <reified T : Any> Module.registerConfig(kClass: KClass<T>, field: String?, noinline default: (Scope.(JsonObject) -> T?)? = null) = registerConfig(kClass.serializer(), field, default)
/**
* Using [single] to register config with getting of [serializer] from [kClass]
*
* @param default Will be used if [field] is absent as an alternative way of config allocation. If null passed, error
* will be thrown
*/
inline fun <reified T : Any> Module.registerConfig(field: String?, noinline default: (Scope.(JsonObject) -> T?)? = null) = registerConfig(T::class, field, default)
inline fun <reified T : Any> Scope.config() = get<T>()
inline fun <reified T : Any> Koin.config() = get<T>()
inline fun <reified T : Any> Scope.configOrNull() = getOrNull<T>()
inline fun <reified T : Any> Koin.configOrNull() = getOrNull<T>()

View File

@ -7,9 +7,7 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
/** /**
@ -32,16 +30,6 @@ interface Plugin : StartPlugin {
*/ */
fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) = setupBotClient() 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 * 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] * should override the method with receiver [BehaviourContextWithFSM]