Compare commits

...

14 Commits

26 changed files with 286 additions and 463 deletions

View File

@ -8,9 +8,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 1.8 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 1.8 java-version: 11
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew build

View File

@ -1,5 +1,31 @@
# Changelog # Changelog
## 1.1.0
* `Versions`
* `tgbotapi`: `2.0.0`
* `microutils`: `0.10.5`
* `Plugin`:
* All plugins will be loaded in parallel
## 1.0.0
* `Versions`
* `kotlin`: `1.6.21`
* `coroutines`: `1.6.1`
* `serialization`: `1.3.3`
* `exposed`: `0.38.2`
* `tgbotapi`: `1.1.0`
* `microutils`: `0.10.4`
* `Common`:
* ___ALL THE SDI/KLASSINDEX FUNCTIONALITY HAS BEEN REMOVED___
* `Plugin`:
* Now plugins must have empty constructor
* Now plugins may provide realization of two methods: `setupDI` and `setupBotPlugin`
* `PlaguBot`:
* `Config` now is simple serializable `data class`
* `PlaguBot` now is more simple as a plugin
## 0.5.1 ## 0.5.1
* `Versions` * `Versions`

View File

@ -1,7 +1,6 @@
plugins { plugins {
id 'org.jetbrains.kotlin.jvm' id 'org.jetbrains.kotlin.jvm'
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "org.jetbrains.kotlin.kapt"
id 'application' id 'application'
} }
@ -11,21 +10,17 @@ project.version="$version"
apply from: "publish.gradle" apply from: "publish.gradle"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation libs.kt.stdlib
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" api libs.kt.coroutines
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_runtime_version" api libs.kt.serialization
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" api libs.jb.exposed.jdbc
api "org.jetbrains.exposed:exposed-jdbc:$kotlin_exposed_version"
api "dev.inmo:tgbotapi:$tgbotapi_version" api libs.tgbotapi
api "dev.inmo:sdi:$sdi_version" api libs.microutils.repos.exposed
api "dev.inmo:micro_utils.repos.exposed:$microutils_version"
api "com.github.matfax.klassindex:library:$klassindex_version" api libs.sqlite
kapt "com.github.matfax.klassindex:processor:$klassindex_version"
api "org.xerial:sqlite-jdbc:$sqlite_version"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testImplementation libs.kt.test.junit
api project(":plagubot.plugin") api project(":plagubot.plugin")
} }
@ -34,13 +29,6 @@ application {
mainClassName = 'dev.inmo.plagubot.AppKt' mainClassName = 'dev.inmo.plagubot.AppKt'
} }
kapt {
arguments {
arg("com.github.matfax.klassindex.IndexSubclasses", "dev.inmo.plagubot.Plugin")
arg("com.github.matfax.klassindex.IndexAnnotated", "dev.inmo.sdi.SDIIncluded")
}
}
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8

View File

@ -1 +0,0 @@
{"bintrayConfig":{"repo":"PlaguBot","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/PlaguBot","autoPublish":true,"overridePublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/PlaguBot/LICENSE"}],"mavenConfig":{"name":"PlaguBot Bot","description":"Base PlaguBot project","url":"https://github.com/InsanusMokrassar/PlaguBot","vcsUrl":"ssh://git@github.com/InsanusMokrassar/PlaguBot.git","includeGpgSigning":true,"publishToMavenCentral":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"JVM"}

View File

@ -1,6 +1,4 @@
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocJar(type: Jar) { task javadocJar(type: Jar) {
from javadoc from javadoc
@ -50,17 +48,8 @@ publishing {
} }
} }
repositories {
repositories { if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "bintray"
url = uri("https://api.bintray.com/maven/${project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')}/PlaguBot/${project.name}/;publish=1;override=1")
credentials {
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
}
}
maven { maven {
name = "sonatype" name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
@ -69,14 +58,24 @@ publishing {
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
} }
} }
} }
}
} }
} }
} }
signing { if (project.hasProperty("signing.gnupg.keyName")) {
useGpgCmd() apply plugin: 'signing'
sign publishing.publications
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
} }

1
bot/publish.kpsb Normal file
View File

@ -0,0 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/PlaguBot/LICENSE"}],"mavenConfig":{"name":"PlaguBot Bot","description":"Base PlaguBot project","url":"https://github.com/InsanusMokrassar/PlaguBot","vcsUrl":"ssh://git@github.com/InsanusMokrassar/PlaguBot.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}

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,39 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import kotlinx.serialization.SerialName import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
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.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.core.module.Module
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 fun Module.setupDI(database: Database, params: JsonObject) {
single {
get<Json>().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null)
}
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
println(koin.get<HelloPluginConfig>().print)
println(getMe())
onCommand("hello_world") {
reply(it, "Hello :)")
}
} }
} }

