temporal progress

This commit is contained in:
InsanusMokrassar 2022-05-16 20:07:57 +06:00
parent a0f9e31b04
commit 41885b8f7b
11 changed files with 98 additions and 282 deletions

View File

@ -3,6 +3,7 @@ package dev.inmo.plagubot
import dev.inmo.plagubot.config.* import dev.inmo.plagubot.config.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.json.jsonObject
import java.io.File import java.io.File
/** /**
@ -12,7 +13,8 @@ import java.io.File
suspend fun main(args: Array<String>) { suspend fun main(args: Array<String>) {
val (configPath) = args val (configPath) = args
val file = File(configPath) 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()
} }

View File

@ -1,16 +1,33 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import kotlinx.serialization.SerialName import kotlinx.serialization.*
import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database 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 @Serializable
@SerialName("Hello") @SerialName("Hello")
data class HelloPlugin( class HelloPlugin : Plugin {
val parameter: String @Serializable
) : Plugin { data class HelloPluginConfig(
override suspend fun BehaviourContext.invoke(database: Database) { val print: String
println(parameter) )
override suspend fun BehaviourContext.invoke(
database: Database,
params: JsonObject
) {
loadModule {
single {
get<Json>().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null)
}
}
println(get<HelloPluginConfig>().print)
} }
} }

View File

@ -1,45 +1,39 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions
import dev.inmo.plagubot.config.* import dev.inmo.plagubot.config.*
import dev.inmo.tgbotapi.bot.ktor.telegramBot 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.extensions.behaviour_builder.*
import dev.inmo.tgbotapi.types.BotCommand import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling
import dev.inmo.tgbotapi.types.botCommandsLimit
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database 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" const val DefaultPlaguBotParamsKey = "plagubot"
val Map<String, Any>.plagubot val Plugin.plagubot: PlaguBot
get() = get(DefaultPlaguBotParamsKey) as? PlaguBot get() = get()
@Serializable @Serializable
data class PlaguBot( data class PlaguBot(
@Serializable(PluginsConfigurationSerializer::class) private val json: JsonObject,
private val config: Config private val config: Config
) : Plugin { ) : Plugin {
@Transient @Transient
private val bot = telegramBot(config.botToken) private val bot = telegramBot(config.botToken)
@Transient
private val database = config.params ?.database ?: config.database.database
override suspend fun getCommands(): List<BotCommand> = config.plugins.flatMap { override suspend fun BehaviourContext.invoke(
it.getCommands() database: Database,
} params: JsonObject
) {
override suspend fun BehaviourContext.invoke(database: Database, params: Map<String, Any>) {
config.plugins.forEach { config.plugins.forEach {
it.apply { invoke(database, params) } 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( suspend fun start(
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
): Job = bot.buildBehaviourWithLongPolling(scope) { ): Job {
invoke(database, paramsMap) 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)
} }
} }

View File

@ -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<BotCommand> = pluginsConfiguration.plugins.flatMap {
it.getCommands()
}
override suspend fun BehaviourContext.invoke(database: Database, params: Map<String, Any>) {
val finalParams = pluginsConfiguration.params ?.plus(params) ?: params
pluginsConfiguration.plugins.forEach {
it.apply { invoke(database, finalParams) }
}
}
}

View File

@ -1,15 +1,11 @@
package dev.inmo.plagubot.config package dev.inmo.plagubot.config
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.sdi.Module
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable @Serializable
data class Config( data class Config(
override val plugins: List<Plugin>,
val database: DatabaseConfig = DatabaseConfig(),
val botToken: String, val botToken: String,
val rawConfig: JsonObject val plugins: List<Plugin>,
) : PluginsConfiguration val database: DatabaseConfig = DatabaseConfig(),
)

View File

@ -1,19 +1,23 @@
package dev.inmo.plagubot.config package dev.inmo.plagubot.config
import dev.inmo.sdi.SDIIncluded import dev.inmo.plagubot.Plugin
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager 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 org.sqlite.JDBC
import java.sql.Connection import java.sql.Connection
const val defaultDatabaseParamsName = "defaultDatabase" const val defaultDatabaseParamsName = "defaultDatabase"
inline val Map<String, Any>.database: Database? inline val Plugin.database: Database?
get() = (get(defaultDatabaseParamsName) as? DatabaseConfig) ?.database get() = getKoin().getOrNull<Database>(named(defaultDatabaseParamsName))
@Serializable @Serializable
@SDIIncluded
data class DatabaseConfig( data class DatabaseConfig(
val url: String = "jdbc:sqlite:file:test?mode=memory&cache=shared", val url: String = "jdbc:sqlite:file:test?mode=memory&cache=shared",
val driver: String = JDBC::class.qualifiedName!!, val driver: String = JDBC::class.qualifiedName!!,

View File

@ -0,0 +1,7 @@
package dev.inmo.plagubot.config
import kotlinx.serialization.json.Json
val defaultJsonFormat = Json {
ignoreUnknownKeys = true
}

View File

@ -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 <T : Plugin> KClass<T>.includeIn(builder: PolymorphicModuleBuilder<Plugin>) = 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<Plugin>
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<Plugin> {
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<Module> {
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<PluginsConfiguration> {
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
}
}
}
}

View File

@ -5,7 +5,7 @@ kt-serialization = "1.3.3"
kt-coroutines = "1.6.1" kt-coroutines = "1.6.1"
microutils = "0.10.4" microutils = "0.10.4"
tgbotapi = "1.0.1" tgbotapi = "1.1.0"
jb-exposed = "0.38.2" jb-exposed = "0.38.2"
jb-dokka = "1.6.21" jb-dokka = "1.6.21"
@ -20,7 +20,7 @@ ktor = "2.0.1"
gh-release = "2.3.7" gh-release = "2.3.7"
android-gradle = "7.0.4" android-gradle = "7.0.4"
dexcount = "3.0.1" dexcount = "3.1.0"
koin = "3.2.0" koin = "3.2.0"
[libraries] [libraries]

View File

@ -8,7 +8,11 @@ import kotlinx.coroutines.CoroutineScope
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.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.KoinApplication 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** * **ANY REALIZATION OF [Plugin] MUST HAVE CONSTRUCTOR WITH ABSENCE OF INCOMING PARAMETERS**
@ -18,28 +22,17 @@ import org.koin.core.KoinApplication
* too. * too.
*/ */
@Serializable(PluginSerializer::class) @Serializable(PluginSerializer::class)
interface Plugin { interface Plugin : KoinComponent {
/** fun loadModule(createdAtStart: Boolean = false, moduleDeclaration: ModuleDeclaration) = getKoin().loadModules(
* In case you want to publish some processed by your plugin commands, you can provide it via this method listOf(
* module(createdAtStart, moduleDeclaration)
* @see BotCommand )
*/ )
suspend fun getCommands(): List<BotCommand> = emptyList()
/** /**
* This method (usually) will be invoked just one time in the whole application. * This method (usually) will be invoked just one time in the whole application.
*/ */
suspend operator fun BehaviourContext.invoke( suspend operator fun BehaviourContext.invoke(
database: Database, 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 params: JsonObject
) = invoke(database, koinApplication) ) {}
} }

View File

@ -8,9 +8,9 @@
}, },
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678", "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [ "plugins": [
{ "dev.inmo.plagubot.HelloPlugin"
"type": "Hello", ],
"parameter": "Example" "helloPlugin": {
} "print": "Hello World"
] }
} }