diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt index 133d990..bfca750 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/App.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/App.kt @@ -3,6 +3,7 @@ package dev.inmo.plagubot import dev.inmo.plagubot.config.* import kotlinx.coroutines.* import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.json.jsonObject import java.io.File /** @@ -12,7 +13,8 @@ import java.io.File suspend fun main(args: Array) { val (configPath) = args val file = File(configPath) - val config = configAndPluginsConfigJsonFormat.decodeFromString(PluginsConfigurationSerializer, file.readText()) as Config + val json = defaultJsonFormat.parseToJsonElement(file.readText()).jsonObject + val config = defaultJsonFormat.decodeFromJsonElement(Config.serializer(), json) - PlaguBot(config).start().join() + PlaguBot(json, config).start().join() } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt b/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt index 1b66d96..4aaa829 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/HelloPlugin.kt @@ -1,16 +1,33 @@ package dev.inmo.plagubot import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.serialization.* +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.KoinApplication +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.dsl.module @Serializable @SerialName("Hello") -data class HelloPlugin( - val parameter: String -) : Plugin { - override suspend fun BehaviourContext.invoke(database: Database) { - println(parameter) +class HelloPlugin : Plugin { + @Serializable + data class HelloPluginConfig( + val print: String + ) + override suspend fun BehaviourContext.invoke( + database: Database, + params: JsonObject + ) { + loadModule { + single { + get().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null) + } + } + + println(get().print) } } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt index a07b70b..875d927 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/PlaguBot.kt @@ -1,45 +1,39 @@ package dev.inmo.plagubot -import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.plagubot.config.* import dev.inmo.tgbotapi.bot.ktor.telegramBot -import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands import dev.inmo.tgbotapi.extensions.behaviour_builder.* -import dev.inmo.tgbotapi.types.BotCommand -import dev.inmo.tgbotapi.types.botCommandsLimit +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.KoinApplication +import org.koin.core.component.get +import org.koin.core.context.GlobalContext +import org.koin.core.qualifier.named +import org.koin.dsl.module const val DefaultPlaguBotParamsKey = "plagubot" -val Map.plagubot - get() = get(DefaultPlaguBotParamsKey) as? PlaguBot +val Plugin.plagubot: PlaguBot + get() = get() @Serializable data class PlaguBot( - @Serializable(PluginsConfigurationSerializer::class) + private val json: JsonObject, private val config: Config ) : Plugin { @Transient private val bot = telegramBot(config.botToken) - @Transient - private val database = config.params ?.database ?: config.database.database - override suspend fun getCommands(): List = config.plugins.flatMap { - it.getCommands() - } - - override suspend fun BehaviourContext.invoke(database: Database, params: Map) { + override suspend fun BehaviourContext.invoke( + database: Database, + params: JsonObject + ) { config.plugins.forEach { it.apply { invoke(database, params) } } - val commands = getCommands() - val futureUnavailable = commands.drop(botCommandsLimit.last) - if (futureUnavailable.isNotEmpty()) { - println("Next commands are out of range in setting command request and will be unavailable from autocompleting: $futureUnavailable") - } - safelyWithoutExceptions { setMyCommands(commands.take(botCommandsLimit.last)) } } /** @@ -47,7 +41,24 @@ data class PlaguBot( */ suspend fun start( scope: CoroutineScope = CoroutineScope(Dispatchers.Default) - ): Job = bot.buildBehaviourWithLongPolling(scope) { - invoke(database, paramsMap) + ): Job { + val koinApp = KoinApplication.init() + koinApp.modules( + module { + single { config } + single { config.plugins } + single { config.database } + single(named(defaultDatabaseParamsName)) { config.database.database } + single { defaultJsonFormat } + single(named(DefaultPlaguBotParamsKey)) { this@PlaguBot } + } + ) + lateinit var behaviourContext: BehaviourContext + bot.buildBehaviour(scope = scope) { + invoke(config.database.database, json) + behaviourContext = this + } + GlobalContext.startKoin(koinApp) + return bot.startGettingOfUpdatesByLongPolling(scope = behaviourContext, updatesFilter = behaviourContext) } } diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/PluginsHolder.kt b/bot/src/main/kotlin/dev/inmo/plagubot/PluginsHolder.kt deleted file mode 100644 index a30a362..0000000 --- a/bot/src/main/kotlin/dev/inmo/plagubot/PluginsHolder.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.inmo.plagubot - -import dev.inmo.plagubot.config.PluginsConfigurationSerializer -import dev.inmo.plagubot.config.SimplePluginsConfiguration -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.types.BotCommand -import kotlinx.serialization.Serializable -import org.jetbrains.exposed.sql.Database - -@Serializable -data class PluginsHolder( - @Serializable(PluginsConfigurationSerializer::class) - private val pluginsConfiguration: SimplePluginsConfiguration -) : Plugin { - override suspend fun getCommands(): List = pluginsConfiguration.plugins.flatMap { - it.getCommands() - } - - override suspend fun BehaviourContext.invoke(database: Database, params: Map) { - val finalParams = pluginsConfiguration.params ?.plus(params) ?: params - pluginsConfiguration.plugins.forEach { - it.apply { invoke(database, finalParams) } - } - } -} \ No newline at end of file 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 67a624f..4f13a4b 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/Config.kt @@ -1,15 +1,11 @@ package dev.inmo.plagubot.config import dev.inmo.plagubot.Plugin -import dev.inmo.sdi.Module -import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject @Serializable data class Config( - override val plugins: List, - val database: DatabaseConfig = DatabaseConfig(), val botToken: String, - val rawConfig: JsonObject -) : PluginsConfiguration + val plugins: List, + val database: DatabaseConfig = DatabaseConfig(), +) 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 86750c1..19e513e 100644 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/DatabaseConfig.kt +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/DatabaseConfig.kt @@ -1,19 +1,23 @@ package dev.inmo.plagubot.config -import dev.inmo.sdi.SDIIncluded +import dev.inmo.plagubot.Plugin import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.transactions.transactionManager +import org.koin.core.KoinApplication +import org.koin.core.context.loadKoinModules +import org.koin.core.qualifier.StringQualifier +import org.koin.core.qualifier.named +import org.koin.core.scope.Scope import org.sqlite.JDBC import java.sql.Connection const val defaultDatabaseParamsName = "defaultDatabase" -inline val Map.database: Database? - get() = (get(defaultDatabaseParamsName) as? DatabaseConfig) ?.database +inline val Plugin.database: Database? + get() = getKoin().getOrNull(named(defaultDatabaseParamsName)) @Serializable -@SDIIncluded data class DatabaseConfig( val url: String = "jdbc:sqlite:file:test?mode=memory&cache=shared", val driver: String = JDBC::class.qualifiedName!!, diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt new file mode 100644 index 0000000..4202346 --- /dev/null +++ b/bot/src/main/kotlin/dev/inmo/plagubot/config/JsonFormat.kt @@ -0,0 +1,7 @@ +package dev.inmo.plagubot.config + +import kotlinx.serialization.json.Json + +val defaultJsonFormat = Json { + ignoreUnknownKeys = true +} diff --git a/bot/src/main/kotlin/dev/inmo/plagubot/config/PluginsConfiguration.kt b/bot/src/main/kotlin/dev/inmo/plagubot/config/PluginsConfiguration.kt deleted file mode 100644 index 092858f..0000000 --- a/bot/src/main/kotlin/dev/inmo/plagubot/config/PluginsConfiguration.kt +++ /dev/null @@ -1,189 +0,0 @@ -package dev.inmo.plagubot.config - -import com.github.matfax.klassindex.KlassIndex -import dev.inmo.plagubot.Plugin -import dev.inmo.sdi.Module -import dev.inmo.sdi.ModuleSerializer -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.* -import kotlinx.serialization.modules.* -import kotlin.reflect.KClass - -@InternalSerializationApi -internal inline fun KClass.includeIn(builder: PolymorphicModuleBuilder) = builder.subclass(this, serializer()) - -@InternalSerializationApi -internal val configAndPluginsConfigJsonFormat: Json - get() = Json { - ignoreUnknownKeys = true - serializersModule = SerializersModule { - polymorphic(Plugin::class) { - KlassIndex.getSubclasses(Plugin::class).flatMap { kclass -> - kclass.includeIn(this) - kclass.annotations.mapNotNull { it as? SerialName }.map { - it.value to kclass.serializer() - } + listOfNotNull( - kclass.simpleName ?.let { - it to kclass.serializer() - } - ) - }.toMap().let { - default { requiredType -> - it[requiredType] - } - } - } - } - } - -internal interface PluginsConfiguration { - val plugins: List - val params: Module? -} - -@Serializable -data class SimplePluginsConfiguration( - override val plugins: List<@Contextual Plugin>, - @Contextual - override val params: Module? = null -) : PluginsConfiguration - - -internal val DefaultModuleSerializer = ModuleSerializer(emptyList()) { - -} - -@Serializer(Plugin::class) -internal class InternalPluginSerializer( - private val params: Module -) : KSerializer { - override val descriptor: SerialDescriptor = PluginSerializer.descriptor - - @OptIn(InternalSerializationApi::class) - override fun deserialize(decoder: Decoder): Plugin { - val asJson = JsonElement.serializer().deserialize(decoder) - - return if (asJson is JsonPrimitive) { - params[asJson.jsonPrimitive.content] as Plugin - } else { - val jsonFormat = ((decoder as? JsonDecoder)?.json ?: configAndPluginsConfigJsonFormat) - jsonFormat.decodeFromJsonElement(PluginSerializer, asJson) - } - } - - @OptIn(InternalSerializationApi::class) - override fun serialize(encoder: Encoder, value: Plugin) { - params.keys.firstOrNull { params[it] === value } ?.also { - encoder.encodeString(it) - } ?: PluginSerializer.serialize(encoder, value) - } -} - -@Serializer(Module::class) -internal class InternalModuleSerializer( - private val original: JsonElement?, - private val params: Module -) : KSerializer { - override val descriptor: SerialDescriptor = PluginSerializer.descriptor - - @OptIn(InternalSerializationApi::class) - override fun deserialize(decoder: Decoder): Module { - val asJson = JsonElement.serializer().deserialize(decoder) - - return if (asJson == original) { - params - } else { - configAndPluginsConfigJsonFormat.decodeFromJsonElement(DefaultModuleSerializer, asJson) - } - } - - @OptIn(InternalSerializationApi::class) - override fun serialize(encoder: Encoder, value: Module) = DefaultModuleSerializer.serialize(encoder, value) -} - -internal fun internalPluginSerializerSerializersModule( - internalPluginSerializer: InternalPluginSerializer, - internalModuleSerializer: InternalModuleSerializer? -) = SerializersModule { - contextual(internalPluginSerializer) - contextual(internalModuleSerializer ?: return@SerializersModule) -} - -@Serializer(PluginsConfiguration::class) -internal object PluginsConfigurationSerializer : KSerializer { - private val jsonSerializer = JsonObject.serializer() - private val moduleSerializer = ModuleSerializer() - override val descriptor: SerialDescriptor = jsonSerializer.descriptor - - @OptIn(InternalSerializationApi::class) - override fun deserialize(decoder: Decoder): PluginsConfiguration { - val json = jsonSerializer.deserialize(decoder) - val jsonFormat = (decoder as? JsonDecoder) ?.json ?: configAndPluginsConfigJsonFormat - val paramsRow = json["params"] - - val resultJsonFormat = if (paramsRow != null && paramsRow != JsonNull) { - val params = jsonFormat.decodeFromJsonElement( - moduleSerializer, - paramsRow - ) - - val pluginsSerializer = InternalPluginSerializer(params) - val moduleSerializer = InternalModuleSerializer(paramsRow, params) - Json(jsonFormat) { - serializersModule = decoder.serializersModule.overwriteWith( - internalPluginSerializerSerializersModule(pluginsSerializer, moduleSerializer) - ) - } - } else { - jsonFormat - } - return try { - resultJsonFormat.decodeFromJsonElement( - Config.serializer(), - json - ) - } catch (e: SerializationException) { - resultJsonFormat.decodeFromJsonElement( - SimplePluginsConfiguration.serializer(), - json - ) - } - } - - @OptIn(InternalSerializationApi::class) - override fun serialize(encoder: Encoder, value: PluginsConfiguration) { - val params = value.params - val serializer = when (value) { - is Config -> Config.serializer() - is SimplePluginsConfiguration -> SimplePluginsConfiguration.serializer() - else -> return - } - if (params != null) { - val pluginsSerializer = InternalPluginSerializer(params) - - val jsonFormat = Json(configAndPluginsConfigJsonFormat) { - serializersModule = encoder.serializersModule.overwriteWith( - internalPluginSerializerSerializersModule(pluginsSerializer, null) - ) - } - - jsonSerializer.serialize( - encoder, - when (value) { - is Config -> jsonFormat.encodeToJsonElement(Config.serializer(), value) - is SimplePluginsConfiguration -> jsonFormat.encodeToJsonElement(SimplePluginsConfiguration.serializer(), value) - else -> return - } as JsonObject - ) - } else { - when (value) { - is Config -> Config.serializer().serialize(encoder, value) - is SimplePluginsConfiguration -> SimplePluginsConfiguration.serializer().serialize(encoder, value) - else -> return - } - } - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8be3969..48b2b7f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ kt-serialization = "1.3.3" kt-coroutines = "1.6.1" microutils = "0.10.4" -tgbotapi = "1.0.1" +tgbotapi = "1.1.0" jb-exposed = "0.38.2" jb-dokka = "1.6.21" @@ -20,7 +20,7 @@ ktor = "2.0.1" gh-release = "2.3.7" android-gradle = "7.0.4" -dexcount = "3.0.1" +dexcount = "3.1.0" koin = "3.2.0" [libraries] diff --git a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt index ac86e7e..439b394 100644 --- a/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt +++ b/plugin/src/main/kotlin/dev/inmo/plagubot/Plugin.kt @@ -8,7 +8,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.serialization.Serializable 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.component.KoinComponent +import org.koin.dsl.ModuleDeclaration +import org.koin.dsl.module /** * **ANY REALIZATION OF [Plugin] MUST HAVE CONSTRUCTOR WITH ABSENCE OF INCOMING PARAMETERS** @@ -18,28 +22,17 @@ import org.koin.core.KoinApplication * too. */ @Serializable(PluginSerializer::class) -interface Plugin { - /** - * In case you want to publish some processed by your plugin commands, you can provide it via this method - * - * @see BotCommand - */ - suspend fun getCommands(): List = emptyList() - +interface Plugin : KoinComponent { + fun loadModule(createdAtStart: Boolean = false, moduleDeclaration: ModuleDeclaration) = getKoin().loadModules( + listOf( + module(createdAtStart, moduleDeclaration) + ) + ) /** * This method (usually) will be invoked just one time in the whole application. */ suspend operator fun BehaviourContext.invoke( database: Database, - koinApplication: KoinApplication, - ) {} - - /** - * This method (usually) will be invoked just one time in the whole application. - */ - suspend operator fun BehaviourContext.invoke( - database: Database, - koinApplication: KoinApplication, params: JsonObject - ) = invoke(database, koinApplication) + ) {} } diff --git a/template.config.json b/template.config.json index d360991..0d12e3e 100644 --- a/template.config.json +++ b/template.config.json @@ -8,9 +8,9 @@ }, "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678", "plugins": [ - { - "type": "Hello", - "parameter": "Example" - } - ] -} \ No newline at end of file + "dev.inmo.plagubot.HelloPlugin" + ], + "helloPlugin": { + "print": "Hello World" + } +}