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.kslog
implementation libs.exposed
implementation libs.clikt
implementation libs.krontab
implementation libs.psql
implementation libs.imageboard
}

View File

@ -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"

View File

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

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.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<String>) {
"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<ChatId, Job>()
// 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()
}

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