View File

@ -1,55 +1,104 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.micro_utils.coroutines.runCatchingSafely
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.api.webhook.deleteWebhook
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.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
import java.util.logging.Level
import java.util.logging.Logger
const val DefaultPlaguBotParamsKey = "plagubot" val Scope.plagubot: PlaguBot
val Map<String, Any>.plagubot get() = get()
get() = get(DefaultPlaguBotParamsKey) as? PlaguBot
@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
private val logger = Logger.getLogger("PlaguBot")
@Transient @Transient
private val bot = telegramBot(config.botToken) private val bot = telegramBot(config.botToken)
@Transient
private val paramsMap = config.params ?.toMap() ?: emptyMap()
@Transient
private val database = config.params ?.database ?: config.database.database
override suspend fun getCommands(): List<BotCommand> = config.plugins.flatMap { override fun Module.setupDI(database: Database, params: JsonObject) {
it.getCommands() single { config }
single { config.plugins }
single { config.databaseConfig }
single { config.databaseConfig.database }
single { defaultJsonFormat }
single { this@PlaguBot }
includes(
config.plugins.mapNotNull {
runCatching {
module {
with(it) {
setupDI(database, params)
}
}
}.onFailure { e ->
logger.log(Level.WARNING, "Unable to load DI part of $it", e)
}.getOrNull()
}
)
} }
override suspend fun BehaviourContext.invoke(database: Database, params: Map<String, Any>) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
config.plugins.forEach { config.plugins.map {
it.apply { invoke(database, params) } launch {
} runCatchingSafely {
val commands = getCommands() logger.info("Start loading of $it")
val futureUnavailable = commands.drop(botCommandsLimit.last) with(it) {
if (futureUnavailable.isNotEmpty()) { setupBotPlugin(koin)
println("Next commands are out of range in setting command request and will be unavailable from autocompleting: $futureUnavailable") }
} }.onFailure { e ->
safelyWithoutExceptions { setMyCommands(commands.take(botCommandsLimit.last)) } logger.log(Level.WARNING, "Unable to load bot part of $it", e)
}.onSuccess {
logger.info("Complete loading of $it")
}
}
}.joinAll()
} }
/** /**
* This method will create an [Job] which will be the main [Job] of ran instance * This method will create an [Job] which will be the main [Job] of ran instance
*/ */
suspend fun start( suspend fun start(
scope: CoroutineScope = CoroutineScope(Dispatchers.Default) scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
): Job = bot.buildBehaviourWithLongPolling(scope) { ): Job {
invoke(database, paramsMap) logger.info("Start initialization")
val koinApp = KoinApplication.init()
koinApp.modules(
module {
setupDI(config.databaseConfig.database, json)
}
)
logger.info("Modules loaded")
GlobalContext.startKoin(koinApp)
logger.info("Koin started")
lateinit var behaviourContext: BehaviourContext
bot.buildBehaviour(scope = scope) {
logger.info("Start setup of bot part")
behaviourContext = this
setupBotPlugin(koinApp.koin)
deleteWebhook()
}
logger.info("Behaviour builder has been setup")
return bot.startGettingOfUpdatesByLongPolling(scope = behaviourContext, updatesFilter = behaviourContext).also {
logger.info("Long polling has been started")
}
} }
} }

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,13 @@
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.SerialName
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Config( data class Config(
override val plugins: List<@Contextual Plugin>,
val database: DatabaseConfig = DatabaseConfig(),
val botToken: String, val botToken: String,
@Contextual val plugins: List<Plugin>,
override val params: Module? = null @SerialName("database")
) : PluginsConfiguration val databaseConfig: DatabaseConfig = DatabaseConfig(),
)

View File

@ -1,19 +1,17 @@
package dev.inmo.plagubot.config package dev.inmo.plagubot.config
import dev.inmo.sdi.SDIIncluded
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.scope.Scope
import org.sqlite.JDBC import org.sqlite.JDBC
import java.sql.Connection import java.sql.Connection
const val defaultDatabaseParamsName = "defaultDatabase" inline val Scope.database: Database?
inline val Map<String, Any>.database: Database? get() = getOrNull<Database>()
get() = (get(defaultDatabaseParamsName) as? DatabaseConfig) ?.database
@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,190 +0,0 @@
package dev.inmo.plagubot.config
import com.github.matfax.klassindex.KlassIndex
import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.PluginSerializer
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

