From 0b6f3aeb17f2905319512badc59b5a73ffff1553 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 7 Sep 2022 15:01:03 +0600 Subject: [PATCH] temporal progress --- build.gradle | 2 + docker-compose.yml | 12 ++- gradle/libs.versions.toml | 3 + example.config.json => sample.config.json | 0 src/main/kotlin/App.kt | 110 ++++++++++++++++++++-- src/main/kotlin/ChatConfig.kt | 11 --- src/main/kotlin/ChatSettings.kt | 58 ++++++++++++ src/main/kotlin/EnableArgsParser.kt | 38 ++++++++ 8 files changed, 210 insertions(+), 24 deletions(-) rename example.config.json => sample.config.json (100%) delete mode 100644 src/main/kotlin/ChatConfig.kt create mode 100644 src/main/kotlin/ChatSettings.kt create mode 100644 src/main/kotlin/EnableArgsParser.kt diff --git a/build.gradle b/build.gradle index d0254d3..fba2f2e 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation libs.microutils.repos.cache implementation libs.kslog implementation libs.exposed + implementation libs.clikt + implementation libs.krontab implementation libs.psql implementation libs.imageboard } diff --git a/docker-compose.yml b/docker-compose.yml index 5696bc0..10fd0a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,12 @@ version: "3.4" services: - server: - build: . - restart: unless-stopped + booru_grabber_postgres: + image: postgres + container_name: "booru_grabber_postgres" + environment: + POSTGRES_USER: "test" + POSTGRES_PASSWORD: "test" + POSTGRES_DB: "test" + ports: + - "8092:5432" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cf8ce5..8871443 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ krontab = "0.8.0" kslog = "0.5.1" exposed = "0.39.2" psql = "42.5.0" +clikt = "3.5.0" [libraries] @@ -24,6 +25,8 @@ psql = { module = "org.postgresql:postgresql", version.ref = "psql" } imageboard = { module = "com.github.Kodehawa:imageboard-api", version.ref = "imageboard" } +clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } + # 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" } diff --git a/example.config.json b/sample.config.json similarity index 100% rename from example.config.json rename to sample.config.json diff --git a/src/main/kotlin/App.kt b/src/main/kotlin/App.kt index 9c56bbc..740f782 100644 --- a/src/main/kotlin/App.kt +++ b/src/main/kotlin/App.kt @@ -1,16 +1,30 @@ +import dev.inmo.krontab.utils.asFlow +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging import dev.inmo.micro_utils.repos.cache.cache.FullKVCache import dev.inmo.micro_utils.repos.cache.cached import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.mappers.withMapper +import dev.inmo.micro_utils.repos.unset import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.extensions.api.bot.getMe +import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands +import dev.inmo.tgbotapi.extensions.api.delete +import dev.inmo.tgbotapi.extensions.api.send.media.* import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand -import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommandWithArgs +import dev.inmo.tgbotapi.requests.abstracts.FileUrl +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.chat.ChannelChat +import dev.inmo.tgbotapi.types.chat.PrivateChat +import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto import java.io.File import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.json.Json import models.Config @@ -35,28 +49,104 @@ suspend fun main(args: Array) { "configs" ).withMapper( { chatId }, - { json.encodeToString(ChatConfig.serializer(), this) }, + { json.encodeToString(ChatSettings.serializer(), this) }, { ChatId(this) }, - { json.decodeFromString(ChatConfig.serializer(), this) }, + { json.decodeFromString(ChatSettings.serializer(), this) }, ).cached(FullKVCache(), scope = scope) val chatsChangingMutex = Mutex() val chatsSendingJobs = mutableMapOf() - // here should be main logic of your bot bot.buildBehaviourWithLongPolling(scope) { // in this lambda you will be able to call methods without "bot." prefix val me = getMe() - // this method will create point to react on each /start command - onCommand("start", requireOnlyCommandInMessage = true) { - // simply reply :) - reply(it, "Hello, I am ${me.firstName}") + suspend fun refreshChatJob(chatId: ChatId, settings: ChatSettings?) { + val settings = settings ?: repo.get(chatId) + chatsChangingMutex.withLock { + chatsSendingJobs[chatId] ?.cancel() + settings ?.let { + chatsSendingJobs[chatId] = settings.scheduler.asFlow().subscribeSafelyWithoutExceptions(scope) { + val result = settings.makeRequest() + when { + result.isEmpty() -> return@subscribeSafelyWithoutExceptions + result.size == 1 -> sendPhoto( + chatId, + FileUrl(result.first().url) + ) + settings.gallery -> result.chunked(mediaCountInMediaGroup.last + 1).forEach { + sendVisualMediaGroup( + chatId, + it.map { + TelegramMediaPhoto(FileUrl(it.url)) + } + ) + } + else -> result.forEach { + sendPhoto( + chatId, + FileUrl(it.url) + ) + } + } + } + } + } } - // That will be called on the end of bot initiation. After that println will be started long polling and bot will - // react on your commands + doForAllWithNextPaging { + repo.keys(it).also { + it.results.forEach { + refreshChatJob(it, null) + } + } + } + + repo.onNewValue.subscribeSafelyWithoutExceptions(this) { + refreshChatJob(it.first, it.second) + } + repo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { + refreshChatJob(it, null) + } + + onCommand(Regex("(help|start)"), requireOnlyCommandInMessage = true) { + reply(it, EnableArgsParser(it.chat.id, repo, scope).getFormattedHelp().takeIf { it.isNotBlank() } ?: return@onCommand) + } + onCommandWithArgs("enable") { message, strings -> + val parser = EnableArgsParser(message.chat.id, repo, this) + runCatchingSafely { + parser.parse(strings) + }.onFailure { e -> + e.printStackTrace() + if (message.chat is PrivateChat) { + reply(message, parser.getFormattedHelp()) + } + } + runCatchingSafely { + if (message.chat is ChannelChat) { + delete(message) + } + } + } + onCommand("disable", requireOnlyCommandInMessage = true) { + runCatchingSafely { + repo.unset(it.chat.id) + } + runCatchingSafely { + delete(it) + } + } + + setMyCommands( + listOf( + BotCommand("start", "Will return the help for the enable command"), + BotCommand("help", "Will return the help for the enable command"), + BotCommand("enable", "Will enable images grabbing for current chat or update exists settings"), + BotCommand("disable", "Will disable bot for current chat"), + ) + ) + println(me) }.join() } diff --git a/src/main/kotlin/ChatConfig.kt b/src/main/kotlin/ChatConfig.kt deleted file mode 100644 index 1750e1e..0000000 --- a/src/main/kotlin/ChatConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -import dev.inmo.krontab.* -import kotlinx.serialization.Serializable - -@Serializable -data class ChatConfig( - val krontab: KrontabTemplate -) { - val scheduler by lazy { - krontab.toSchedule() - } -} diff --git a/src/main/kotlin/ChatSettings.kt b/src/main/kotlin/ChatSettings.kt new file mode 100644 index 0000000..68c8fac --- /dev/null +++ b/src/main/kotlin/ChatSettings.kt @@ -0,0 +1,58 @@ +import dev.inmo.krontab.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.kodehawa.lib.imageboards.DefaultImageBoards +import net.kodehawa.lib.imageboards.ImageBoard +import net.kodehawa.lib.imageboards.boards.DefaultBoards +import net.kodehawa.lib.imageboards.entities.BoardImage + +@Serializable +data class ChatSettings( + val query: String, + val krontabTemplate: KrontabTemplate, + @Serializable(BoardSerializer::class) + private val boardBase: DefaultBoards, + val count: Int = 1, + val gallery: Boolean = false +) { + val scheduler by lazy { + krontabTemplate.toSchedule() + } + + val board: ImageBoard<*> + get() = when (boardBase) { + DefaultBoards.R34 -> DefaultImageBoards.RULE34 + DefaultBoards.E621 -> DefaultImageBoards.E621 + DefaultBoards.KONACHAN -> DefaultImageBoards.KONACHAN + DefaultBoards.YANDERE -> DefaultImageBoards.YANDERE + DefaultBoards.DANBOORU -> DefaultImageBoards.DANBOORU + DefaultBoards.SAFEBOORU -> DefaultImageBoards.SAFEBOORU + DefaultBoards.GELBOORU -> DefaultImageBoards.GELBOORU + DefaultBoards.E926 -> DefaultImageBoards.E926 + } + + suspend fun makeRequest(): List { + return withContext(Dispatchers.IO) { + board.search(count, query).blocking() + } + } + + @Serializer(DefaultBoards::class) + object BoardSerializer : KSerializer { + override val descriptor: SerialDescriptor = String.serializer().descriptor + val types = DefaultBoards.values().associateBy { it.name.lowercase() } + override fun deserialize(decoder: Decoder): DefaultBoards { + val type = decoder.decodeString() + return types.getValue(type) + } + + override fun serialize(encoder: Encoder, value: DefaultBoards) { + encoder.encodeString(value.name.lowercase()) + } + } +} diff --git a/src/main/kotlin/EnableArgsParser.kt b/src/main/kotlin/EnableArgsParser.kt new file mode 100644 index 0000000..47de15d --- /dev/null +++ b/src/main/kotlin/EnableArgsParser.kt @@ -0,0 +1,38 @@ +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.* +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.int +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.set +import dev.inmo.tgbotapi.types.ChatId +import kotlinx.coroutines.CoroutineScope + +class EnableArgsParser( + private val chatId: ChatId, + private val repo: KeyValueRepo, + private val scope: CoroutineScope +) : CliktCommand(name = "enable") { + val count by option("-n").int().help("Amount of pictures to grab each trigger time").default(1).check("Count should be in range 1-10") { + it in 1 .. 10 + } + val query by argument().multiple(required = true).help("Your query to booru. Use syntax \"-- -sometag\" to add excluding of some tag in query") + val krontab by option("-k", "--krontab").required().help("Krontab in format * * * * *. See https://bookstack.inmo.dev/books/krontab/page/string-format") + val board by option("-b", "--board").convert { + ChatSettings.BoardSerializer.types.getValue(it) + }.required().help("Board type. Possible values: ${ChatSettings.BoardSerializer.types.keys.joinToString { it }}") + val gallery by option("-g", "--gallery").flag(default = false).help("Effective only when count passed > 1. Will send chosen images as gallery instead of separated images") + + override fun run() { + val chatSettings = ChatSettings( + query.joinToString(" "), + krontab, + board, + count, + gallery + ) + scope.launchSafelyWithoutExceptions { + repo.set(chatId, chatSettings) + } + } +}