Compare commits

..

No commits in common. "master" and "v5.1.0" have entirely different histories.

24 changed files with 169 additions and 607 deletions

View File

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

View File

@ -1,232 +1,5 @@
# 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
* `Bot`:
* Now bot is not built-in into `PlaguBot` and setted up as all other `Koin` dependencies
* Now it is possible to use `testServer` parameter for bots out of the box
* `Plugin`:
* New method `setupBotClient` with arguments to let plugin setup bot more freely
## 9.2.0
* `Versions`:
* `kotlin`: `2.0.20`
* `serialization`: `1.7.2`
* `microutils`: `0.22.2`
* `tgbotapi`: `18.1.0`
* `exposed`: `0.54.0`
* `sqlite`: `3.46.1.0`
## 9.1.0
* `Versions`:
* `tgbotapi`: `17.0.0`
## 9.0.0
* `Versions`:
* `Kotlin`: `2.0.10`
* `Serialization`: `1.7.1`
* `MicroUtils`: `0.22.0`
* `tgbotapi`: `16.0.0`
* `Exposed`: `0.53.0`
## 8.5.1
* `Versions`:
* `MicroUtils`: `0.21.4`
* `tgbotapi`: `15.2.0`
## 8.5.0
* `Versions`:
* `MicroUtils`: `0.21.2`
* `tgbotapi`: `15.1.0`
## 8.4.0
* `Versions`:
* `Coroutines`: `1.8.1`
* `MicroUtils`: `0.21.1`
* `tgbotapi`: `15.0.0`
* `Exposed`: `0.51.1`
## 8.3.0
* `Versions`:
* `Serialization`: `1.6.3`
* `MicroUtils`: `0.20.45`
* `tgbotapi`: `12.0.1`
* `Exposed`: `0.49.0`
* `SQLite`: `3.45.3.0`
* `Koin`: `3.5.6`
## 8.2.0
* `Versions`:
* `Coroutines`: `1.8.0`
* `tgbotapi`: `10.1.0`
* `MicroUtils`: `0.20.35`
## 8.1.1
* `Versions`:
* `tgbotapi`: `10.0.1`
* `MicroUtils`: `0.20.32`
* `Exposed`: `0.47.0`
## 8.1.0
* Integrate `dev.inmo:micro_utils.startup` into project
## 8.0.0
* `Versions`:
* `tgbotapi`: `10.0.0`
* `MicroUtils`: `0.20.26`
* `Exposed`: `0.46.0`
## 7.4.2
* `Versions`:
* `Kotlin`: `1.9.22`
* `tgbotapi`: `9.4.3`
* `MicroUtils`: `0.20.23`
* `Koin`: `3.5.7`
## 7.4.1
* `Versions`:
* `Serialization`: `1.6.2`
* `tgbotapi`: `9.4.2`
* `Exposed`: `0.45.0`
* `SQLite`: `3.44.1.0`
* `MicroUtils`: `0.20.19`
* `uuid`: `0.8.2`
* `ktor`: `2.3.7`
## 7.3.0
* `Versions`:
* `Kotlin`: `1.9.21`
* `Serialization`: `1.6.1`
* `tgbotapi`: `9.4.1`
* `ktor`: `2.3.6`
* `KSLog`: Removed explicit dependency, now it is declared in tgbotapi
* `MicroUtils`: `0.20.15`
## 7.2.3
* `Versions`:
* `tgbotapi`: `9.2.2`
* `exposed`: `0.44.0`
* `koin`: `3.5.0`
* `ktor`: `2.3.5`
## 7.2.2
* `Bot`:
* Now you may customize both `onStart` and `onUpdate` conflicts resolvers
## 7.2.1
* `Versions`:
* `tgbotapi`: `9.2.1`
* `ktor`: `2.3.4`
## 7.2.0
* `Version`:
* `tgbotapi`: `9.2.0`
* `kslog`: `1.1.2`
* `sqlite`: `3.43.0.0`
## 7.1.0
* `Version`:
* `microutils`: `0.19.9`
* `tgbotapi`: `9.1.0`
* `ktor`: `2.3.3`
* `coroutines`: `1.7.3`
* `koin`: `3.4.3`
## 7.0.0
* `Version`:
* `microutils`: `0.19.7`
* `tgbotapi`: `9.0.0`
* `ktor`: `2.3.2`
* `coroutines`: `1.7.2`
## 6.1.0
* `Version`:
* `kotlin`: `1.8.22`
* `microutils`: `0.19.4`
* `tgbotapi`: `8.1.0`
* `koin`: `3.4.2`
* `sqlite`: `3.42.0.0`
## 6.0.1
* `Version`:
* `microutils`: `0.19.2`
* `tgbotapi`: `8.0.1`
* `uuid`: `0.7.1`
* `ktor`: `2.3.1`
* `koin`: `3.4.1`
## 6.0.0
* `Versions`:
* `microutils`: `0.19.1`
* `tgbotapi`: `8.0.0`
* `klock`: `4.0.3`
## 5.1.3
* `Versions`:
* `serialization`: `1.5.1`
* `microutils`: `0.18.4`
* `tgbotapi`: `7.1.3`
## 5.1.2
* `Versions`:
* `microutils`: `0.18.1`
* `tgbotapi`: `7.1.2`
## 5.1.1
* `Versions`:
* `kotlin`: `1.8.21`
* `microutils`: `0.18.0`
* `tgbotapi`: `7.1.1`
## 5.1.0 ## 5.1.0
* `Versions`: * `Versions`:

