mirror of
https://github.com/InsanusMokrassar/BooruGrabberTelegramBot.git
synced 2024-12-22 06:07:11 +00:00
temporal progress
This commit is contained in:
parent
4a6e7e472b
commit
0b6f3aeb17
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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" }
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
58
src/main/kotlin/ChatSettings.kt
Normal file
58
src/main/kotlin/ChatSettings.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
38
src/main/kotlin/EnableArgsParser.kt
Normal file
38
src/main/kotlin/EnableArgsParser.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user