mirror of
				https://github.com/InsanusMokrassar/BooruGrabberTelegramBot.git
				synced 2025-10-25 09:10:13 +00:00 
			
		
		
		
	temporal progress
This commit is contained in:
		| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -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" } | ||||||
|   | |||||||
| @@ -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() | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user