3 Commits

Author SHA1 Message Date
aaf01d686b Merge pull request #29 from InsanusMokrassar/0.0.6
0.0.6
2026-05-07 12:14:11 +06:00
348f17127f update dependencies and upfill readme 2026-05-07 12:12:54 +06:00
f546c4791f update dependencies 2025-09-20 01:34:35 +06:00
7 changed files with 125 additions and 28 deletions

View File

@@ -13,6 +13,99 @@ Bot for booru-boards grabbing. It uses [imageboard-api](https://github.com/Kodeh
Sample bot presented here: [@booru_grabber_bot](https://t.me/booru_grabber_bot) Sample bot presented here: [@booru_grabber_bot](https://t.me/booru_grabber_bot)
## Fast how to start
1. Create a `config.json`. Minimal required config:
```json
{
"token": "your bot token",
"database": {
"url": "jdbc:postgresql://booru_grabber_postgres:5432/test",
"username": "test",
"password": "test"
}
}
```
All available properties:
```json
{
"token": "your bot token",
"database": {
"url": "jdbc:postgresql://booru_grabber_postgres:5432/test",
"driver": "org.postgresql.Driver",
"username": "test",
"password": "test",
"reconnectOptions": {
"attempts": 3,
"delay": 1000
}
},
"client": {
"connectionTimeoutMillis": null,
"requestTimeoutMillis": null,
"responseTimeoutMillis": null,
"proxy": {
"hostname": "proxy.example.com",
"type": "socks",
"port": 1080,
"username": null,
"password": null
}
}
}
```
Property reference:
**`token`** *(required)* — Telegram bot token from [@BotFather](https://t.me/BotFather).
**`database`** *(required)*:
- `url` — JDBC connection URL (default: `jdbc:pgsql://localhost:12346/test`)
- `driver` — JDBC driver class (default: `org.postgresql.Driver`)
- `username` — database user (default: empty)
- `password` — database password (default: empty)
- `reconnectOptions.attempts` — how many times to retry connecting on startup (default: `3`)
- `reconnectOptions.delay` — delay in milliseconds between retries (default: `1000`)
**`client`** *(optional)* — HTTP client settings for Telegram API requests:
- `connectionTimeoutMillis` — connection timeout in ms
- `requestTimeoutMillis` — write/request timeout in ms
- `responseTimeoutMillis` — read/response timeout in ms
- `proxy.hostname` *(required if proxy set)* — proxy host
- `proxy.type``socks` (default) or `http`
- `proxy.port` — proxy port (default: `1080` for socks, `3128` for http)
- `proxy.username` — proxy username (optional; for socks, password is required when username is set)
- `proxy.password` — proxy password (optional)
2. In `docker-compose.yml`, uncomment the `booru_grabber_bot` service and set the path to your config file:
```yaml
services:
booru_grabber_postgres:
image: postgres
container_name: "booru_grabber_postgres"
environment:
POSTGRES_USER: "test"
POSTGRES_PASSWORD: "test"
POSTGRES_DB: "test"
booru_grabber_bot:
image: insanusmokrassar/booru_grabber_bot
container_name: "booru_grabber_bot"
volumes:
- "/absolute/path/to/config.json:/booru_grabber_bot/config.json:ro"
```
3. Start the services:
```bash
docker-compose up -d
```
## Available commands ## Available commands
Bot have two helping commands: `/start` and `/help`. These commands will return help for bot `/request`/`/enable` commands: Bot have two helping commands: `/start` and `/help`. These commands will return help for bot `/request`/`/enable` commands:

View File

@@ -4,4 +4,4 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true kotlin.js.generate.externals=true
kotlin.incremental=true kotlin.incremental=true
docker_version=0.0.5 docker_version=0.0.6

View File

@@ -1,15 +1,15 @@
[versions] [versions]
kotlin = "2.0.21" kotlin = "2.3.21"
tgbotapi = "20.0.1" tgbotapi = "33.1.0"
microutils = "0.23.0" microutils = "0.29.2"
imageboard = "2.7.0" imageboard = "2.6.1.1"
krontab = "2.6.1" krontab = "2.9.0"
kslog = "1.3.6" kslog = "1.6.1"
ktor = "3.0.1" ktor = "3.4.3"
exposed = "0.55.0" exposed = "1.2.0"
psql = "42.7.4" psql = "42.7.11"
clikt = "5.0.1" clikt = "5.1.0"
[libraries] [libraries]

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -106,12 +106,12 @@ suspend fun main(args: Array<String>) {
} }
result.take(settings.count) result.take(settings.count)
}.takeIf { it.isNotEmpty() } ?: return }.takeIf { it.isNotEmpty() } ?: return
runCatchingSafely { runCatchingLogging {
val urls = result.map { it.url } val urls = result.map { it.url }
chatsUrlsSeenRepo.add(chatId, urls) chatsUrlsSeenRepo.add(chatId, urls)
seenUrls.addAll(urls) seenUrls.addAll(urls)
when { when {
urls.isEmpty() -> return@runCatchingSafely urls.isEmpty() -> return@runCatchingLogging
urls.size == 1 -> sendPhoto( urls.size == 1 -> sendPhoto(
chatId, chatId,
FileUrl(urls.first()), FileUrl(urls.first()),
@@ -143,24 +143,24 @@ suspend fun main(args: Array<String>) {
chatsChangingMutex.withLock { chatsChangingMutex.withLock {
chatsSendingJobs[chatId] ?.cancel() chatsSendingJobs[chatId] ?.cancel()
settings ?.scheduler ?.let { settings ?.scheduler ?.let {
chatsSendingJobs[chatId] = it.asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) { chatsSendingJobs[chatId] = it.asFlowWithDelays().subscribeLoggingDropExceptions(scope) {
triggerSendForChat(chatId, settings) triggerSendForChat(chatId, settings)
} }
} }
} }
} }
repo.onNewValue.subscribeSafelyWithoutExceptions(this) { repo.onNewValue.subscribeLoggingDropExceptions(this) {
refreshChatJob(it.first, it.second) refreshChatJob(it.first, it.second)
} }
repo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { repo.onValueRemoved.subscribeLoggingDropExceptions(this) {
refreshChatJob(it, null) refreshChatJob(it, null)
} }
doForAllWithNextPaging { doForAllWithNextPaging {
repo.keys(it).also { repo.keys(it).also {
it.results.forEach { it.results.forEach {
runCatchingSafely { runCatchingLogging {
refreshChatJob(it, null) refreshChatJob(it, null)
} }
} }
@@ -173,16 +173,16 @@ suspend fun main(args: Array<String>) {
onCommand("enable", requireOnlyCommandInMessage = false) { onCommand("enable", requireOnlyCommandInMessage = false) {
val args = it.content.textSources.drop(1).joinToString("") { it.source }.split(" ") val args = it.content.textSources.drop(1).joinToString("") { it.source }.split(" ")
val parser = EnableArgsParser() val parser = EnableArgsParser()
runCatchingSafely { runCatchingLogging {
parser.parse(args) parser.parse(args)
repo.set(ChatId(it.chat.id.chatId), parser.resultSettings ?: return@runCatchingSafely) repo.set(ChatId(it.chat.id.chatId), parser.resultSettings ?: return@runCatchingLogging)
}.onFailure { e -> }.onFailure { e ->
e.printStackTrace() e.printStackTrace()
if (it.chat is PrivateChat) { if (it.chat is PrivateChat) {
reply(it, parser.getFormattedHelp()!!) reply(it, parser.getFormattedHelp()!!)
} }
} }
runCatchingSafely { runCatchingLogging {
if (it.chat is ChannelChat) { if (it.chat is ChannelChat) {
delete(it) delete(it)
} }
@@ -200,7 +200,7 @@ suspend fun main(args: Array<String>) {
} }
} else { } else {
val parser = EnableArgsParser(repo.get(ChatId(it.chat.id.chatId)) ?: ChatSettings.DEFAULT) val parser = EnableArgsParser(repo.get(ChatId(it.chat.id.chatId)) ?: ChatSettings.DEFAULT)
runCatchingSafely { runCatchingLogging {
parser.parse(args) parser.parse(args)
parser.resultSettings parser.resultSettings
}.onFailure { e -> }.onFailure { e ->
@@ -214,18 +214,18 @@ suspend fun main(args: Array<String>) {
triggerSendForChat(ChatId(it.chat.id.chatId), chatSettings ?: return@onCommand) triggerSendForChat(ChatId(it.chat.id.chatId), chatSettings ?: return@onCommand)
} }
onCommand("disable", requireOnlyCommandInMessage = true) { onCommand("disable", requireOnlyCommandInMessage = true) {
runCatchingSafely { runCatchingLogging {
repo.unset(ChatId(it.chat.id.chatId)) repo.unset(ChatId(it.chat.id.chatId))
} }
runCatchingSafely { runCatchingLogging {
delete(it) delete(it)
} }
} }
onCommand("take_settings", requireOnlyCommandInMessage = true) { onCommand("take_settings", requireOnlyCommandInMessage = true) {
val settings = runCatchingSafely { val settings = runCatchingLogging {
repo.get(ChatId(it.chat.id.chatId)) repo.get(ChatId(it.chat.id.chatId))
}.getOrNull() }.getOrNull()
runCatchingSafely { runCatchingLogging {
if (settings == null) { if (settings == null) {
reply(it, "You didn't enable requesting") reply(it, "You didn't enable requesting")
} else { } else {

View File

@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalSerializationApi::class)
import dev.inmo.krontab.* import dev.inmo.krontab.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -44,6 +46,8 @@ data class ChatSettings(
} }
} }
@ExperimentalSerializationApi
@Suppress("EXTERNAL_SERIALIZER_USELESS")
@Serializer(DefaultBoards::class) @Serializer(DefaultBoards::class)
object BoardSerializer : KSerializer<DefaultBoards> { object BoardSerializer : KSerializer<DefaultBoards> {
override val descriptor: SerialDescriptor = String.serializer().descriptor override val descriptor: SerialDescriptor = String.serializer().descriptor

View File

@@ -4,8 +4,8 @@ import dev.inmo.kslog.common.e
import dev.inmo.kslog.common.logger import dev.inmo.kslog.common.logger
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.sql.transactions.transactionManager import org.jetbrains.exposed.v1.jdbc.transactions.transactionManager
import org.postgresql.Driver import org.postgresql.Driver
import java.sql.Connection import java.sql.Connection