BooruGrabberTelegramBot/src/main/kotlin/App.kt

258 lines
11 KiB
Kotlin
Raw Normal View History

import dev.inmo.krontab.utils.asFlowWithDelays
2022-09-07 11:34:07 +00:00
import dev.inmo.micro_utils.coroutines.*
2022-09-07 09:01:03 +00:00
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
2022-09-09 10:19:14 +00:00
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.full.fullyCached
2022-09-06 18:52:45 +00:00
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
2022-09-07 11:34:07 +00:00
import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedKeyValuesRepo
2022-09-06 18:52:45 +00:00
import dev.inmo.micro_utils.repos.mappers.withMapper
2022-09-06 18:24:01 +00:00
import dev.inmo.tgbotapi.bot.ktor.telegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
2022-09-07 09:01:03 +00:00
import dev.inmo.tgbotapi.extensions.api.bot.setMyCommands
import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.send.media.*
2022-09-06 18:24:01 +00:00
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
2022-09-07 09:01:03 +00:00
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
2022-12-11 06:17:15 +00:00
import dev.inmo.tgbotapi.utils.code
2022-09-06 18:24:01 +00:00
import java.io.File
import kotlinx.coroutines.*
2022-09-06 18:52:45 +00:00
import kotlinx.coroutines.sync.Mutex
2022-09-07 09:01:03 +00:00
import kotlinx.coroutines.sync.withLock
2022-09-06 18:24:01 +00:00
import kotlinx.serialization.json.Json
2022-09-06 18:52:45 +00:00
import models.Config
2022-09-07 11:34:07 +00:00
import net.kodehawa.lib.imageboards.ImageBoard
2022-12-11 06:17:15 +00:00
import net.kodehawa.lib.imageboards.boards.DefaultBoards
2022-09-07 11:34:07 +00:00
import net.kodehawa.lib.imageboards.entities.BoardImage
2022-09-06 18:24:01 +00:00
/**
* This method by default expects one argument in [args] field: telegram bot configuration
*/
suspend fun main(args: Array<String>) {
// create json to decode config
val json = Json { ignoreUnknownKeys = true }
// decode config
val config: Config = json.decodeFromString(Config.serializer(), File(args.first()).readText())
// that is your bot
val bot = telegramBot(config.token)
2022-09-07 11:34:07 +00:00
ImageBoard.setUserAgent("WhoAmI?")
2022-09-06 18:24:01 +00:00
// that is kotlin coroutine scope which will be used in requests and parallel works under the hood
2022-09-07 11:34:07 +00:00
val scope = CoroutineScope(Dispatchers.Default + ContextSafelyExceptionHandler { it.printStackTrace() })
2022-09-06 18:24:01 +00:00
2022-09-06 18:52:45 +00:00
val repo = ExposedKeyValueRepo(
config.database.database,
{ long("chat_id") },
{ text("config") },
"configs"
).withMapper(
{ chatId },
2022-09-07 09:01:03 +00:00
{ json.encodeToString(ChatSettings.serializer(), this) },
2022-09-06 18:52:45 +00:00
{ ChatId(this) },
2022-09-07 09:01:03 +00:00
{ json.decodeFromString(ChatSettings.serializer(), this) },
).fullyCached(scope = scope)
2022-09-06 18:52:45 +00:00
val chatsUrlsSeenRepo = ExposedKeyValuesRepo(
2022-09-07 11:34:07 +00:00
config.database.database,
{ long("chat_id") },
{ text("url") },
"chatsUrlsSeen"
).withMapper(
{ chatId },
{ this },
{ ChatId(this) },
{ this },
)
2022-09-07 11:34:07 +00:00
2022-09-06 18:52:45 +00:00
val chatsChangingMutex = Mutex()
val chatsSendingJobs = mutableMapOf<ChatId, Job>()
2022-09-06 18:24:01 +00:00
// 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()
2022-09-07 14:13:39 +00:00
suspend fun triggerSendForChat(chatId: ChatId, settings: ChatSettings) {
val seenUrls = chatsUrlsSeenRepo.getAll(chatId).toMutableSet()
2022-09-07 14:13:39 +00:00
val result = let {
val result = mutableListOf<BoardImage>()
var i = 0
while (result.size < settings.count) {
val images = settings.makeRequest(i).takeIf { it.isNotEmpty() } ?: break
result.addAll(
images.filterNot {
seenUrls.contains(it.url ?: return@filterNot true)
2022-09-07 14:13:39 +00:00
}
)
i++
}
result.take(settings.count)
2022-09-07 14:13:39 +00:00
}.takeIf { it.isNotEmpty() } ?: return
runCatchingSafely {
2022-09-09 10:19:14 +00:00
val urls = result.map { it.url }
chatsUrlsSeenRepo.add(chatId, urls)
seenUrls.addAll(urls)
2022-09-07 14:13:39 +00:00
when {
urls.isEmpty() -> return@runCatchingSafely
urls.size == 1 -> sendPhoto(
chatId,
2022-09-09 10:42:59 +00:00
FileUrl(urls.first()),
if (settings.attachUrls) urls.first() else null
2022-09-07 14:13:39 +00:00
)
settings.gallery -> urls.chunked(mediaCountInMediaGroup.last + 1).forEach {
sendVisualMediaGroup(
chatId,
it.map {
2022-09-09 10:42:59 +00:00
TelegramMediaPhoto(FileUrl(it), if (settings.attachUrls) it else null)
2022-09-07 14:13:39 +00:00
}
)
}
else -> urls.forEach {
sendPhoto(
chatId,
2022-09-09 10:42:59 +00:00
FileUrl(it),
if (settings.attachUrls) it else null
2022-09-07 14:13:39 +00:00
)
}
}
}.onFailure {
triggerSendForChat(chatId, settings)
}
}
2022-09-07 09:01:03 +00:00
suspend fun refreshChatJob(chatId: ChatId, settings: ChatSettings?) {
val settings = settings ?: repo.get(chatId)
chatsChangingMutex.withLock {
chatsSendingJobs[chatId] ?.cancel()
2022-09-09 10:28:14 +00:00
settings ?.scheduler ?.let {
chatsSendingJobs[chatId] = it.asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) {
2022-09-07 14:13:39 +00:00
triggerSendForChat(chatId, settings)
2022-09-07 09:01:03 +00:00
}
}
}
}
repo.onNewValue.subscribeSafelyWithoutExceptions(this) {
refreshChatJob(it.first, it.second)
}
repo.onValueRemoved.subscribeSafelyWithoutExceptions(this) {
refreshChatJob(it, null)
}
2022-09-07 09:01:03 +00:00
doForAllWithNextPaging {
repo.keys(it).also {
it.results.forEach {
2022-09-07 11:34:07 +00:00
runCatchingSafely {
refreshChatJob(it, null)
}
2022-09-07 09:01:03 +00:00
}
}
}
onCommand(Regex("(help|start)"), requireOnlyCommandInMessage = true) {
2023-10-05 14:29:43 +00:00
reply(it, EnableArgsParser().getFormattedHelp() ?.takeIf { it.isNotBlank() } ?: return@onCommand)
2022-09-07 09:01:03 +00:00
}
2022-09-07 11:34:07 +00:00
onCommand("enable", requireOnlyCommandInMessage = false) {
val args = it.content.textSources.drop(1).joinToString("") { it.source }.split(" ")
val parser = EnableArgsParser()
2022-09-07 09:01:03 +00:00
runCatchingSafely {
2022-09-07 11:34:07 +00:00
parser.parse(args)
2022-12-11 06:02:36 +00:00
repo.set(ChatId(it.chat.id.chatId), parser.resultSettings ?: return@runCatchingSafely)
2022-09-07 09:01:03 +00:00
}.onFailure { e ->
e.printStackTrace()
2022-09-07 11:34:07 +00:00
if (it.chat is PrivateChat) {
2023-10-05 14:29:43 +00:00
reply(it, parser.getFormattedHelp()!!)
2022-09-07 09:01:03 +00:00
}
}
runCatchingSafely {
2022-09-07 11:34:07 +00:00
if (it.chat is ChannelChat) {
delete(it)
2022-09-07 09:01:03 +00:00
}
}
}
2022-09-09 10:19:14 +00:00
onCommand("request", requireOnlyCommandInMessage = false) {
2022-09-09 10:28:14 +00:00
val args = it.content.textSources.drop(1).joinToString("") { it.source }.trim().takeIf { it.isNotBlank() } ?.split(" ")
2022-09-09 10:19:14 +00:00
2022-09-09 10:28:14 +00:00
val chatSettings = if (args.isNullOrEmpty()) {
2022-12-11 06:02:36 +00:00
repo.get(ChatId(it.chat.id.chatId)) ?: run {
2022-09-09 10:19:14 +00:00
if (it.chat is PrivateChat) {
reply(it, "Unable to find default config")
}
return@onCommand
}
} else {
val parser = EnableArgsParser(repo.get(ChatId(it.chat.id.chatId)) ?: ChatSettings.DEFAULT)
2022-09-09 10:19:14 +00:00
runCatchingSafely {
parser.parse(args)
parser.resultSettings
}.onFailure { e ->
e.printStackTrace()
if (it.chat is PrivateChat) {
2023-10-05 14:29:43 +00:00
reply(it, parser.getFormattedHelp()!!)
2022-09-09 10:19:14 +00:00
}
}.getOrNull()
}
2022-12-11 06:02:36 +00:00
triggerSendForChat(ChatId(it.chat.id.chatId), chatSettings ?: return@onCommand)
2022-09-09 10:19:14 +00:00
}
2022-09-07 09:01:03 +00:00
onCommand("disable", requireOnlyCommandInMessage = true) {
runCatchingSafely {
2022-12-11 06:02:36 +00:00
repo.unset(ChatId(it.chat.id.chatId))
2022-09-07 09:01:03 +00:00
}
runCatchingSafely {
delete(it)
}
}
2022-12-11 06:17:15 +00:00
onCommand("take_settings", requireOnlyCommandInMessage = true) {
val settings = runCatchingSafely {
repo.get(ChatId(it.chat.id.chatId))
}.getOrNull()
runCatchingSafely {
if (settings == null) {
reply(it, "You didn't enable requesting")
} else {
reply(it, ) {
+"Query: " + code(settings.query) + "\n"
+"Krontab: " + code(settings.krontabTemplate ?: "unset") + "\n"
+"Board: " + code(DefaultBoards.values().first { it == settings.board.boardType }.name) + "\n"
+"Count: " + code(settings.count.toString()) + "\n"
+"Gallery: " + code(settings.gallery.toString()) + "\n"
+"Rating: " + code(settings.rating ?.name ?: "unset") + "\n"
+"Attach urls: " + code(settings.attachUrls.toString()) + "\n"
+"Command: " + code(
"/request " +
"${settings.query} " +
(settings.krontabTemplate ?.let { "-k $it " } ?: "") +
"-b ${DefaultBoards.values().first { it == settings.board.boardType }.name.lowercase()} " +
"-n ${settings.count} " +
(if (settings.gallery) "-g " else "") +
(settings.rating ?.let { "-r ${it.name} " } ?: "") +
(if (settings.attachUrls) "-a " else "")
)
2022-12-11 06:17:15 +00:00
}
}
}
}
2022-09-07 09:01:03 +00:00
setMyCommands(
listOf(
BotCommand("start", "Will return the help for the enable command"),
BotCommand("help", "Will return the help for the enable command"),
2022-09-09 10:19:14 +00:00
BotCommand("request", "Will trigger image immediately with custom settings from arguments or default settings of chat if any"),
2022-09-07 09:01:03 +00:00
BotCommand("enable", "Will enable images grabbing for current chat or update exists settings"),
BotCommand("disable", "Will disable bot for current chat"),
2022-12-11 06:17:15 +00:00
BotCommand("take_settings", "Take your current settings"),
2022-09-07 09:01:03 +00:00
)
)
2022-09-06 18:24:01 +00:00
println(me)
}.join()
}