@ -1,40 +0,0 @@
package dev.inmo.plagubot.config
import dev.inmo.plagubot.HelloPlugin
import kotlinx.serialization.InternalSerializationApi
import org.junit.Test
import kotlin.test.assertEquals
class ConfigTest {
@InternalSerializationApi
@Test
fun testThatPluginPassedToParamsWillBeCorrectlyUsedInPlugins() {
val rawConfig = """
{
"database": {
},
"botToken": "",
"plugins": [
"helloPlugin"
],
"params": {
"helloPlugin": {"type": "dev.inmo.plagubot.HelloPlugin", "parameter": "Example"}
}
}
""".trimIndent()
val config = configAndPluginsConfigJsonFormat.decodeFromString(PluginsConfigurationSerializer, rawConfig) as Config
assert(config.plugins.size == 1)
assert(config.plugins.first() is HelloPlugin)
assert((config.plugins.first() as HelloPlugin).parameter == "Example")
val redecoded = configAndPluginsConfigJsonFormat.decodeFromString(
PluginsConfigurationSerializer,
configAndPluginsConfigJsonFormat.encodeToString(PluginsConfigurationSerializer, config)
) as Config
assertEquals(config.database, redecoded.database)
assertEquals(config.plugins, redecoded.plugins)
assertEquals(config.botToken, redecoded.botToken)
assertEquals(config.params ?.toMap(), redecoded.params ?.toMap())
}
}

View File

@ -1,20 +1,19 @@
buildscript { buildscript {
repositories { repositories {
jcenter() mavenCentral()
maven { url "https://plugins.gradle.org/m2/" } maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath libs.buildscript.kt.gradle
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath libs.buildscript.kt.serialization
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" classpath libs.buildscript.gh.release
} }
} }
allprojects { allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
jcenter()
mavenLocal() mavenLocal()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
} }

View File

@ -4,19 +4,5 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true kotlin.js.generate.externals=true
kotlin.incremental=true kotlin.incremental=true
kotlin_version=1.6.10
kotlin_coroutines_version=1.6.0
kotlin_serialisation_runtime_version=1.3.2
kotlin_exposed_version=0.37.2
sdi_version=0.6.0
tgbotapi_version=0.38.4
microutils_version=0.9.5
klassindex_version=4.1.0-rc.1
sqlite_version=3.36.0.3
github_release_plugin_version=2.2.12
group=dev.inmo group=dev.inmo
version=0.5.1 version=1.1.0

49
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,49 @@
[versions]
kt = "1.6.21"
kt-serialization = "1.3.3"
kt-coroutines = "1.6.1"
microutils = "0.10.5"
tgbotapi = "2.0.0"
jb-exposed = "0.38.2"
jb-dokka = "1.6.21"
sqlite = "3.36.0.3"
klock = "2.7.0"
uuid = "0.4.0"
ktor = "2.0.1"
gh-release = "2.3.7"
android-gradle = "7.0.4"
dexcount = "3.1.0"
koin = "3.2.0"
[libraries]
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" }
microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" }
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
jb-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "jb-exposed" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -9,11 +9,13 @@ project.version="$version"
apply from: "publish.gradle" apply from: "publish.gradle"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation libs.kt.stdlib
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_runtime_version" api libs.kt.serialization
api "dev.inmo:tgbotapi:$tgbotapi_version" api libs.tgbotapi
api "dev.inmo:micro_utils.repos.exposed:$microutils_version" api libs.microutils.repos.exposed
api libs.koin
} }
java { java {

View File

@ -1 +0,0 @@
{"bintrayConfig":{"repo":"PlaguBot","packageName":"${project.name}","packageVcs":"https://github.com/InsanusMokrassar/PlaguBot","autoPublish":true,"overridePublish":true},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/PlaguBot/LICENSE"}],"mavenConfig":{"name":"PlaguBot Plugin","description":"Base dependency for whole PlaguBot project","url":"https://github.com/InsanusMokrassar/PlaguBot","vcsUrl":"ssh://git@github.com/InsanusMokrassar/PlaguBot.git","includeGpgSigning":true,"publishToMavenCentral":true,"developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"JVM"}

View File

@ -1,6 +1,4 @@
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
apply plugin: 'signing'
task javadocJar(type: Jar) { task javadocJar(type: Jar) {
from javadoc from javadoc
@ -50,17 +48,8 @@ publishing {
} }
} }
repositories {
repositories { if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "bintray"
url = uri("https://api.bintray.com/maven/${project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')}/PlaguBot/${project.name}/;publish=1;override=1")
credentials {
username = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
password = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
}
}
maven { maven {
name = "sonatype" name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
@ -69,14 +58,24 @@ publishing {
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
} }
} }
} }
}
} }
} }
} }
signing { if (project.hasProperty("signing.gnupg.keyName")) {
useGpgCmd() apply plugin: 'signing'
sign publishing.publications
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
} }