View File

@ -2,7 +2,6 @@ plugins {
id 'org.jetbrains.kotlin.jvm' id 'org.jetbrains.kotlin.jvm'
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id 'application' id 'application'
id "com.google.devtools.ksp"
} }
project.group="$group" project.group="$group"
@ -19,8 +18,7 @@ dependencies {
api libs.tgbotapi api libs.tgbotapi
api libs.tgbotapi.behaviourBuilder.fsm api libs.tgbotapi.behaviourBuilder.fsm
api libs.microutils.repos.exposed api libs.microutils.repos.exposed
api libs.microutils.koin api libs.kslog
api libs.microutils.startup.launcher
api libs.sqlite api libs.sqlite
@ -34,10 +32,7 @@ application {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
}
ksp { // this generator do not require any arguments and we should left `ksp` empty
} }

View File

@ -2,11 +2,11 @@ apply plugin: 'maven-publish'
task javadocJar(type: Jar) { task javadocJar(type: Jar) {
from javadoc from javadoc
archiveClassifier = 'javadoc' classifier = 'javadoc'
} }
task sourcesJar(type: Jar) { task sourcesJar(type: Jar) {
from sourceSets.main.allSource from sourceSets.main.allSource
archiveClassifier = 'sources' classifier = 'sources'
} }
publishing { publishing {
@ -49,14 +49,18 @@ publishing {
} }
} }
repositories { repositories {
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) { if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
maven { maven {
name = "InmoNexus" name = "Gitea"
url = uri("https://nexus.inmo.dev/repository/maven-releases/") url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
credentials { credentials(HttpHeaderCredentials) {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER') name = "Authorization"
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD') value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
}
authentication {
header(HttpHeaderAuthentication)
} }
} }
@ -92,27 +96,4 @@ if (project.hasProperty("signing.gnupg.keyName")) {
dependsOn(it) dependsOn(it)
} }
} }
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
} }

View File

