temporal progress

This commit is contained in:
InsanusMokrassar 2022-09-07 15:01:03 +06:00
parent 4a6e7e472b
commit 0b6f3aeb17
8 changed files with 210 additions and 24 deletions

View File

@ -28,6 +28,8 @@ dependencies {
implementation libs.microutils.repos.cache implementation libs.microutils.repos.cache
implementation libs.kslog implementation libs.kslog
implementation libs.exposed implementation libs.exposed
implementation libs.clikt
implementation libs.krontab
implementation libs.psql implementation libs.psql
implementation libs.imageboard implementation libs.imageboard
} }

View File

@ -1,6 +1,12 @@
version: "3.4" version: "3.4"
services: services:
server: booru_grabber_postgres:
build: . image: postgres
restart: unless-stopped container_name: "booru_grabber_postgres"
environment:
POSTGRES_USER: "test"
POSTGRES_PASSWORD: "test"
POSTGRES_DB: "test"
ports:
- "8092:5432"

View File

@ -8,6 +8,7 @@ krontab = "0.8.0"
kslog = "0.5.1" kslog = "0.5.1"
exposed = "0.39.2" exposed = "0.39.2"
psql = "42.5.0" psql = "42.5.0"
clikt = "3.5.0"
[libraries] [libraries]
@ -24,6 +25,8 @@ psql = { module = "org.postgresql:postgresql", version.ref = "psql" }
imageboard = { module = "com.github.Kodehawa:imageboard-api", version.ref = "imageboard" } imageboard = { module = "com.github.Kodehawa:imageboard-api", version.ref = "imageboard" }
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
# Libs for classpath # Libs for classpath
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 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" } kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }

View File

@ -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.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.cached import dev.inmo.micro_utils.repos.cache.cached
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.mappers.withMapper 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.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe 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.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling import dev.inmo.tgbotapi.extensions.behaviour_builder.buildBehaviourWithLongPolling
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand 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 java.io.File
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import models.Config import models.Config
@ -35,28 +49,104 @@ suspend fun main(args: Array<String>) {
"configs" "configs"
).withMapper( ).withMapper(
{ chatId }, { chatId },
{ json.encodeToString(ChatConfig.serializer(), this) }, { json.encodeToString(ChatSettings.serializer(), this) },
{ ChatId(this) }, { ChatId(this) },
{ json.decodeFromString(ChatConfig.serializer(), this) }, { json.decodeFromString(ChatSettings.serializer(), this) },
).cached(FullKVCache(), scope = scope) ).cached(FullKVCache(), scope = scope)
val chatsChangingMutex = Mutex() val chatsChangingMutex = Mutex()
val chatsSendingJobs = mutableMapOf<ChatId, Job>() val chatsSendingJobs = mutableMapOf<ChatId, Job>()
// here should be main logic of your bot // here should be main logic of your bot
bot.buildBehaviourWithLongPolling(scope) { bot.buildBehaviourWithLongPolling(scope) {
// in this lambda you will be able to call methods without "bot." prefix // in this lambda you will be able to call methods without "bot." prefix
val me = getMe() val me = getMe()
// this method will create point to react on each /start command suspend fun refreshChatJob(chatId: ChatId, settings: ChatSettings?) {
onCommand("start", requireOnlyCommandInMessage = true) { val settings = settings ?: repo.get(chatId)
// simply reply :) chatsChangingMutex.withLock {
reply(it, "Hello, I am ${me.firstName}") 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 doForAllWithNextPaging {
// react on your commands 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) println(me)
}.join() }.join()
} }

View File

@ -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()
}
}

View File

@ -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<BoardImage> {
return withContext(Dispatchers.IO) {
board.search(count, query).blocking()
}
}
@Serializer(DefaultBoards::class)
object BoardSerializer : KSerializer<DefaultBoards> {
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())
}
}
}

View File

@ -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<ChatId, ChatSettings>,
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)
}
}
}