1
plugin/publish.kpsb Normal file
View File

@ -0,0 +1 @@
{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/PlaguBot/LICENSE"}],"mavenConfig":{"name":"PlaguBot Plugin","description":"Base dependency for whole PlaguBot project","url":"https://github.com/InsanusMokrassar/PlaguBot","vcsUrl":"ssh://git@github.com/InsanusMokrassar/PlaguBot.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"}

View File

@ -1,14 +1,15 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
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.module.Module
/** /**
* **ANY REALIZATION OF [Plugin] MUST HAVE CONSTRUCTOR WITH ABSENCE OF INCOMING PARAMETERS**
*
* Use this interface for your bot. It is possible to use [kotlinx.serialization.SerialName] annotations on your plugins * Use this interface for your bot. It is possible to use [kotlinx.serialization.SerialName] annotations on your plugins
* to set up short name for your plugin. Besides, simple name of your class will be used as key for deserialization * to set up short name for your plugin. Besides, simple name of your class will be used as key for deserialization
* too. * too.
@ -16,32 +17,13 @@ import org.jetbrains.exposed.sql.Database
@Serializable(PluginSerializer::class) @Serializable(PluginSerializer::class)
interface Plugin { interface Plugin {
/** /**
* In case you want to publish some processed by your plugin commands, you can provide it via this method * This method will be called when this plugin should configure di module based on the incoming params
*
* @see BotCommand
*/ */
suspend fun getCommands(): List<BotCommand> = emptyList() fun Module.setupDI(
@Deprecated("Override other method with receiver BehaviourContext")
suspend operator fun invoke(
bot: TelegramBot,
database: Database, database: Database,
updatesFilter: FlowsUpdatesFilter, params: JsonObject
scope: CoroutineScope ) {}
suspend fun BehaviourContext.setupBotPlugin(
koin: Koin
) {} ) {}
/**
* This method (usually) will be invoked just one time in the whole application.
*/
suspend operator fun BehaviourContext.invoke(
database: Database
) = invoke(bot, database, flowsUpdatesFilter, scope)
/**
* This method (usually) will be invoked just one time in the whole application.
*/
suspend operator fun BehaviourContext.invoke(
database: Database,
params: Map<String, Any>
) = invoke(database)
} }

View File

@ -1,53 +1,24 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import kotlinx.serialization.* import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
private val defaultJson = Json {
ignoreUnknownKeys = true
}
@Serializer(Plugin::class) @Serializer(Plugin::class)
object PluginSerializer : KSerializer<Plugin> { class PluginSerializer : KSerializer<Plugin> {
private val polymorphic = PolymorphicSerializer(Plugin::class) override val descriptor: SerialDescriptor
override val descriptor: SerialDescriptor = JsonObject.serializer().descriptor get() = String.serializer().descriptor
@OptIn(InternalSerializationApi::class)
override fun deserialize(decoder: Decoder): Plugin { override fun deserialize(decoder: Decoder): Plugin {
val format = (decoder as? JsonDecoder) ?.json ?: defaultJson return Class.forName(decoder.decodeString()).getDeclaredConstructor().newInstance() as Plugin
val asJson = JsonElement.serializer().deserialize(decoder)
val jsonObject = (asJson as? JsonObject)
val type = (jsonObject ?.get("type") as? JsonPrimitive) ?.contentOrNull
val external = if (type != null) {
try {
Class.forName(type) ?.kotlin ?.serializerOrNull()
} catch (e: Exception) {
null
}
} else {
null
}
return if (jsonObject != null && external != null) {
format.decodeFromJsonElement(
external as KSerializer<Plugin>,
JsonObject(jsonObject.toMutableMap().also { it.remove("type") })
)
} else {
format.decodeFromJsonElement(
polymorphic,
asJson
)
}
} }
@OptIn(InternalSerializationApi::class)
override fun serialize(encoder: Encoder, value: Plugin) { override fun serialize(encoder: Encoder, value: Plugin) {
val serializer = (value::class.serializerOrNull() ?: polymorphic) as KSerializer<Plugin> encoder.encodeString(
serializer.serialize(encoder, value) value::class.java.canonicalName
)
} }
} }

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"
] }
} }