@ -1 +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":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"} {"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":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"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,11 +1,25 @@
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>) {
PlaguBot.start(args).join() KSLog.default = KSLog("PlaguBot")
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

@ -2,18 +2,22 @@ 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
@ -25,8 +29,12 @@ object HelloPlugin : Plugin {
val print: String val print: String
) )
override fun Module.setupDI(config: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
registerConfig<HelloPluginConfig>("helloPlugin") { null } single {
get<Json>().decodeFromJsonElement(HelloPluginConfig.serializer(), params["helloPlugin"] ?: return@single null)
}
} }
private sealed interface InternalFSMState : State { private sealed interface InternalFSMState : State {
@ -35,13 +43,8 @@ object HelloPlugin : Plugin {
data class SaidHelloOnce(override val context: IdChatIdentifier) : InternalFSMState data class SaidHelloOnce(override val context: IdChatIdentifier) : InternalFSMState
} }
override suspend fun startPlugin(koin: Koin) {
super.startPlugin(koin)
logger.i { "This logic called BEFORE the bot will be started and setup" }
}
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
val toPrint = koin.configOrNull<HelloPluginConfig>() ?.print ?: "Hello :)" val toPrint = koin.getOrNull<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

@ -1,28 +0,0 @@
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

@ -1,16 +0,0 @@
package dev.inmo.plagubot
import dev.inmo.micro_utils.fsm.common.State
fun interface OnStartContextsConflictResolver {
/**
* @param old Old state which is currently placed on the [State.context]
* @param new New state pretend to replace [old] one
* @return Should return:
*
* * Null in case when current realization unable to resolve conflict
* * False when current realization knows that [new] [State] must **not** replace [old] one
* * True when current realization knows that [new] [State] must replace [old] one
*/
suspend operator fun invoke(old: State, new: State): Boolean?
}

View File

@ -1,20 +0,0 @@
package dev.inmo.plagubot
import dev.inmo.micro_utils.fsm.common.State
fun interface OnUpdateContextsConflictResolver {
/**
* This method will be called when [sourceStateWithOldContext] [State.context] and [newStateWithNewContext] are not equal and currently there is
* launched [currentStateOnNewContext] state on the chain with [State.context] from [currentStateOnNewContext]
*
* @param sourceStateWithOldContext Old state where from [newStateWithNewContext] came
* @param newStateWithNewContext New state with changing [State.context] (it is different with [sourceStateWithOldContext] [State.context])
* @param currentStateOnNewContext State which is currently running on [newStateWithNewContext] [State.context]
* @return Should return:
*
* * Null in case when current realization unable to resolve conflict
* * False when [currentStateOnNewContext] **should not** be stopped in favor to [newStateWithNewContext]
* * True when [currentStateOnNewContext] **should** be stopped in favor to [newStateWithNewContext]
*/
suspend operator fun invoke(sourceStateWithOldContext: State, newStateWithNewContext: State, currentStateOnNewContext: State): Boolean?
}

View File

@ -6,105 +6,64 @@ import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.fsm.common.State 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.startup.launcher.StartLauncherPlugin
import dev.inmo.plagubot.config.* import dev.inmo.plagubot.config.*
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder
import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.webhook.deleteWebhook 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.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.* import kotlinx.serialization.Transient
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.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 java.io.File import org.koin.dsl.module
val Scope.plagubot: PlaguBot
get() = get()
val Koin.plagubot: PlaguBot
get() = get()
@OptIn(Warning::class) @OptIn(Warning::class)
@Serializable @Serializable
object PlaguBot : Plugin { data class PlaguBot(
override fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) { private val json: JsonObject,
scope.plugins.filter { it !== this@PlaguBot }.forEach { private val config: Config
with(it) { ) : Plugin {
setupBotClient(scope, params) @Transient
} private val bot = telegramBot(config.botToken)
}
}
override fun Module.setupDI(config: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), config) }
single { config } single { config }
single { get<Config>().databaseConfig } single { config.plugins }
single { get<Config>().databaseConfig.database } single { config.databaseConfig }
single { config.databaseConfig.database }
single { defaultJsonFormat }
single { this@PlaguBot } single { this@PlaguBot }
singlePlugins { get<dev.inmo.micro_utils.startup.launcher.Config>().plugins.filterIsInstance<Plugin>() } single { bot }
single {
val config = get<Config>() includes(
telegramBot( config.plugins.mapNotNull {
token = config.botToken, runCatching {
testServer = config.testServer, module {
apiUrl = config.botApiServer with(it) {
) { setupDI(database, params)
setupBotClient(this@single, get<JsonObject>())
} }
} }
}.onFailure { e ->
logger.w("Unable to load DI part of $it", e)
}.getOrNull()
}
)
} }
/**
* Getting all [OnStartContextsConflictResolver], [OnUpdateContextsConflictResolver], [StatesManager] and [DefaultStatesManagerRepo]
* and pass them into [buildBehaviourWithFSM] on top of [TelegramBot] took from [koin]. In time of
* [buildBehaviourWithFSM] configuration will call [setupBotPlugin] and [deleteWebhook].
*
* After all preparation, the result of [buildBehaviourWithFSM] will be passed to [startGettingOfUpdatesByLongPolling]
* as [CoroutineScope] and [UpdatesFilter].
*
* The [Job] took from [startGettingOfUpdatesByLongPolling] will be used to prevent app stopping by calling [Job.join]
* on it
*/
override suspend fun startPlugin(koin: Koin) {
super.startPlugin(koin)
val scope = koin.get<CoroutineScope>()
lateinit var behaviourContext: BehaviourContext
val onStartContextsConflictResolver by lazy { koin.getAllDistinct<OnStartContextsConflictResolver>() }
val onUpdateContextsConflictResolver by lazy { koin.getAllDistinct<OnUpdateContextsConflictResolver>() }
val bot = koin.get<TelegramBot>()
bot.buildBehaviourWithFSM(
scope = scope,
defaultExceptionsHandler = {
logger.e("Something went wrong", it)
},
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) {
koin.plugins.filter { it !== this@PlaguBot }.forEach { plugin -> config.plugins.forEach { plugin ->
runCatchingSafely { runCatchingSafely {
logger.i("Start loading of $plugin") logger.i("Start loading of $plugin")
with(plugin) { with(plugin) {
@ -119,47 +78,44 @@ object PlaguBot : Plugin {
} }
/** /**
* Starting plugins system using [StartLauncherPlugin.start]. In time of parsing [initialJson] [PlaguBot] may * This method will create an [Job] which will be the main [Job] of ran instance
* 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(initialJson: JsonObject): Job { suspend fun start(
val initialConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), initialJson) scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
): Job {
KSLog.i("Config has been read") logger.i("Start initialization")
val koinApp = KoinApplication.init()
// Adding of PlaguBot when it absent in config koinApp.modules(
val (resultJson, resultConfig) = if (PlaguBot in initialConfig.plugins) { module {
KSLog.i("Initial config contains PlaguBot, pass config as is to StartLauncherPlugin") setupDI(config.databaseConfig.database, json)
initialJson to initialConfig }
} else {
KSLog.i("Start fixing of PlaguBot absence. If PlaguBot has been skipped by some reason, use dev.inmo.micro_utils.startup.launcher.main as startup point or StartLauncherPlugin directly")
val resultJson = JsonObject(
initialJson + Pair("plugins", JsonArray(initialJson["plugins"]!!.jsonArray + JsonPrimitive(PlaguBot::class.qualifiedName!!)))
) )
val resultConfig = defaultJsonFormat.decodeFromJsonElement(dev.inmo.micro_utils.startup.launcher.Config.serializer(), resultJson) logger.i("Modules loaded")
resultJson to resultConfig GlobalContext.startKoin(koinApp)
logger.i("Koin started")
lateinit var behaviourContext: BehaviourContext
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 = { _, _ -> false }
),
onStateHandlingErrorHandler = koinApp.koin.getOrNull<StateHandlingErrorHandler<State>>() ?: { state, e ->
logger.eS(e) { "Unable to handle state $state" }
null
} }
) {
KSLog.i("Config initialization done. Passing config to StartLauncherPlugin") logger.i("Start setup of bot part")
return StartLauncherPlugin.start( behaviourContext = this
resultConfig, setupBotPlugin(koinApp.koin)
resultJson deleteWebhook()
).koin.get<CoroutineScope>().coroutineContext.job }.start()
logger.i("Behaviour builder has been setup")
return bot.startGettingOfUpdatesByLongPolling(scope = behaviourContext, updatesFilter = behaviourContext).also {
logger.i("Long polling has been started")
} }
/**
* 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

@ -1,18 +1,15 @@
package dev.inmo.plagubot.config package dev.inmo.plagubot.config
import dev.inmo.micro_utils.common.Warning import dev.inmo.micro_utils.common.Warning
import dev.inmo.micro_utils.startup.plugin.StartPlugin
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Warning("This API is internal and can be changed without notifications or mentions of changes") @Warning("This API is internal and can be changed without notifications of mentions of changes")
@Serializable @Serializable
data class Config( data class Config(
val botToken: String, val botToken: String,
val plugins: List<Plugin>,
@SerialName("database") @SerialName("database")
val databaseConfig: DatabaseConfig = DatabaseConfig(), val databaseConfig: DatabaseConfig = DatabaseConfig(),
val botApiServer: String = telegramBotAPIDefaultUrl,
val testServer: Boolean = false
) )

View File

@ -1,9 +1,7 @@
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

@ -8,7 +8,6 @@ buildscript {
classpath libs.buildscript.kt.gradle classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization classpath libs.buildscript.kt.serialization
classpath libs.buildscript.gh.release classpath libs.buildscript.gh.release
classpath libs.buildscript.ksp
} }
} }
@ -17,7 +16,7 @@ allprojects {
mavenCentral() mavenCentral()
mavenLocal() mavenLocal()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url "https://nexus.inmo.dev/repository/maven-releases/" } maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
} }
} }

View File

@ -20,13 +20,13 @@ if (new File(projectDir, "secret.gradle").exists()) {
releaseAssets.from('bot/build/distributions') releaseAssets.from('bot/build/distributions')
owner = "InsanusMokrassar" owner "InsanusMokrassar"
repo = "PlaguBot" repo "PlaguBot"
tagName = "v${project.version}" tagName "v${project.version}"
releaseName = "${project.version}" releaseName "${project.version}"
targetCommitish = "${project.version}" targetCommitish "${project.version}"
body = getCurrentVersionChangelog() body getCurrentVersionChangelog()
} }
} }

View File

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

View File

@ -1,22 +1,26 @@
[versions] [versions]
kt = "2.0.20" kt = "1.8.20"
kt-serialization = "1.7.2" kt-serialization = "1.5.0"
kt-coroutines = "1.8.1" kt-coroutines = "1.6.4"
microutils = "0.22.2" microutils = "0.17.8"
tgbotapi = "18.1.0" tgbotapi = "7.1.0"
kslog = "1.1.1"
ksp = "2.0.20-1.0.24" jb-exposed = "0.41.1"
jb-dokka = "1.8.10"
jb-exposed = "0.54.0" sqlite = "3.41.2.1"
jb-dokka = "1.9.20"
sqlite = "3.46.1.0" klock = "3.4.0"
uuid = "0.7.0"
gh-release = "2.5.2" ktor = "2.3.0"
koin = "3.5.6" gh-release = "2.4.1"
koin = "3.4.0"
[libraries] [libraries]
@ -28,10 +32,7 @@ kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json"
tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" } tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" }
tgbotapi-behaviourBuilder-fsm = { module = "dev.inmo:tgbotapi.behaviour_builder.fsm", version.ref = "tgbotapi" } tgbotapi-behaviourBuilder-fsm = { module = "dev.inmo:tgbotapi.behaviour_builder.fsm", version.ref = "tgbotapi" }
microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" } microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" }
microutils-koin = { module = "dev.inmo:micro_utils.koin", version.ref = "microutils" } kslog = { module = "dev.inmo:kslog", version.ref = "kslog" }
microutils-koin-generator = { module = "dev.inmo:micro_utils.koin.generator", version.ref = "microutils" }
microutils-startup-launcher = { module = "dev.inmo:micro_utils.startup.launcher", version.ref = "microutils" }
microutils-startup-plugin = { module = "dev.inmo:micro_utils.startup.plugin", version.ref = "microutils" }
koin = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
@ -47,4 +48,3 @@ buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin",
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", 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-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" } buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
buildscript-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }

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-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -14,12 +14,11 @@ dependencies {
api libs.tgbotapi api libs.tgbotapi
api libs.microutils.repos.exposed api libs.microutils.repos.exposed
api libs.microutils.startup.plugin
api libs.koin api libs.koin
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
} }

View File

@ -2,11 +2,11 @@ apply plugin: 'maven-publish'
task javadocJar(type: Jar) { task javadocJar(type: Jar) {
from javadoc from javadoc
archiveClassifier = 'javadoc' classifier = 'javadoc'
} }
task sourcesJar(type: Jar) { task sourcesJar(type: Jar) {
from sourceSets.main.allSource from sourceSets.main.allSource
archiveClassifier = 'sources' classifier = 'sources'
} }
publishing { publishing {
@ -49,14 +49,18 @@ publishing {
} }
} }
repositories { repositories {
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) { if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
maven { maven {
name = "InmoNexus" name = "Gitea"
url = uri("https://nexus.inmo.dev/repository/maven-releases/") url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
credentials { credentials(HttpHeaderCredentials) {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER') name = "Authorization"
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD') value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
}
authentication {
header(HttpHeaderAuthentication)
} }
} }
@ -92,27 +96,4 @@ if (project.hasProperty("signing.gnupg.keyName")) {
dependsOn(it) dependsOn(it)
} }
} }
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
} }

View File

@ -1 +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":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"} {"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":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"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,11 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -1,14 +1,13 @@
package dev.inmo.plagubot package dev.inmo.plagubot
import dev.inmo.micro_utils.fsm.common.State import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.startup.plugin.StartPlugin
import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext 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.scope.Scope import org.koin.core.module.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,17 +17,14 @@ import org.koin.core.scope.Scope
* too. * too.
*/ */
@Serializable(PluginSerializer::class) @Serializable(PluginSerializer::class)
interface Plugin : StartPlugin { interface Plugin {
@Deprecated("Deprecated in favor to setupBotClient with arguments")
fun KtorRequestsExecutorBuilder.setupBotClient() {}
/** /**
* Will be called on stage of bot setup * This method will be called when this plugin should configure di module based on the incoming params
*
* @param scope The scope of [org.koin.core.module.Module.single] of bot definition
* @param params Params (in fact, the whole bot config)
*/ */
fun KtorRequestsExecutorBuilder.setupBotClient(scope: Scope, params: JsonObject) = setupBotClient() fun Module.setupDI(
database: Database,
params: JsonObject
) {}
/** /**
* 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