From dcf4433551ce65911724ffef6234795fa326727d Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sat, 9 Jul 2022 20:49:33 +0600 Subject: [PATCH] initialization --- CHANGELOG.md | 5 + build.gradle | 4 + changelog_parser.sh | 24 +++++ github_release.gradle | 30 ++++++ gradle.properties | 4 +- gradle/libs.versions.toml | 1 + publish.gradle | 91 ++++++++++++++++ publish.kpsb | 1 + settings.gradle | 2 +- src/main/kotlin/BotCommandFullInfo.kt | 45 ++++++++ src/main/kotlin/CommandsKeeper.kt | 148 ++++++++++++++++++++++++++ src/main/kotlin/CommandsKeeperKey.kt | 35 ++++++ src/main/kotlin/CommandsPlugin.kt | 78 ++++++++++++++ src/main/kotlin/Plugin.kt | 45 -------- 14 files changed, 465 insertions(+), 48 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 changelog_parser.sh create mode 100644 github_release.gradle create mode 100644 publish.gradle create mode 100644 publish.kpsb create mode 100644 src/main/kotlin/BotCommandFullInfo.kt create mode 100644 src/main/kotlin/CommandsKeeper.kt create mode 100644 src/main/kotlin/CommandsKeeperKey.kt create mode 100644 src/main/kotlin/CommandsPlugin.kt delete mode 100644 src/main/kotlin/Plugin.kt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d738e8d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +* Project has been created diff --git a/build.gradle b/build.gradle index 29226db..84d3ea6 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { dependencies { classpath libs.kotlin.gradle.plugin classpath libs.kotlin.serialization.plugin + classpath libs.gh.release.plugin } } @@ -17,6 +18,9 @@ plugins { project.group = project_group project.version = project_version +apply from: "./publish.gradle" +apply from: "./github_release.gradle" + repositories { mavenCentral() mavenLocal() diff --git a/changelog_parser.sh b/changelog_parser.sh new file mode 100644 index 0000000..9dd39e6 --- /dev/null +++ b/changelog_parser.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +function parse() { + version="$1" + + while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+ $version"`" ] + do + : # do nothing + done + + while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+"`" ] + do + echo "$line" + done +} + +version="$1" +file="$2" + +if [ -n "$file" ]; then + parse "$version" < "$file" +else + parse "$version" +fi diff --git a/github_release.gradle b/github_release.gradle new file mode 100644 index 0000000..bc91add --- /dev/null +++ b/github_release.gradle @@ -0,0 +1,30 @@ +private String getCurrentVersionChangelog() { + OutputStream changelogDataOS = new ByteArrayOutputStream() + exec { + commandLine 'chmod', "+x", './changelog_parser.sh' + } + exec { + standardOutput = changelogDataOS + commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md' + } + + return changelogDataOS.toString().trim() +} + +if (new File(projectDir, "secret.gradle").exists()) { + apply from: './secret.gradle' + apply plugin: "com.github.breadmoirai.github-release" + + githubRelease { + token "${project.property('GITHUB_RELEASE_TOKEN')}" + + owner "InsanusMokrassar" + repo "PlaguBotCommandsPlugin" + + tagName "v${project.version}" + releaseName "${project.version}" + targetCommitish "${project.version}" + + body getCurrentVersionChangelog() + } +} diff --git a/gradle.properties b/gradle.properties index b6ab4da..800e323 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ org.gradle.parallel=true kotlin.js.generate.externals=true kotlin.incremental=true -project_group=plagubot_plugin -project_version=0.0.1 +project_group=dev.inmo +project_version=0.1.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 128807e..fc364c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } # Libs for classpath kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } +gh-release-plugin = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" } [plugins] diff --git a/publish.gradle b/publish.gradle new file mode 100644 index 0000000..a02c5f2 --- /dev/null +++ b/publish.gradle @@ -0,0 +1,91 @@ +apply plugin: 'maven-publish' + +task javadocJar(type: Jar) { + from javadoc + classifier = 'javadoc' +} +task sourcesJar(type: Jar) { + from sourceSets.main.allSource + classifier = 'sources' +} + +publishing { + publications { + maven(MavenPublication) { + from components.java + + artifact javadocJar + artifact sourcesJar + + pom { + resolveStrategy = Closure.DELEGATE_FIRST + + description = "Plugin for centralized declaration and management of commands inside of plagubot system" + name = "${project.name}" + url = "https://github.com/InsanusMokrassar/PlaguBotCommandsPlugin" + + scm { + developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/PlaguBotCommandsPlugin.git[push=]https://github.com/InsanusMokrassar/PlaguBotCommandsPlugin.git" + url = "https://github.com/InsanusMokrassar/PlaguBotCommandsPlugin.git" + } + + developers { + + developer { + id = "InsanusMokrassar" + name = "Ovsiannikov Aleksei" + email = "ovsyannikov.alexey95@gmail.com" + } + + } + + licenses { + + license { + name = "MIT License" + url = "https://opensource.org/licenses/MIT" + } + + } + } + repositories { + if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) { + maven { + name = "GithubPackages" + url = uri("https://maven.pkg.github.com/InsanusMokrassar/PlaguBotCommandsPlugin") + credentials { + username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER') + password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD') + } + } + } + if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) { + maven { + name = "sonatype" + url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") + credentials { + username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER') + password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') + } + } + } + } + } + } +} + +if (project.hasProperty("signing.gnupg.keyName")) { + apply plugin: 'signing' + + signing { + useGpgCmd() + + sign publishing.publications + } + + task signAll { + tasks.withType(Sign).forEach { + dependsOn(it) + } + } +} diff --git a/publish.kpsb b/publish.kpsb new file mode 100644 index 0000000..17e7571 --- /dev/null +++ b/publish.kpsb @@ -0,0 +1 @@ +{"licenses":[{"id":"MIT","title":"MIT License","url":"https://opensource.org/licenses/MIT"}],"mavenConfig":{"name":"${project.name}","description":"Plugin for centralized declaration and management of commands inside of plagubot system","url":"https://github.com/InsanusMokrassar/PlaguBotCommandsPlugin","vcsUrl":"https://github.com/InsanusMokrassar/PlaguBotCommandsPlugin.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/PlaguBotCommandsPlugin"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 3ccd004..0c45a7e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'plagubot_plugin' +rootProject.name = 'plagubot.plugins.commands' diff --git a/src/main/kotlin/BotCommandFullInfo.kt b/src/main/kotlin/BotCommandFullInfo.kt new file mode 100644 index 0000000..0b83a7b --- /dev/null +++ b/src/main/kotlin/BotCommandFullInfo.kt @@ -0,0 +1,45 @@ +package dev.inmo.plagubot.plugins.commands + +import dev.inmo.micro_utils.language_codes.IetfLanguageCode +import dev.inmo.tgbotapi.types.BotCommand +import dev.inmo.tgbotapi.types.commands.BotCommandScope +import kotlinx.serialization.Serializable + +/** + * Full info about the command its [key] and the [command] itself + * + * @see full + */ +@Serializable +data class BotCommandFullInfo( + val key: CommandsKeeperKey, + val command: BotCommand +) { + constructor(command: BotCommand) : this(CommandsKeeperKey.DEFAULT, command) +} + +fun BotCommand.full( + key: CommandsKeeperKey = CommandsKeeperKey.DEFAULT +) = BotCommandFullInfo(key, this) + +fun BotCommand.full( + scope: BotCommandScope +) = full(CommandsKeeperKey(scope)) + +fun BotCommand.full( + languageCode: String +) = full(CommandsKeeperKey(languageCode = languageCode)) + +fun BotCommand.full( + languageCode: IetfLanguageCode +) = full(CommandsKeeperKey(BotCommandScope.Default, languageCode = languageCode)) + +fun BotCommand.full( + scope: BotCommandScope, + languageCode: String +) = full(CommandsKeeperKey(scope, languageCode)) + +fun BotCommand.full( + scope: BotCommandScope, + languageCode: IetfLanguageCode +) = full(CommandsKeeperKey(scope, languageCode)) diff --git a/src/main/kotlin/CommandsKeeper.kt b/src/main/kotlin/CommandsKeeper.kt new file mode 100644 index 0000000..536a267 --- /dev/null +++ b/src/main/kotlin/CommandsKeeper.kt @@ -0,0 +1,148 @@ +package dev.inmo.plagubot.plugins.commands + +import dev.inmo.micro_utils.language_codes.IetfLanguageCode +import dev.inmo.tgbotapi.types.BotCommand +import dev.inmo.tgbotapi.types.commands.BotCommandScope +import dev.inmo.tgbotapi.types.commands.BotCommandScopeDefault +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * In memory commands keeper. Contains all the registered commands inside and can be useful in case you wish to + * [addCommand] or [removeCommand] + */ +class CommandsKeeper( + preset: List = emptyList() +) { + internal val onScopeChanged = MutableSharedFlow() + private val scopesCommands: MutableMap> = preset.groupBy { + it.key + }.mapValues { (_, v) -> + v.map { it.command }.toMutableSet() + }.toMutableMap() + private val changesMutex = Mutex() + + suspend fun addCommand(scope: CommandsKeeperKey, command: BotCommand) { + changesMutex.withLock { + val added = scopesCommands.getOrPut(scope) { + mutableSetOf() + }.add(command) + + if (added) { + onScopeChanged.emit(scope) + } + } + } + + suspend fun addCommand( + scope: BotCommandScope, + command: BotCommand + ) = addCommand( + CommandsKeeperKey(scope, null), + command + ) + + suspend fun addCommand( + languageCode: String, + command: BotCommand + ) = addCommand( + CommandsKeeperKey(languageCode = languageCode), + command + ) + + suspend fun addCommand( + languageCode: IetfLanguageCode, + command: BotCommand + ) = addCommand( + CommandsKeeperKey(BotCommandScopeDefault, languageCode), + command + ) + + suspend fun addCommand( + scope: BotCommandScope, + languageCode: IetfLanguageCode, + command: BotCommand + ) = addCommand( + CommandsKeeperKey(scope, languageCode), + command + ) + + suspend fun addCommand( + scope: BotCommandScope, + languageCode: String, + command: BotCommand + ) = addCommand( + CommandsKeeperKey(scope, languageCode), + command + ) + + suspend fun addCommand( + command: BotCommand + ) = addCommand( + CommandsKeeperKey.DEFAULT, + command + ) + + suspend fun removeCommand(scope: CommandsKeeperKey, command: BotCommand) { + changesMutex.withLock { + val removed = scopesCommands[scope] ?.remove(command) == true + + if (removed) { + onScopeChanged.emit(scope) + } + } + } + + suspend fun removeCommand( + scope: BotCommandScope, + command: BotCommand + ) = removeCommand( + CommandsKeeperKey(scope), + command + ) + + suspend fun removeCommand( + languageCode: String, + command: BotCommand + ) = removeCommand( + CommandsKeeperKey(languageCode = languageCode), + command + ) + + suspend fun removeCommand( + languageCode: IetfLanguageCode, + command: BotCommand + ) = removeCommand( + CommandsKeeperKey(BotCommandScopeDefault, languageCode), + command + ) + + suspend fun removeCommand( + scope: BotCommandScope, + languageCode: IetfLanguageCode, + command: BotCommand + ) = removeCommand( + CommandsKeeperKey(scope, languageCode), + command + ) + + suspend fun removeCommand( + scope: BotCommandScope, + languageCode: String, + command: BotCommand + ) = removeCommand( + CommandsKeeperKey(scope, languageCode), + command + ) + + suspend fun removeCommand( + command: BotCommand + ) = removeCommand( + CommandsKeeperKey.DEFAULT, + command + ) + + internal fun getCommands(scope: CommandsKeeperKey) = scopesCommands[scope] ?.toList() + internal fun getKeys() = scopesCommands.keys.toList() +} diff --git a/src/main/kotlin/CommandsKeeperKey.kt b/src/main/kotlin/CommandsKeeperKey.kt new file mode 100644 index 0000000..249608a --- /dev/null +++ b/src/main/kotlin/CommandsKeeperKey.kt @@ -0,0 +1,35 @@ +package dev.inmo.plagubot.plugins.commands + +import dev.inmo.micro_utils.language_codes.IetfLanguageCode +import dev.inmo.tgbotapi.types.commands.BotCommandScope +import dev.inmo.tgbotapi.types.commands.BotCommandScopeDefault +import kotlinx.serialization.Serializable + +/** + * Full info about the command scope including [BotCommandScope] and its optional language code (see [languageCode] and + * [languageCodeIetf]) + * + * @see CommandsKeeperKey.DEFAULT + */ +@Serializable +@JvmInline +value class CommandsKeeperKey( + val key: Pair +) { + val scope: BotCommandScope + get() = key.first + val languageCode: String? + get() = key.second + val languageCodeIetf: IetfLanguageCode? + get() = languageCode ?.let(::IetfLanguageCode) + + constructor(scope: BotCommandScope = BotCommandScope.Default, languageCode: String? = null) : this(scope to languageCode) + constructor(scope: BotCommandScope, languageCode: IetfLanguageCode) : this(scope to languageCode.code) + + companion object { + /** + * Default realization of [CommandsKeeperKey] with null [languageCode] and [BotCommandScope.Default] [scope] + */ + val DEFAULT = CommandsKeeperKey() + } +} diff --git a/src/main/kotlin/CommandsPlugin.kt b/src/main/kotlin/CommandsPlugin.kt new file mode 100644 index 0000000..210e733 --- /dev/null +++ b/src/main/kotlin/CommandsPlugin.kt @@ -0,0 +1,78 @@ +package dev.inmo.plagubot.plugins.commands + +import dev.inmo.kslog.common.* +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.plagubot.Plugin +import dev.inmo.tgbotapi.extensions.api.bot.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.types.BotCommand +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import org.jetbrains.exposed.sql.Database +import org.koin.core.Koin +import org.koin.core.module.Module + +/** + * This plugin has been created for centralized work with commands in context of [Plugin]s system of plagubot. Pass + * [BotCommandFullInfo] in your [Plugin.setupDI] section to declare some command. You may use [CommandsKeeper] for + * flexible setup of commands in runtime. + */ +@Serializable +class CommandsPlugin : Plugin { + private val log = KSLog(logTag) + + /** + * Creating [CommandsKeeper] and pass it to the DI. It uses [org.koin.core.scope.Scope.getAll] to get all the + * [BotCommandFullInfo] instances declared in the DI. + */ + override fun Module.setupDI(database: Database, params: JsonObject) { + single { CommandsKeeper(getAll().distinct()) } + } + + private suspend fun BehaviourContext.setScopeCommands(key: CommandsKeeperKey, commands: List?) { + runCatchingSafely { + commands ?.let { + setMyCommands( + commands, + key.scope, + key.languageCode + ) + } ?: deleteMyCommands( + key.scope, + key.languageCode + ) + }.onFailure { + log.e { + "Unable to ${if (commands == null) "delete commands" else "set new commands (${commands.joinToString { it.command }})"} for key $key" + } + }.onSuccess { + log.i { + "Successfully ${if (commands == null) "deleted commands" else "set new commands (${commands.joinToString { it.command }})"} for key $key" + } + } + } + + /** + * Uses [CommandsKeeper] from [koin]. Subscribe on [CommandsKeeper.scopesCommands] to follow changed in scopes and + * take all the available keys in the [CommandsKeeper] and set commands for each key + */ + override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { + val commandsKeeper = koin.get() + + log.d { "Subscribe to scopes changed flow" } + commandsKeeper.onScopeChanged.subscribeSafelyWithoutExceptions(scope) { + val commands = commandsKeeper.getCommands(it) + setScopeCommands(it, commands) + } + log.d { "Subscribed to scopes changed flow" } + + log.d { "Start setup initially passed commands" } + commandsKeeper.getKeys().forEach { + val commands = commandsKeeper.getCommands(it) + log.d { "Start setup initially passed commands for key $it: ${commands ?.joinToString { it.command }}" } + setScopeCommands(it, commandsKeeper.getCommands(it)) + } + log.d { "Complete setup initially passed commands" } + } +} diff --git a/src/main/kotlin/Plugin.kt b/src/main/kotlin/Plugin.kt deleted file mode 100644 index d268b60..0000000 --- a/src/main/kotlin/Plugin.kt +++ /dev/null @@ -1,45 +0,0 @@ -package plagubot_plugin - -import dev.inmo.kslog.common.KSLog -import dev.inmo.kslog.common.logTag -import dev.inmo.kslog.common.d -import dev.inmo.kslog.common.dS -import dev.inmo.plagubot.Plugin -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.triggers_handling.onCommand -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import org.jetbrains.exposed.sql.Database -import org.koin.core.Koin -import org.koin.core.module.Module - -// Left empty constructor for the plugin instantiation in bot -@Serializable -class PlaguBotPlugin : Plugin { - private val logger = KSLog(logTag) - - @Serializable - data class Config( - val someParam: String = "Default value" - ) - override fun Module.setupDI(database: Database, params: JsonObject) { - // Here you may declare any dependencies you need - single { - // Take from config value by field "plugin". Replace to your plugin name and fill Config with your plugin configuration - get().decodeFromJsonElement(Config.serializer(), params["plugin"] ?: return@single null) - } - } - - override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { - // Here you may configure the behaviour of your plugin - // All dependencies available in bot can be accessed via koin - logger.d { koin.get().someParam } - logger.dS { getMe().toString() } - onCommand("hello_world") { - reply(it, "Hello :)") - } - } -}