143 Commits
0.0.4 ... 0.5.4

Author SHA1 Message Date
cef350667b update dependencies 2024-02-18 22:41:33 +06:00
80ed679241 start 0.5.4 2024-02-18 21:26:02 +06:00
39391636ac Merge pull request #20 from InsanusMokrassar/several_sources
Several sources
2024-02-15 20:38:17 +06:00
29a19df7fc update dependencies and version 2024-02-15 20:30:10 +06:00
250f88e2fe preview adding of several sources chats 2024-02-15 20:21:48 +06:00
bb433a6441 Merge pull request #21 from InsanusMokrassar/0.5.2
0.5.2
2023-12-11 00:21:28 +06:00
7a8166153f update dependencies 2023-12-11 00:01:05 +06:00
114add0391 start 0.5.2 2023-12-10 23:58:00 +06:00
58b1f26502 0.5.1 2023-11-12 21:57:21 +06:00
ba3d054f0f Update docker-compose.yml 2023-11-09 01:55:24 +06:00
eef2bfce14 add support of inline messages with data callback query in common posts gc 2023-11-06 22:02:54 +06:00
fe96101631 change check of yes/no in checking of messages 2023-11-06 21:56:37 +06:00
7abb6efba3 add force posts check shortcut in inline mode 2023-11-06 21:48:03 +06:00
2f0a823f7c Merge pull request #19 from InsanusMokrassar/0.5.0
0.5.0
2023-11-06 21:31:25 +06:00
730e3c50e9 complete adding of common posts gc 2023-11-06 21:27:43 +06:00
0cc0510876 add retryOnPostFailureTimes 2023-11-06 19:18:19 +06:00
947bd7c2c4 fix build 2023-11-06 18:59:17 +06:00
7f54e86962 update github workflows 2023-11-06 18:52:27 +06:00
db419165a7 fix in publish.gradle 2023-11-06 18:48:43 +06:00
a5b0f429a0 fix settings.gradle 2023-11-06 17:57:21 +06:00
9c161b6dab update dependencies 2023-11-06 17:56:47 +06:00
f6067bb096 start 0.5.0 2023-11-06 17:54:13 +06:00
248740f246 update samples 2023-11-02 22:06:48 +06:00
3ae3cabd80 update sample folder 2023-10-31 20:31:47 +06:00
5fd4042fe3 Merge pull request #18 from InsanusMokrassar/0.4.0
0.4.0
2023-10-31 20:30:00 +06:00
6df4546b81 update sample files 2023-10-31 20:29:06 +06:00
12635c654a update sample files 2023-10-31 20:28:19 +06:00
15bd013eaa Update libs.versions.toml 2023-09-30 07:28:37 +06:00
39b607c4e7 Update gradle.properties 2023-09-26 16:11:33 +06:00
98f3e2a461 Update libs.versions.toml 2023-09-26 16:11:06 +06:00
0d31d90efd Merge pull request #17 from InsanusMokrassar/0.3.0
0.3.0
2023-08-21 11:44:07 +06:00
0ce202a5f6 fill changelog 2023-08-20 15:59:05 +06:00
077f8c30a6 update dependencies 2023-08-20 15:57:10 +06:00
1e9559a2c9 Update config.json 2023-08-13 00:11:04 +06:00
feef8efee1 Merge pull request #16 from InsanusMokrassar/0.2.3
0.2.3
2023-08-13 00:04:27 +06:00
54eb7515d3 small fixes in repos 2023-08-13 00:02:19 +06:00
1bb12bee0e fix of build 2023-08-12 23:58:28 +06:00
467525e48d fill changelog 2023-08-12 23:52:53 +06:00
29e5a04135 update dependencies && add opportunity to use several targetChatIds instead of one 2023-08-12 23:50:56 +06:00
6eb43055a7 0.2.3 2023-08-12 23:31:30 +06:00
57eebb61d5 0.2.2 2023-05-10 11:05:14 +06:00
87957dba30 update dependenies. 0.2.1 2023-05-07 19:05:30 +06:00
20148c02f0 0.2.0 2023-04-25 00:48:39 +06:00
e17cfa1c7c update dependencies 2023-04-20 00:41:19 +06:00
0a5ffee808 improvements 2023-04-06 15:52:44 +06:00
847b285ce3 Merge pull request #15 from InsanusMokrassar/0.1.1
0.1.1
2023-03-30 12:14:07 +06:00
c449457d86 update krontab 2023-03-18 14:11:40 +06:00
1b3a632d7b replace OfferTemplate 2023-03-18 14:05:09 +06:00
ebfa79cf64 add OfferTemplate and change autoschedule command 2023-03-18 13:59:37 +06:00
e59c7b0f7e fixes 2023-03-18 13:48:26 +06:00
7a4fb05bfb add publishing_autoschedule 2023-03-18 13:12:05 +06:00
7bc7bf6e8c update dependencies 2023-03-18 12:35:18 +06:00
c64faf75d0 start 0.1.1 2023-03-18 12:28:35 +06:00
ee80d8a3a1 Merge pull request #14 from InsanusMokrassar/0.1.0
0.1.0
2023-03-14 23:58:49 +06:00
22c94a4c43 update base image 2023-03-12 22:56:20 +06:00
c6bcfc0068 upgrade version retieving from gradle.properties 2023-03-12 22:49:34 +06:00
8a648cb066 fixes in docker publishing script 2023-03-12 22:41:41 +06:00
345a156334 fixes 2023-03-12 22:38:21 +06:00
7fb7f923f7 Update libs.versions.toml 2023-03-12 17:37:52 +06:00
3b858a3c00 Update libs.versions.toml 2023-03-09 12:00:57 +06:00
f09e80b8bd fixes for building 2023-03-05 23:30:22 +06:00
fea25743d5 Update libs.versions.toml 2023-03-05 00:55:00 +06:00
86183f5f74 Update libs.versions.toml 2023-02-28 14:25:45 +06:00
bc8d0b26bd start 0.1.0 2023-02-28 14:23:35 +06:00
b05844737b Merge pull request #13 from InsanusMokrassar/0.0.10
0.0.10
2023-02-21 22:13:19 +06:00
d1b597d2c9 add deploy workflow 2023-02-21 22:11:51 +06:00
aef864d5fd update dependencies 2023-02-21 22:08:11 +06:00
f438ede791 Update libs.versions.toml 2023-02-11 18:33:47 +06:00
dc5833c407 fixes 2023-01-18 23:39:56 +06:00
2e7a1b83c5 update microutils 2023-01-18 23:23:22 +06:00
b603fa8822 add caches 2023-01-18 23:22:54 +06:00
8206131425 start 0.0.10 2023-01-18 22:51:01 +06:00
12d3d5eeea Merge pull request #12 from InsanusMokrassar/0.0.9
0.0.9
2023-01-05 22:37:07 +06:00
2c335b43ab add changelog 2023-01-05 20:12:51 +06:00
2b84c224ec add publishing settings 2023-01-05 20:11:59 +06:00
46adc04a9b Update libs.versions.toml 2023-01-02 10:26:13 +06:00
cffc6a62c2 update dependencies 2022-12-20 10:00:24 +06:00
d6b684a17e start 0.0.9 2022-12-20 09:11:59 +06:00
5cd3a6fb35 Merge pull request #11 from InsanusMokrassar/0.0.8
0.0.8
2022-12-15 11:00:27 +06:00
b453401c33 Update libs.versions.toml 2022-12-15 10:37:48 +06:00
01b9d0b2ab experimental fix of krontab 2022-12-14 23:20:45 +06:00
5f65095698 hotfix 2022-12-14 16:45:06 +06:00
2baaac8e6d add several supporting hints 2022-12-14 16:38:14 +06:00
8899fb299f actualize sample onfig 2022-12-14 12:10:21 +06:00
bc0324a34f small fixes 2022-12-14 12:09:20 +06:00
b9c78982b5 complete timer plugin 2022-12-14 11:50:02 +06:00
c632a2ba14 add timers repo and timers handler 2022-12-14 09:43:12 +06:00
9403b133f9 Update ButtonsBuilder.kt 2022-12-13 14:00:13 +06:00
00803fa933 fixes 2022-12-13 13:16:55 +06:00
74f3503413 add fix todo 2022-12-13 12:41:30 +06:00
34e253a12e add checking of current date 2022-12-13 12:39:32 +06:00
65dfe8abd0 start adding of timer 2022-12-13 12:32:18 +06:00
8c42f2e879 update dependencies 2022-12-13 11:27:57 +06:00
2b41082a48 Merge pull request #10 from InsanusMokrassar/0.0.7
0.0.7
2022-12-08 21:59:53 +06:00
0dc459d5dc update dependencies and add interactive mode for ratings 2022-12-08 10:28:42 +06:00
4024b040e0 update dependencies 2022-12-05 18:12:45 +06:00
74cf8c1a9a temporal progress on ratings buttons 2022-12-02 18:46:58 +06:00
82640a0c5d start adding of ratings buttons 2022-11-28 20:21:03 +06:00
2052d003e5 update gradle wrapper version 2022-11-28 19:33:58 +06:00
791a161f8c update dependencies 2022-11-28 19:32:51 +06:00
d6bd90267d start 0.0.7 2022-11-28 19:29:14 +06:00
bea7fb7e46 Merge pull request #9 from InsanusMokrassar/0.0.6
0.0.6
2022-11-25 15:07:54 +06:00
44b2b849e4 more fixes to god of fixes 2022-11-18 15:39:06 +06:00
8730c67084 fix of an error when message in reply didn't contain any post 2022-11-18 15:24:21 +06:00
3242810ef6 add panel command 2022-11-18 12:54:18 +06:00
4423eba1d9 fixes 2022-11-18 12:43:53 +06:00
1f6dd7aad1 fixes in panel 2022-11-18 11:58:42 +06:00
5366dcdba1 updates and fixes 2022-11-17 15:09:10 +06:00
18ed638bcc start 0.0.6 2022-11-17 14:18:48 +06:00
6673b6c69b fixes in exposed posts repo 2022-10-25 13:02:19 +06:00
65f613fd97 Merge pull request #6 from InsanusMokrassar/0.0.5
0.0.5
2022-10-25 12:53:44 +06:00
d605c8c650 small fixes 2022-10-25 12:51:45 +06:00
c47c7b09f4 fixes in exposed tables 2022-10-25 12:46:31 +06:00
4c8c93c9f8 update ExposedPostsRepo 2022-10-25 12:43:12 +06:00
a3ee0d4c3b Update build.yml 2022-10-25 00:29:47 +06:00
c8085701d4 Update build.gradle 2022-10-25 00:29:11 +06:00
2daaf8a6b3 Update build.gradle 2022-10-25 00:27:01 +06:00
100ee1520f Update build.gradle 2022-10-25 00:25:06 +06:00
94b3c97efa Update build.gradle 2022-10-25 00:24:38 +06:00
bf8436fa4b Update build.gradle 2022-10-25 00:24:05 +06:00
5ea80ca1e5 Update build.gradle 2022-10-25 00:23:08 +06:00
a563267da0 Update build.gradle 2022-10-25 00:22:43 +06:00
4aae1230bc Update build.gradle 2022-10-25 00:22:12 +06:00
8f29810291 Update build.gradle 2022-10-25 00:21:37 +06:00
851f3a1c55 Update build.gradle 2022-10-25 00:21:09 +06:00
b398ad43ab Update build.gradle 2022-10-25 00:20:37 +06:00
1ad7988ae5 Update build.gradle 2022-10-25 00:18:59 +06:00
e114d74c74 Update build.gradle 2022-10-25 00:18:27 +06:00
6b82879a3e Update build.gradle 2022-10-25 00:17:52 +06:00
c55376da88 Update build.gradle 2022-10-25 00:16:32 +06:00
6389912b1d Update libs.versions.toml 2022-10-25 00:14:45 +06:00
ad98ca339d Update libs.versions.toml 2022-10-25 00:14:02 +06:00
3e62eca8ac Update libs.versions.toml 2022-10-25 00:12:27 +06:00
f4e097f7d1 Update mppProjectWithSerialization.gradle 2022-10-25 00:10:34 +06:00
3d5e15d545 Update mppProjectWithSerialization.gradle 2022-10-25 00:09:18 +06:00
eef167422e Delete mppAndroidProject.gradle 2022-10-25 00:07:43 +06:00
5ae07394dc Delete defaultAndroidSettings.gradle 2022-10-25 00:06:56 +06:00
4ac30cc667 Update libs.versions.toml 2022-10-25 00:01:49 +06:00
6a43cb32c6 init scheduling trigger 2022-10-11 11:43:26 +06:00
e285cc9ec6 deprecate uncommon inline plugin 2022-10-11 11:38:50 +06:00
ceec312208 start 0.0.5 2022-10-11 11:28:59 +06:00
089be36601 fixes 2022-10-03 11:16:09 +06:00
af1bddcc85 Merge pull request #5 from InsanusMokrassar/0.0.4
0.0.4
2022-10-03 00:57:58 +06:00
106 changed files with 2415 additions and 827 deletions

View File

@@ -1,19 +0,0 @@
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Build with Gradle
run: ./gradlew build

27
.github/workflows/build_and_publish.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
- name: Build
run: ./gradlew build
- name: Publish
continue-on-error: true
run: ./gradlew publishAllPublicationsToGiteaRepository
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

31
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Docker
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
if [[ "$branch" != "master" ]]; then
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
fi
- name: Log into registry
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Deploy
run: ./gradlew build && cd ./runner && ./nonsudo_deploy.sh

82
CHANGELOG.md Normal file
View File

@@ -0,0 +1,82 @@
# PlaguPoster
## 0.5.4
* Dependencies update
## 0.5.3
* Dependencies update
## 0.5.2
* Dependencies update
## 0.5.1
* Add opportunity to set unique
## 0.5.0
* Dependencies update
* Since this update bots will require **`JDK` 17+**
## 0.3.0
* `Versions`:
* `tgbotapi`: `9.1.0`
* `plagubot`: `7.1.0`
* `plagubot-plugins`: `0.14.0`
## 0.2.3
* Add opportunity to use several target chat ids
* Update dependencies
## 0.2.2
* `GarbageCollector`:
* Now on start will all clearing job done
## 0.2.1
* `Versions`:
* `kotlin`: `1.8.21`
* `tgbotapi`: `7.1.2`
* `plagubot`: `5.1.2`
* `microutils`: `0.18.1`
* `kslog`: `1.1.1`
* `plagubot.plugins`: `0.11.2`
* `psql`: `42.6.0`
## 0.2.0
* `Versions`:
* `tgbotapi`: `7.1.0`
* `plagubot`: `5.1.0`
* `krontab`: `1.0.0`
* `plagubot.plugins`: `0.11.0`
## 0.1.2
* `Versions`:
* `kotlin`: `1.8.20`
* `plagubot`: `5.0.2`
* `microutils`: `0.17.8`
* `kslog`: `1.1.1`
* `plagubot.plugins`: `0.10.2`
* `psql`: `42.6.0`
## 0.1.1
* Update dependencies
* `Triggers`
* `SelectorWithTimer`
* Opportunity to get schedule of posts using `publishing_autoschedule` command
## 0.0.10
## 0.0.9
* Update dependencies

View File

@@ -7,8 +7,6 @@ buildscript {
}
dependencies {
classpath libs.android.tools.build
classpath libs.android.dexcount
classpath libs.kotlin.gradle.plugin
classpath libs.kotlin.serialization.plugin
classpath libs.kotlin.dokka.plugin
@@ -20,8 +18,9 @@ allprojects {
mavenLocal()
mavenCentral()
google()
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
}
}
apply from: "./extensions.gradle"
// apply from: "./github_release.gradle"
apply from: "./github_release.gradle"

24
changelog_parser.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
function parse() {
version="$1"
while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+ $version"`" ]
do
: # do nothing
done
while IFS= read -r line && [ -z "`echo "$line" | grep -e "^#\+"`" ]
do
echo "$line"
done
}
version="$1"
file="$2"
if [ -n "$file" ]; then
parse "$version" < "$file"
else
parse "$version"
fi

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -12,7 +11,9 @@ kotlin {
dependencies {
api libs.tgbotapi
api libs.microutils.repos.common
api libs.microutils.repos.cache
api libs.kslog
api libs.microutils.koin
}
}
jvmMain {

View File

@@ -1,15 +1,43 @@
package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ChatConfig(
@SerialName("targetChat")
val targetChatId: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
val targetChatId: IdChatIdentifier? = null,
@SerialName("sourceChat")
val sourceChatId: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
val sourceChatId: IdChatIdentifier?,
@SerialName("cacheChat")
val cacheChatId: ChatId
)
@Serializable(FullChatIdentifierSerializer::class)
val cacheChatId: IdChatIdentifier,
@SerialName("targetChats")
val targetChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
@SerialName("sourceChats")
val sourceChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
) {
val allTargetChatIds by lazy {
(listOfNotNull(targetChatId) + targetChatIds).toSet()
}
val allSourceChatIds by lazy {
(listOfNotNull(sourceChatId) + sourceChatIds).toSet()
}
init {
require(targetChatId != null || targetChatIds.isNotEmpty()) {
"One of fields, 'targetChat' or 'targetChats' should be presented"
}
}
fun check(chatId: IdChatIdentifier) = when (chatId) {
in allTargetChatIds,
in allSourceChatIds,
cacheChatId -> true
else -> false
}
}

View File

@@ -2,13 +2,14 @@ package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.message.abstracts.*
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
val FirstSourceIsCommandsFilter = SimpleFilter<Message> {
it is ContentMessage<*> && it.content.textContentOrNull() ?.textSources ?.firstOrNull {
it is BotCommandTextSource
} != null
it.contentMessageOrNull() ?.withContentOrNull<TextContent>() ?.content ?.textSources ?.firstOrNull() is BotCommandTextSource
}

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.common
import com.soywiz.klock.DateTime
import korlibs.time.DateTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.serializer
@@ -8,7 +8,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializer(DateTime::class)
object DateTimeSerializer : KSerializer<DateTime> {
override val descriptor: SerialDescriptor = Double.serializer().descriptor
override fun deserialize(decoder: Decoder): DateTime = DateTime(decoder.decodeDouble())

View File

@@ -1,13 +1,16 @@
package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.message.abstracts.Message
import kotlinx.serialization.Serializable
@Serializable
data class ShortMessageInfo(
val chatId: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier,
val messageId: MessageIdentifier
)

View File

@@ -0,0 +1,16 @@
package dev.inmo.plaguposter.common
import org.koin.core.Koin
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
val Scope.useCache: Boolean
get() = getOrNull(named("useCache")) ?: false
val Koin.useCache: Boolean
get() = getOrNull(named("useCache")) ?: false
fun Module.useCache(useCache: Boolean) {
single(named("useCache")) { useCache }
}

View File

@@ -0,0 +1,33 @@
package dev.inmo.plaguposter.common
import dev.inmo.kslog.common.iS
import dev.inmo.kslog.common.logger
import dev.inmo.plagubot.Plugin
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
object CommonPlugin : Plugin {
private val Log = logger
override fun Module.setupDI(database: Database, params: JsonObject) {
single { CoroutineScope(Dispatchers.Default + SupervisorJob()) }
val useCache = (params["useCache"] as? JsonPrimitive) ?.booleanOrNull ?: true
useCache(useCache)
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val config = koin.get<ChatConfig>()
Log.iS { "Target chats info: ${config.allTargetChatIds.map { getChat(it) }.joinToString()}" }
Log.iS { "Source chats info: ${config.allSourceChatIds.map { getChat(it) }.joinToString()}" }
Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" }
}
}

View File

@@ -1,66 +0,0 @@
apply plugin: 'com.getkeepsafe.dexcount'
android {
ext {
jvmKotlinFolderFile = {
String sep = File.separator
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
}
enableIncludingJvmCodeInAndroidPart = {
File jvmKotlinFolder = jvmKotlinFolderFile()
if (jvmKotlinFolder.exists()) {
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
}
}
disableIncludingJvmCodeInAndroidPart = {
File jvmKotlinFolder = jvmKotlinFolderFile()
String[] oldDirs = android.sourceSets.main.java.srcDirs
android.sourceSets.main.java.srcDirs = []
for (oldDir in oldDirs) {
if (oldDir != jvmKotlinFolder.path) {
android.sourceSets.main.java.srcDirs += oldDir
}
}
}
}
compileSdkVersion libs.versions.android.compileSdk.get().toInteger()
buildToolsVersion libs.versions.android.buildTools.get()
defaultConfig {
minSdkVersion libs.versions.android.minSdk.get().toInteger()
targetSdkVersion libs.versions.android.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
}
buildTypes {
release {
minifyEnabled false
}
debug {
debuggable true
}
}
packagingOptions {
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sourceSets {
String sep = File.separator
main.java.srcDirs += "src${sep}main${sep}kotlin"
}
}

View File

@@ -20,6 +20,6 @@ allprojects {
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
// publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
}
}

31
github_release.gradle Normal file
View File

@@ -0,0 +1,31 @@
private String getCurrentVersionChangelog() {
OutputStream changelogDataOS = new ByteArrayOutputStream()
exec {
commandLine 'chmod', "+x", './changelog_parser.sh'
}
exec {
standardOutput = changelogDataOS
commandLine './changelog_parser.sh', "${project.version}", 'CHANGELOG.md'
}
return changelogDataOS.toString().trim()
}
if (new File(projectDir, "secret.gradle").exists()) {
apply from: './secret.gradle'
apply plugin: "com.github.breadmoirai.github-release"
githubRelease {
token "${project.property('GITHUB_RELEASE_TOKEN')}"
owner "InsanusMokrassar"
repo "PlaguPoster"
tagName "v${project.version}"
releaseName "${project.version}"
targetCommitish "${project.version}"
body getCurrentVersionChangelog()
}
}

View File

@@ -10,5 +10,4 @@ android.enableJetifier=true
# Project data
group=dev.inmo
version=0.0.4
android_code_version=4
version=0.5.4

View File

@@ -1,26 +1,18 @@
[versions]
kotlin = "1.7.10"
kotlin-serialization = "1.4.0"
kotlin = "1.9.22"
kotlin-serialization = "1.6.2"
plagubot = "2.3.4"
tgbotapi = "3.2.7"
microutils = "0.12.16"
kslog = "0.5.2"
krontab = "0.8.1"
tgbotapi-libraries = "0.5.5"
plagubot = "8.2.0"
tgbotapi = "10.1.0"
microutils = "0.20.35"
kslog = "1.3.2"
krontab = "2.2.8"
plagubot-plugins = "0.18.2"
psql = "42.5.0"
dokka = "1.9.10"
dexcount = "3.1.0"
junit_version = "4.12"
test_ext_junit_version = "1.1.3"
espresso_core = "3.4.0"
android-gradle-plugin = "7.2.2"
android-minSdk = "21"
android-compileSdk = "33"
android-buildTools = "33.0.0"
psql = "42.6.0"
[libraries]
@@ -30,12 +22,12 @@ kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", versi
kotlin-test-annotations-common = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
kotlin-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kotlin" }
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "test_ext_junit_version" }
android-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso_core" }
tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" }
plagubot-plugin = { module = "dev.inmo:plagubot.plugin", version.ref = "plagubot" }
plagubot-bot = { module = "dev.inmo:plagubot.bot", version.ref = "plagubot" }
plagubot-plugins-inline-queries = { module = "dev.inmo:plagubot.plugins.inline.queries", version.ref = "plagubot-plugins" }
plagubot-plugins-inline-buttons = { module = "dev.inmo:plagubot.plugins.inline.buttons", version.ref = "plagubot-plugins" }
microutils-repos-common = { module = "dev.inmo:micro_utils.repos.common", version.ref = "microutils" }
microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" }
microutils-repos-cache = { module = "dev.inmo:micro_utils.repos.cache", version.ref = "microutils" }
@@ -47,11 +39,9 @@ psql = { module = "org.postgresql:postgresql", version.ref = "psql" }
# buildscript classpaths
android-tools-build = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
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-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "kotlin" }
kotlin-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
[plugins]

View File

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

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,6 +14,7 @@ kotlin {
}
jvmMain {
dependencies {
api libs.plagubot.plugins.inline.queries
}
}
}

View File

@@ -1 +1 @@
package dev.inmo.plaguposter.inlines
package dev.inmo.plagubot.plugins.inline.queries

View File

@@ -1,81 +1,29 @@
package dev.inmo.plaguposter.inlines
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.w
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.inlines.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo
import dev.inmo.tgbotapi.bot.exceptions.RequestException
import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.types.inlineQueryAnswerResultsLimit
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
object Plugin : Plugin {
@Serializable
internal data class Config(
val preset: List<OfferTemplate>
)
private val actualPlugin = dev.inmo.plagubot.plugins.inline.queries.Plugin
object Plugin : Plugin by actualPlugin {
private val log = TagLogger("InlinePlugin")
override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["inlines"] ?: return@single Config(emptyList())) }
single { InlineTemplatesRepo(getOrNull<Config>() ?.preset ?.toMutableSet() ?: mutableSetOf()) }
single { actualPlugin }
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val templatesRepo = koin.get<InlineTemplatesRepo>()
onBaseInlineQuery { query ->
val page = query.offset.toIntOrNull() ?: 0
val queryString = query.query.trim()
try {
answerInlineQuery(
query,
templatesRepo.templates.paginate(
Pagination(
page,
inlineQueryAnswerResultsLimit.last + 1
)
).results.mapIndexedNotNull { index, offerTemplate ->
offerTemplate.createArticleResult(
index.toString(),
queryString
)
},
nextOffset = (page + 1).toString(),
cachedTime = 0
)
} catch (e: RequestException) {
bot.answerInlineQuery(
query,
cachedTime = 0
)
}
log.w {
"Built-in inline plugin has been deprecated. Use \"${actualPlugin::class.qualifiedName}\" instead"
}
onCommand("help", requireOnlyCommandInMessage = true) {
reply(
it,
"Push the button above to see available commands",
replyMarkup = flatInlineKeyboard {
inlineQueryInCurrentChatButton("Toggle commands", "")
}
)
}
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(
OfferTemplate(
"Trigger help button",
listOf(Format("/help"))
)
)
with(actualPlugin) {
setupBotPlugin(koin)
}
}
}

View File

@@ -1,40 +0,0 @@
package dev.inmo.plaguposter.inlines.models
import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent
import dev.inmo.tgbotapi.types.message.MarkdownV2
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
data class Format(
val template: String,
val regexTemplate: String = "^$",
val splitBy: String? = null,
val enableMarkdownSupport: Boolean = false
) {
@Transient
val queryRegex = Regex(regexTemplate, RegexOption.DOT_MATCHES_ALL)
init {
println(queryRegex)
}
fun formatByRegex(with: String): String? {
return if (queryRegex.matches(with)) {
template.format(*(splitBy ?.let { with.split(it).toTypedArray() } ?: arrayOf(with)))
} else {
null
}
}
fun createContent(with: String): InputTextMessageContent? {
return if (queryRegex.matches(with)) {
InputTextMessageContent(
template.format(*(splitBy ?.let { with.split(it).toTypedArray() } ?: arrayOf(with))),
if (enableMarkdownSupport) MarkdownV2 else null
)
} else {
null
}
}
}

View File

@@ -1,22 +0,0 @@
package dev.inmo.plaguposter.inlines.models
import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle
import kotlinx.serialization.Serializable
@Serializable
data class OfferTemplate(
val title: String,
val formats: List<Format> = emptyList(),
val description: String? = null
) {
fun createArticleResult(id: String, query: String): InlineQueryResultArticle? = formats.firstOrNull {
it.queryRegex.matches(query)
} ?.createContent(query) ?.let { content ->
InlineQueryResultArticle(
id,
title,
content,
description = description
)
}
}

View File

@@ -1,16 +0,0 @@
package dev.inmo.plaguposter.inlines.repos
import dev.inmo.plaguposter.inlines.models.OfferTemplate
class InlineTemplatesRepo(
private val _templates: MutableSet<OfferTemplate>
) {
internal val templates
get() = _templates.toList()
suspend fun addTemplate(offerTemplate: OfferTemplate): Boolean {
return _templates.add(offerTemplate)
}
suspend fun dropTemplate(offerTemplate: OfferTemplate): Boolean {
return _templates.remove(offerTemplate)
}
}

View File

@@ -1 +1 @@
<manifest package="dev.inmo.plaguposter.inlines"/>
<manifest package="dev.inmo.plagubot.plugins.inline.queries"/>

View File

@@ -1,34 +0,0 @@
project.version = "$version"
project.group = "$group"
// apply from: "$publishGradlePath"
kotlin {
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
dependencies {
implementation libs.kotlin
api libs.kotlin.serialization
}
}
commonTest {
dependencies {
implementation libs.kotlin.test.common
implementation libs.kotlin.test.annotations.common
}
}
androidTest {
dependencies {
implementation libs.kotlin.test.junit
implementation libs.android.test.junit
implementation libs.android.test.espresso.core
}
}
}
}
apply from: "$defaultAndroidSettingsPresetPath"

View File

@@ -1,13 +1,13 @@
project.version = "$version"
project.group = "$group"
// apply from: "$publishGradlePath"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
}
}
@@ -34,6 +34,6 @@ kotlin {
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -1,7 +1,7 @@
project.version = "$version"
project.group = "$group"
// apply from: "$publishGradlePath"
apply from: "$publishGradlePath"
kotlin {
js (IR) {

View File

@@ -1,24 +1,15 @@
project.version = "$version"
project.group = "$group"
// apply from: "$publishGradlePath"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
jvm()
js (IR) {
browser()
nodejs()
}
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
dependencies {
@@ -43,19 +34,7 @@ kotlin {
implementation libs.kotlin.test.junit
}
}
androidTest {
dependencies {
implementation libs.kotlin.test.junit
implementation libs.android.test.junit
implementation libs.android.test.espresso.core
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
apply from: "$defaultAndroidSettingsPresetPath"

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"

24
posts/gc/build.gradle Normal file
View File

@@ -0,0 +1,24 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.posts")
api libs.microutils.koin
api libs.krontab
}
}
jvmMain {
dependencies {
api libs.plagubot.plugins.inline.queries
}
}
}
}

View File

@@ -0,0 +1 @@
package dev.inmo.plaguposter.posts.gc

View File

@@ -0,0 +1,194 @@
package dev.inmo.plaguposter.posts.gc
import com.benasher44.uuid.uuid4
import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.toKronScheduler
import dev.inmo.krontab.utils.asFlowWithDelays
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.i
import dev.inmo.kslog.common.iS
import dev.inmo.micro_utils.coroutines.actor
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.NewPost
import dev.inmo.plaguposter.posts.models.PostContentInfo
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.forwardMessage
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitInlineMessageIdDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.oneOf
import dev.inmo.tgbotapi.extensions.behaviour_builder.parallel
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.MilliSeconds
import dev.inmo.tgbotapi.utils.bold
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
object Plugin : Plugin {
@Serializable
internal data class Config (
val krontab: KrontabTemplate? = null,
val throttlingMillis: MilliSeconds = 1000,
val doFullCheck: Boolean = false
)
override fun Module.setupDI(database: Database, params: JsonObject) {
params["messagesChecker"] ?.let { element ->
single { get<Json>().decodeFromJsonElement(Config.serializer(), element) }
}
}
private val gcLogger = KSLog("GarbageCollector")
private suspend fun BehaviourContext.doRecheck(
throttlingMillis: MilliSeconds,
doFullCheck: Boolean,
postsRepo: PostsRepo,
chatsConfig: ChatConfig
) {
val posts = postsRepo.getAll()
gcLogger.i {
"Start garbage collecting of posts. Initial posts count: ${posts.size}"
}
posts.forEach { (postId, post) ->
val surelyAbsentMessages = mutableListOf<PostContentInfo>()
for (content in post.content) {
try {
forwardMessage(
toChatId = chatsConfig.cacheChatId,
fromChatId = content.chatId,
messageId = content.messageId
)
if (!doFullCheck) {
break
}
} catch (e: Throwable) {
if (e.message ?.contains("message to forward not found") == true) {
surelyAbsentMessages.add(content)
}
}
delay(throttlingMillis)
}
val existsPostMessages = post.content.filter {
it !in surelyAbsentMessages
}
if (existsPostMessages.isNotEmpty() && surelyAbsentMessages.isNotEmpty()) {
runCatching {
postsRepo.update(
postId,
NewPost(
content = existsPostMessages
)
)
}
}
if (existsPostMessages.isNotEmpty()) {
return@forEach
}
runCatching {
send(
chatsConfig.cacheChatId,
"Can't find any messages for post $postId. So, deleting it"
)
}
runCatching {
postsRepo.deleteById(postId)
}
}
gcLogger.iS {
"Complete garbage collecting of posts. Result posts count: ${postsRepo.count()}"
}
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val postsRepo = koin.get<PostsRepo>()
val chatsConfig = koin.get<ChatConfig>()
val config = koin.getOrNull<Config>() ?: Config()
val scope = koin.get<CoroutineScope>()
val recheckActor = scope.actor<Unit>(0) {
runCatching {
doRecheck(
config.throttlingMillis,
config.doFullCheck,
postsRepo,
chatsConfig
)
}
}
config.krontab ?.toKronScheduler() ?.asFlowWithDelays() ?.subscribeSafelyWithoutExceptions(koin.get()) {
recheckActor.trySend(Unit)
}
onCommand("force_garbage_collection") { message ->
launch {
val prefix = uuid4().toString()
val yesData = "${prefix}yes"
val noData = "${prefix}no"
edit(
message,
text = "Are you sure want to trigger posts garbage collecting?",
replyMarkup = flatInlineKeyboard {
dataButton("Sure", yesData)
dataButton("No", noData)
}
)
val answer = oneOf(
parallel {
waitMessageDataCallbackQuery().filter {
it.message.sameMessage(message)
}.first()
},
parallel {
waitInlineMessageIdDataCallbackQuery().filter {
it.data == yesData || it.data == noData
}.first()
}
)
if (answer.data == yesData) {
if (recheckActor.trySend(Unit).isSuccess) {
edit(message, "Checking of posts without exists messages triggered")
} else {
edit(message) {
+"Checking of posts without exists messages has been triggered " + bold("earlier")
}
}
} else {
delete(message)
}
}
}
koin.getOrNull<InlineTemplatesRepo>() ?.addTemplate(
OfferTemplate(
"Force posts check",
listOf(
Format("/force_garbage_collection")
),
"Force check posts without exists messages"
)
)
}
}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,5 +14,10 @@ kotlin {
api libs.microutils.koin
}
}
jvmMain {
dependencies {
api libs.plagubot.plugins.inline.queries
}
}
}
}

View File

@@ -3,6 +3,21 @@ package dev.inmo.plaguposter.posts.panel
import dev.inmo.plaguposter.posts.models.RegisteredPost
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton
fun interface PanelButtonBuilder {
interface PanelButtonBuilder {
val weight: Int
suspend fun buildButton(post: RegisteredPost): InlineKeyboardButton?
class Default(override val weight: Int = 0, private val block: suspend (RegisteredPost) -> InlineKeyboardButton?) : PanelButtonBuilder {
override suspend fun buildButton(post: RegisteredPost): InlineKeyboardButton? = block(post)
}
companion object {
operator fun invoke(block: suspend (RegisteredPost) -> InlineKeyboardButton?) = Default(
block = block
)
operator fun invoke(weight: Int, block: suspend (RegisteredPost) -> InlineKeyboardButton?) = Default(
weight,
block
)
}
}

View File

@@ -5,14 +5,14 @@ import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineK
import kotlinx.coroutines.flow.MutableSharedFlow
class PanelButtonsAPI(
private val preset: List<PanelButtonBuilder>,
private val preset: Map<Int, List<PanelButtonBuilder>>,
private val rootPanelButtonText: String
) {
private val _buttons = mutableSetOf<PanelButtonBuilder>().also {
it.addAll(preset)
private val _buttonsMap = mutableMapOf<Int, MutableList<PanelButtonBuilder>>().also {
it.putAll(preset.map { it.key to it.value.toMutableList() })
}
internal val buttonsBuilders: List<PanelButtonBuilder>
get() = _buttons.toList()
get() = _buttonsMap.toList().sortedBy { it.first }.flatMap { it.second }
internal val forceRefreshFlow = MutableSharedFlow<PostId>()
val RootPanelButtonBuilder = PanelButtonBuilder {
@@ -22,8 +22,13 @@ class PanelButtonsAPI(
)
}
fun add(button: PanelButtonBuilder) = _buttons.add(button)
fun remove(button: PanelButtonBuilder) = _buttons.remove(button)
fun add(button: PanelButtonBuilder, weight: Int = button.weight) = _buttonsMap.getOrPut(weight) { mutableListOf() }.add(button)
fun remove(button: PanelButtonBuilder) = _buttonsMap.mapNotNull { (k, v) ->
v.remove(button)
k.takeIf { v.isEmpty() }
}.forEach {
_buttonsMap.remove(it)
}
suspend fun forceRefresh(postId: PostId) {
forceRefreshFlow.emit(postId)
}

View File

@@ -4,10 +4,15 @@ import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.getAllDistinct
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.micro_utils.repos.set
import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.cached
import dev.inmo.micro_utils.repos.cache.full.cached
import dev.inmo.micro_utils.repos.cache.full.fullyCached
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.common.useCache
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.repos.PostsMessages
import dev.inmo.plaguposter.posts.repo.PostsRepo
@@ -15,18 +20,24 @@ import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.edit.text.editMessageText
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.command
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.ReplyParameters
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.ParseMode
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.italic
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
@@ -69,9 +80,13 @@ object Plugin : Plugin {
}
)
PanelButtonsAPI(
getAllDistinct<PanelButtonBuilder>() + builtInButtons,
emptyMap(),
config.rootButtonText
)
).apply {
(getAllDistinct<PanelButtonBuilder>() + builtInButtons).forEach {
add(it)
}
}
}
}
@@ -80,7 +95,12 @@ object Plugin : Plugin {
val chatsConfig = koin.get<ChatConfig>()
val config = koin.getOrNull<Config>() ?: Config()
val api = koin.get<PanelButtonsAPI>()
val postsMessages = PostsMessages(koin.get(), koin.get())
val basePostsMessages = PostsMessages(koin.get(), koin.get())
val postsMessages = if (koin.useCache) {
basePostsMessages.fullyCached(MapKeyValueRepo(), koin.get())
} else {
basePostsMessages
}
postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) {
val firstContent = it.content.first()
@@ -93,7 +113,7 @@ object Plugin : Plugin {
firstContent.chatId,
text = config.text,
parseMode = config.parseMode,
replyToMessageId = firstContent.messageId,
replyParameters = ReplyParameters(firstContent.chatId, firstContent.messageId),
replyMarkup = InlineKeyboardMarkup(buttons),
disableNotification = true
).also { sentMessage ->
@@ -108,7 +128,7 @@ object Plugin : Plugin {
suspend fun refreshPostMessage(
postId: PostId,
chatId: ChatId,
chatId: IdChatIdentifier,
messageId: MessageIdentifier
) {
val post = postsRepo.getById(postId) ?: return
@@ -127,7 +147,7 @@ object Plugin : Plugin {
onMessageDataCallbackQuery (
initialFilter = {
it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id == chatsConfig.sourceChatId
it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id in chatsConfig.allSourceChatIds
}
) {
val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId)
@@ -136,7 +156,7 @@ object Plugin : Plugin {
}
onMessageDataCallbackQuery(
initialFilter = {
it.data.startsWith("delete ") && it.message.chat.id == chatsConfig.sourceChatId
it.data.startsWith("delete ") && it.message.chat.id in chatsConfig.allSourceChatIds
}
) { query ->
val postId = query.data.removePrefix("delete ").let(::PostId)
@@ -163,7 +183,7 @@ object Plugin : Plugin {
}
onMessageDataCallbackQuery(
initialFilter = {
it.data.startsWith("refresh ") && it.message.chat.id == chatsConfig.sourceChatId
it.data.startsWith("refresh ") && it.message.chat.id in chatsConfig.allSourceChatIds
}
) { query ->
val postId = query.data.removePrefix("refresh ").let(::PostId)
@@ -183,5 +203,59 @@ object Plugin : Plugin {
val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions
refreshPostMessage(it, chatId, messageId)
}
command("panel") {
val reply = it.replyTo
if (reply == null) {
runCatchingSafely {
edit(
it,
it.content.textSources + buildEntities {
+"${UnsuccessfulSymbol}\n" + bold("Result") + ": " + italic("You should reply post content to trigger panel retrieving")
}
)
}.onFailure { _ ->
reply(
it,
buildEntities {
bold("Result") + ": " + italic("You should reply post content to trigger panel retrieving")
}
)
}
return@command
}
val postId = postsRepo.getIdByChatAndMessage(reply.chat.id, reply.messageId)
if (postId == null) {
runCatchingSafely {
edit(
it,
it.content.textSources + buildEntities {
+"${UnsuccessfulSymbol}\n" + bold("Result") + ": " + italic("Unable to find post related to replied message")
}
)
}.onFailure { _ ->
reply(
it,
buildEntities {
bold("Result") + ": " + italic("Unable to find post related to replied message")
}
)
}
return@command
}
postsMessages.get(postId) ?.let {
runCatchingSafely { delete(it.id, it.value) }
postsMessages.unset(postId)
}
refreshPostMessage(postId, it.chat.id, it.messageId)
postsMessages.set(postId, it.chat.id to it.messageId)
}
}
}

View File

@@ -5,18 +5,20 @@ import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.Database
private val ChatIdToMessageSerializer = PairSerializer(ChatId.serializer(), MessageIdentifier.serializer())
private val ChatIdToMessageSerializer = PairSerializer(FullChatIdentifierSerializer, MessageIdentifier.serializer())
fun PostsMessages(
database: Database,
json: Json
): KeyValueRepo<PostId, Pair<ChatId, MessageIdentifier>> = ExposedKeyValueRepo<String, String>(
): KeyValueRepo<PostId, Pair<IdChatIdentifier, MessageIdentifier>> = ExposedKeyValueRepo<String, String>(
database,
{ text("postId") },
{ text("chatToMessage") },
@@ -25,5 +27,5 @@ fun PostsMessages(
{ string },
{ json.encodeToString(ChatIdToMessageSerializer, this) },
{ PostId(this) },
{ json.decodeFromString(ChatIdToMessageSerializer, this) }
{ json.decodeFromString(ChatIdToMessageSerializer, this).let { (it.first as IdChatIdentifier) to it.second } }
)

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.posts.models
import com.soywiz.klock.DateTime
import korlibs.time.DateTime
import dev.inmo.plaguposter.common.DateTimeSerializer
import dev.inmo.tgbotapi.types.ChatId
import kotlinx.serialization.Serializable

View File

@@ -1,25 +1,39 @@
package dev.inmo.plaguposter.posts.models
import dev.inmo.tgbotapi.extensions.utils.mediaGroupMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.possiblyMediaGroupMessageOrNull
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MessageContent
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import kotlinx.serialization.Serializable
@Serializable
data class PostContentInfo(
val chatId: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier,
val messageId: MessageIdentifier,
val group: String?,
val order: Int
) {
companion object {
fun fromMessage(message: ContentMessage<*>, order: Int) = PostContentInfo(
private fun fromMessage(message: ContentMessage<*>, order: Int) = PostContentInfo(
message.chat.id,
message.messageId,
message.mediaGroupMessageOrNull() ?.mediaGroupId,
message.possiblyMediaGroupMessageOrNull() ?.mediaGroupId,
order
)
fun fromMessage(message: ContentMessage<*>): List<PostContentInfo> {
val content = message.content
return if (content is MediaGroupContent<*>) {
content.group.mapIndexed { i, it ->
fromMessage(it.sourceMessage, i)
}
} else {
listOf(fromMessage(message, 0))
}
}
}
}

View File

@@ -7,4 +7,6 @@ import kotlin.jvm.JvmInline
@JvmInline
value class PostId(
val string: String
)
) {
override fun toString(): String = string
}

View File

@@ -1,12 +1,14 @@
package dev.inmo.plaguposter.posts.repo
import com.soywiz.klock.DateTime
import korlibs.time.DateTime
import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.plaguposter.posts.models.*
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
interface ReadPostsRepo : ReadCRUDRepo<RegisteredPost, PostId> {
suspend fun getIdByChatAndMessage(chatId: ChatId, messageId: MessageIdentifier): PostId?
suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId?
suspend fun getPostCreationTime(postId: PostId): DateTime?
suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo?
}

View File

@@ -11,19 +11,19 @@ import dev.inmo.tgbotapi.extensions.api.send.copyMessage
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class PostPublisher(
private val bot: TelegramBot,
private val postsRepo: PostsRepo,
private val cachingChatId: ChatId,
private val targetChatId: ChatId,
private val cachingChatId: IdChatIdentifier,
private val targetChatIds: List<IdChatIdentifier>,
private val deleteAfterPosting: Boolean = true
) {
suspend fun publish(postId: PostId) {
suspend fun publish(postId: PostId): Boolean {
val messagesInfo = postsRepo.getById(postId) ?: let {
logger.w { "Unable to get post with id $postId for publishing" }
return
return false
}
val sortedMessagesContents = messagesInfo.content.groupBy { it.group }.flatMap { (group, list) ->
if (group == null) {
@@ -34,27 +34,54 @@ class PostPublisher(
listOf(list.first().order to list)
}
}.sortedBy { it.first }
var haveSentMessages = false
sortedMessagesContents.forEach { (_, contents) ->
contents.singleOrNull() ?.also {
bot.copyMessage(targetChatId, it.chatId, it.messageId)
targetChatIds.forEach { targetChatId ->
runCatching {
bot.copyMessage(targetChatId, it.chatId, it.messageId)
}.onFailure { _ ->
runCatching {
bot.forwardMessage(
fromChatId = it.chatId,
toChatId = cachingChatId,
messageId = it.messageId
)
}.onSuccess {
bot.copyMessage(targetChatId, it)
haveSentMessages = true
}
}.onSuccess {
haveSentMessages = true
}
}
return@forEach
}
val resultContents = contents.mapNotNull {
it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null)
}.sortedBy { it.first }.mapNotNull { (_, it) ->
it.withContentOrNull<MediaGroupContent>() ?: null.also { _ ->
bot.copyMessage(targetChatId, it)
}.sortedBy { it.first }.mapNotNull { (_, forwardedMessage) ->
forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ ->
targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, forwardedMessage)
haveSentMessages = true
}
}
}
resultContents.singleOrNull() ?.also {
bot.copyMessage(targetChatId, it)
targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, it)
haveSentMessages = true
}
return@forEach
} ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach {
bot.send(
targetChatId,
it.map { it.content.toMediaGroupMemberTelegramMedia() }
)
targetChatIds.forEach { targetChatId ->
bot.send(
targetChatId,
it.map { it.content.toMediaGroupMemberTelegramMedia() }
)
haveSentMessages = true
}
}
}
@@ -62,5 +89,6 @@ class PostPublisher(
postsRepo.deleteById(postId)
}
return haveSentMessages
}
}

View File

@@ -4,15 +4,18 @@ import dev.inmo.kslog.common.logger
import dev.inmo.kslog.common.w
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithBinds
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.inlines.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.common.useCache
import dev.inmo.plaguposter.posts.cached.CachedPostsRepo
import dev.inmo.plaguposter.posts.repo.*
import dev.inmo.plaguposter.posts.sending.PostPublisher
import dev.inmo.tgbotapi.extensions.api.delete
@@ -26,7 +29,6 @@ import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
import org.koin.dsl.binds
object Plugin : Plugin {
@Serializable
@@ -44,14 +46,19 @@ object Plugin : Plugin {
}
single { get<Json>().decodeFromJsonElement(Config.serializer(), configJson) }
single { get<Config>().chats }
single { ExposedPostsRepo(database) } binds arrayOf(
PostsRepo::class,
ReadPostsRepo::class,
WritePostsRepo::class,
)
single { ExposedPostsRepo(database) }
singleWithBinds<PostsRepo> {
val base = get<ExposedPostsRepo>()
if (useCache) {
CachedPostsRepo(base, get())
} else {
base
}
}
single {
val config = get<Config>()
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.targetChatId, config.deleteAfterPublishing)
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds.toList(), config.deleteAfterPublishing)
}
}

View File

@@ -0,0 +1,54 @@
package dev.inmo.plaguposter.posts.cached
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import korlibs.time.DateTime
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.repos.CRUDRepo
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.FullCRUDCacheRepo
import dev.inmo.plaguposter.posts.models.NewPost
import dev.inmo.plaguposter.posts.models.PostContentInfo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.models.RegisteredPost
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
class CachedPostsRepo(
private val parentRepo: PostsRepo,
private val scope: CoroutineScope,
private val kvCache: KeyValueRepo<PostId, RegisteredPost> = MapKeyValueRepo()
) : PostsRepo, CRUDRepo<RegisteredPost, PostId, NewPost> by FullCRUDCacheRepo(
parentRepo,
kvCache,
scope,
skipStartInvalidate = false,
locker = SmartRWLocker(),
{ it.id }
) {
override val removedPostsFlow: Flow<RegisteredPost> by parentRepo::removedPostsFlow
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId? {
doForAllWithNextPaging(firstPageWithOneElementPagination) {
kvCache.values(it).also {
it.results.forEach {
return it.takeIf {
it.content.any { it.chatId == chatId && it.messageId == messageId }
} ?.id ?: return@forEach
}
}
}
return null
}
override suspend fun getPostCreationTime(postId: PostId): DateTime? = getById(postId) ?.created
override suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo? = getById(postId) ?.content ?.firstOrNull()
}

View File

@@ -5,6 +5,7 @@ import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.exposed.*
import dev.inmo.plaguposter.posts.models.*
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import org.jetbrains.exposed.sql.*
internal class ExposedContentInfoRepo(
@@ -13,13 +14,14 @@ internal class ExposedContentInfoRepo(
) : ExposedRepo, Table(name = "posts_content") {
val postIdColumn = text("post_id").references(postIdColumnReference, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
val chatIdColumn = long("chat_id")
val threadIdColumn = long("thread_id").nullable().default(null)
val messageIdColumn = long("message_id")
val groupColumn = text("group").nullable()
val orderColumn = integer("order")
val ResultRow.asObject
get() = PostContentInfo(
ChatId(get(chatIdColumn)),
IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)),
get(messageIdColumn),
get(groupColumn),
get(orderColumn)

View File

@@ -1,19 +1,22 @@
package dev.inmo.plaguposter.posts.exposed
import com.benasher44.uuid.uuid4
import com.soywiz.klock.DateTime
import korlibs.time.DateTime
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction
class ExposedPostsRepo(
@@ -21,8 +24,9 @@ class ExposedPostsRepo(
) : PostsRepo, AbstractExposedCRUDRepo<RegisteredPost, PostId, NewPost>(
tableName = "posts"
) {
val idColumn = text("id").clientDefault { uuid4().toString() }
val idColumn = text("id")
val createdColumn = double("datetime").default(0.0)
val latestUpdateColumn = double("latest_update").default(0.0)
private val contentRepo by lazy {
ExposedContentInfoRepo(
@@ -33,11 +37,13 @@ class ExposedPostsRepo(
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
override val selectById: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) }
override val selectByIds: SqlExpressionBuilder.(List<PostId>) -> Op<Boolean> = { idColumn.inList(it.map { it.string }) }
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) }
override val selectByIds: ISqlExpressionBuilder.(List<PostId>) -> Op<Boolean> = { idColumn.inList(it.map { it.string }) }
override val ResultRow.asId: PostId
get() = PostId(get(idColumn))
override val ResultRow.asObject: RegisteredPost
get() {
val id = PostId(get(idColumn))
val id = asId
return RegisteredPost(
id,
DateTime(get(createdColumn)),
@@ -59,18 +65,6 @@ class ExposedPostsRepo(
override fun InsertStatement<Number>.asObject(value: NewPost): RegisteredPost {
val id = PostId(get(idColumn))
with(contentRepo) {
value.content.forEach { contentInfo ->
insert {
it[postIdColumn] = id.string
it[chatIdColumn] = contentInfo.chatId.chatId
it[messageIdColumn] = contentInfo.messageId
it[groupColumn] = contentInfo.group
it[orderColumn] = contentInfo.order
}
}
}
return RegisteredPost(
id,
DateTime(get(createdColumn)),
@@ -82,25 +76,55 @@ class ExposedPostsRepo(
)
}
override fun update(id: PostId, value: NewPost, it: UpdateStatement) {
with(contentRepo) {
deleteWhere { postIdColumn.eq(id.string) }
value.content.forEach { contentInfo ->
insert {
it[postIdColumn] = id.string
it[chatIdColumn] = contentInfo.chatId.chatId
it[messageIdColumn] = contentInfo.messageId
it[groupColumn] = contentInfo.group
it[orderColumn] = contentInfo.order
override fun createAndInsertId(value: NewPost, it: InsertStatement<Number>): PostId {
val id = PostId(uuid4().toString())
it[idColumn] = id.string
return id
}
override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {
it[latestUpdateColumn] = DateTime.now().unixMillis
}
private fun updateContent(post: RegisteredPost) {
transaction(database) {
with(contentRepo) {
deleteWhere { postIdColumn.eq(post.id.string) }
post.content.forEach { contentInfo ->
insert {
it[postIdColumn] = post.id.string
it[chatIdColumn] = contentInfo.chatId.chatId
it[threadIdColumn] = contentInfo.chatId.threadId
it[messageIdColumn] = contentInfo.messageId
it[groupColumn] = contentInfo.group
it[orderColumn] = contentInfo.order
}
}
}
}
}
override fun insert(value: NewPost, it: InsertStatement<Number>) {
super.insert(value, it)
it[createdColumn] = DateTime.now().unixMillis
}
override suspend fun onAfterCreate(values: List<Pair<NewPost, RegisteredPost>>): List<RegisteredPost> {
return values.map {
val actual = it.second.copy(content = it.first.content)
updateContent(actual)
actual
}
}
override suspend fun onAfterUpdate(value: List<UpdatedValuePair<NewPost, RegisteredPost>>): List<RegisteredPost> {
return value.map {
val actual = it.second.copy(content = it.first.content)
updateContent(actual)
actual
}
}
override suspend fun deleteById(ids: List<PostId>) {
onBeforeDelete(ids)
val posts = ids.mapNotNull {
@@ -109,7 +133,7 @@ class ExposedPostsRepo(
val existsIds = posts.keys.toList()
transaction(db = database) {
val deleted = deleteWhere(null, null) {
selectByIds(existsIds)
selectByIds(it, existsIds)
}
with(contentRepo) {
deleteWhere {
@@ -129,10 +153,14 @@ class ExposedPostsRepo(
}
}
override suspend fun getIdByChatAndMessage(chatId: ChatId, messageId: MessageIdentifier): PostId? {
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId? {
return transaction(database) {
with(contentRepo) {
select { chatIdColumn.eq(chatId.chatId).and(messageIdColumn.eq(messageId)) }.limit(1).firstOrNull() ?.get(postIdColumn)
select {
chatIdColumn.eq(chatId.chatId)
.and(chatId.threadId ?.let { threadIdColumn.eq(it) } ?: threadIdColumn.isNull())
.and(messageIdColumn.eq(messageId))
}.limit(1).firstOrNull() ?.get(postIdColumn)
} ?.let(::PostId)
}
}
@@ -140,4 +168,10 @@ class ExposedPostsRepo(
override suspend fun getPostCreationTime(postId: PostId): DateTime? = transaction(database) {
select { selectById(postId) }.limit(1).firstOrNull() ?.get(createdColumn) ?.let(::DateTime)
}
override suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo? = transaction(database) {
with(contentRepo) {
select { postIdColumn.eq(postId.string) }.limit(1).firstOrNull() ?.asObject
}
}
}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -13,9 +12,5 @@ kotlin {
api project(":plaguposter.posts")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -3,20 +3,24 @@ package dev.inmo.plaguposter.posts.registrar.state
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.plaguposter.posts.models.PostContentInfo
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.serialization.Serializable
interface RegistrationState : State {
override val context: ChatId
override val context: IdChatIdentifier
@Serializable
data class InProcess(
override val context: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
override val context: IdChatIdentifier,
val messages: List<PostContentInfo>
) : RegistrationState
@Serializable
data class Finish(
override val context: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
override val context: IdChatIdentifier,
val messages: List<PostContentInfo>
) : RegistrationState
}

View File

@@ -5,9 +5,9 @@ import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.repos.create
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.*
import dev.inmo.plaguposter.inlines.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.registrar.state.RegistrationState
import dev.inmo.plaguposter.posts.repo.PostsRepo
@@ -20,88 +20,93 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities
import dev.inmo.tgbotapi.extensions.utils.formatting.regular
import dev.inmo.tgbotapi.extensions.utils.mediaGroupMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MessageContent
import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.*
import kotlinx.serialization.Serializable
import org.koin.core.Koin
@Serializable
object Plugin : Plugin {
@Serializable
data class Config(
val useInlineFinishingOpportunity: Boolean = true
)
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
val config = koin.get<ChatConfig>()
val postsRepo = koin.get<PostsRepo>()
strictlyOn {state: RegistrationState.InProcess ->
strictlyOn { state: RegistrationState.InProcess ->
val buttonUuid = "finish"
val messageToDelete = send(
state.context,
buildEntities {
if (state.messages.isNotEmpty()) {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
val suggestionMessageDeferred = async {
send(
state.context,
dev.inmo.tgbotapi.utils.buildEntities {
if (state.messages.isNotEmpty()) {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
} else {
regular("Ok, send me your messages for new post")
}
},
replyMarkup = if (state.messages.isNotEmpty()) {
flatInlineKeyboard {
dataButton(
"Finish",
buttonUuid
)
}
} else {
regular("Ok, send me your messages for new post")
null
}
},
replyMarkup = if (state.messages.isNotEmpty()) {
flatInlineKeyboard {
dataButton(
"Finish",
buttonUuid
)
}
} else {
null
}
)
)
}
val newMessagesInfo = firstOf {
firstOf {
add {
listOf(
waitContentMessage(
includeMediaGroups = false
).filter {
it.chat.id == state.context
}.take(1).first()
)
}
add {
waitMediaGroupMessages().filter {
it.first().chat.id == state.context
}.take(1).first()
val receivedMessage = waitAnyContentMessage().filter {
it.sameChat(state.context)
}.first()
when {
receivedMessage.content.textContentOrNull() ?.text == "/finish_post" -> {
val messageToDelete = suggestionMessageDeferred.await()
edit(messageToDelete, "Ok, finishing your request")
RegistrationState.Finish(
state.context,
state.messages
)
}
else -> {
RegistrationState.InProcess(
state.context,
state.messages + PostContentInfo.fromMessage(receivedMessage)
).also {
runCatchingSafely {
suggestionMessageDeferred.cancel()
}
runCatchingSafely {
delete(suggestionMessageDeferred.await())
}
}
}
}
}
add {
val messageToDelete = suggestionMessageDeferred.await()
val finishPressed = waitMessageDataCallbackQuery().filter {
it.message.sameMessage(messageToDelete) && it.data == buttonUuid
}.first()
emptyList<ContentMessage<MessageContent>>()
}
add {
val finishPressed = waitCommandMessage("finish_post").filter {
it.sameChat(messageToDelete)
}.first()
emptyList<ContentMessage<MessageContent>>()
}
}.ifEmpty {
edit(messageToDelete, "Ok, finishing your request")
return@strictlyOn RegistrationState.Finish(
state.context,
state.messages
)
}.map {
PostContentInfo.fromMessage(it, state.messages.size)
}
RegistrationState.InProcess(
state.context,
state.messages + newMessagesInfo
).also {
delete(messageToDelete)
edit(messageToDelete, "Ok, finishing your request")
RegistrationState.Finish(
state.context,
state.messages
)
}
}
}
@@ -114,30 +119,14 @@ object Plugin : Plugin {
null
}
onCommand("start_post", initialFilter = { it.chat.id == config.sourceChatId }) {
onCommand("start_post", initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
startChain(RegistrationState.InProcess(it.chat.id, emptyList()))
}
onContentMessage(
initialFilter = { it.chat.id == config.sourceChatId && it.mediaGroupMessageOrNull() ?.mediaGroupId == null && !FirstSourceIsCommandsFilter(it) }
initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } && !FirstSourceIsCommandsFilter(it) }
) {
startChain(RegistrationState.Finish(it.chat.id, listOf(PostContentInfo.fromMessage(it, 0))))
}
onMediaGroup(
initialFilter = { it.first().chat.id == config.sourceChatId }
) {
startChain(
RegistrationState.Finish(
it.first().chat.id,
it.map {
PostContentInfo.fromMessage(
it,
0
)
}
)
)
startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it)))
}
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(

105
publish.gradle Normal file
View File

@@ -0,0 +1,105 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
archiveClassifier = 'javadoc'
}
publishing {
publications.all {
artifact javadocsJar
pom {
description = "${project.name}"
name = "${project.name}"
url = "https://github.com/InsanusMokrassar/PlaguPoster"
scm {
developerConnection = "scm:git:[fetch=]https://github.com/InsanusMokrassar/PlaguPoster.git[push=]https://github.com/InsanusMokrassar/PlaguPoster.git"
url = "https://github.com/InsanusMokrassar/PlaguPoster.git"
}
developers {
developer {
id = "InsanusMokrassar"
name = "Aleksei Ovsiannikov"
email = "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
}
}
repositories {
if (project.hasProperty('GITEA_TOKEN') || System.getenv('GITEA_TOKEN') != null) {
maven {
name = "Gitea"
url = uri("https://git.inmo.dev/api/packages/InsanusMokrassar/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = project.hasProperty('GITEA_TOKEN') ? project.property('GITEA_TOKEN') : System.getenv('GITEA_TOKEN')
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
}
}
}
}
}
if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing'
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
}

1
publish.kpsb Normal file
View File

@@ -0,0 +1 @@
{"licenses":[],"mavenConfig":{"name":"${project.name}","description":"${project.name}","url":"https://github.com/InsanusMokrassar/PlaguPoster","vcsUrl":"https://github.com/InsanusMokrassar/PlaguPoster.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"Gitea","url":"https://git.inmo.dev/api/packages/InsanusMokrassar/maven","credsType":{"type":"dev.inmo.kmppscriptbuilder.core.models.MavenPublishingRepository.CredentialsType.HttpHeaderCredentials","headerName":"Authorization","headerValueProperty":"GITEA_TOKEN"}},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -14,9 +13,5 @@ kotlin {
api project(":plaguposter.posts")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,9 +14,5 @@ kotlin {
api libs.krontab
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -1,18 +1,19 @@
package dev.inmo.plaguposter.ratings.gc
import com.soywiz.klock.milliseconds
import com.soywiz.klock.seconds
import korlibs.time.DateTime
import korlibs.time.seconds
import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.toSchedule
import dev.inmo.krontab.utils.asFlow
import dev.inmo.krontab.utils.asFlowWithDelays
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.*
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.types.MilliSeconds
import dev.inmo.tgbotapi.types.Seconds
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
@@ -43,21 +44,35 @@ object Plugin : Plugin {
val config = koin.get<Config>()
config.immediateDrop ?.let { toDrop ->
ratingsRepo.onNewValue.subscribeSafelyWithoutExceptions(this) {
if (it.value <= toDrop) {
postsRepo.deleteById(it.id)
suspend fun checkAndOptionallyDrop(postId: PostId, rating: Rating) {
if (rating <= toDrop) {
postsRepo.deleteById(postId)
}
}
ratingsRepo.getAll().forEach {
runCatchingSafely {
checkAndOptionallyDrop(it.key, it.value)
}
}
ratingsRepo.onNewValue.subscribeSafelyWithoutExceptions(this) {
checkAndOptionallyDrop(it.first, it.second)
}
}
config.autoclear ?.let { autoclear ->
autoclear.autoClearKrontab.toSchedule().asFlow().subscribeSafelyWithoutExceptions(scope) {
val dropCreatedBefore = it - (autoclear.skipPostAge ?: 0).seconds
suspend fun doAutoClear() {
val dropCreatedBefore = DateTime.now() - (autoclear.skipPostAge ?: 0).seconds
ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.forEach {
if ((postsRepo.getPostCreationTime(it) ?: return@forEach) < dropCreatedBefore) {
postsRepo.deleteById(it)
}
}
}
runCatchingSafely {
doAutoClear()
}
autoclear.autoClearKrontab.toSchedule().asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) {
doAutoClear()
}
}
}
}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -14,9 +13,5 @@ kotlin {
api project(":plaguposter.ratings")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -1,6 +1,7 @@
package dev.inmo.plaguposter.ratings.selector
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.repos.KeyValueRepo
import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
@@ -9,13 +10,14 @@ import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
class DefaultSelector (
private val config: SelectorConfig,
private val ratingsRepo: RatingsRepo,
private val postsRepo: PostsRepo
private val postsRepo: PostsRepo,
private val latestChosenRepo: KeyValueRepo<PostId, DateTime>
) : Selector {
override suspend fun take(n: Int, now: DateTime): List<PostId> {
override suspend fun take(n: Int, now: DateTime, exclude: List<PostId>): List<PostId> {
val result = mutableListOf<PostId>()
do {
val selected = config.active(now.time) ?.rating ?.select(ratingsRepo, postsRepo, result, now) ?: break
val selected = config.active(now.time) ?.rating ?.select(ratingsRepo, postsRepo, result + exclude, now, latestChosenRepo) ?: break
result.add(selected)
} while (result.size < n)

View File

@@ -1,8 +1,13 @@
package dev.inmo.plaguposter.ratings.selector
import com.soywiz.klock.DateTime
import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId
interface Selector {
suspend fun take(n: Int = 1, now: DateTime = DateTime.now()): List<PostId>
suspend fun take(n: Int = 1, now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): List<PostId>
suspend fun takeOneOrNull(now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): PostId? = take(
n = 1,
now = now,
exclude = exclude
).firstOrNull()
}

View File

@@ -1,12 +1,10 @@
package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.DateTime
import com.soywiz.klock.seconds
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import korlibs.time.DateTime
import korlibs.time.seconds
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.plaguposter.common.DateTimeSerializer
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating
@@ -25,17 +23,23 @@ data class RatingConfig(
val max: Rating? = null,
val prefer: Prefer = Prefer.Random,
val otherwise: RatingConfig? = null,
val postAge: Seconds? = null
val postAge: Seconds? = null,
val uniqueCount: Int? = null
) {
suspend fun select(
ratingsRepo: RatingsRepo,
postsRepo: PostsRepo,
exclude: List<PostId>,
now: DateTime
now: DateTime,
latestChosenRepo: KeyValueRepo<PostId, DateTime>
): PostId? {
var reversed: Boolean = false
var count: Int? = null
val allowedCreationTime = now - (postAge ?: 0).seconds
val excludedByRepo = uniqueCount ?.let {
latestChosenRepo.getAll().toList().sortedBy { it.second }.takeLast(uniqueCount).map { it.first }
} ?: emptyList()
val resultExcluded = exclude + excludedByRepo
when (prefer) {
Prefer.Max -> {
@@ -59,40 +63,53 @@ data class RatingConfig(
ratingsRepo.getAllByWithNextPaging { keys(it) }
}
else -> {
ratingsRepo.getPostsWithRatingLessEq(max, exclude = exclude).keys
ratingsRepo.getPostsWithRatingLessEq(max, exclude = resultExcluded).keys
}
}
}
else -> {
when (max) {
null -> {
ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = exclude).keys
ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = resultExcluded).keys
}
else -> {
ratingsRepo.getPosts(min .. max, reversed, count, exclude = exclude).keys
ratingsRepo.getPosts(min .. max, reversed, count, exclude = resultExcluded).keys
}
}
}
}.filter {
it !in exclude && (postsRepo.getPostCreationTime(it) ?.let { it < allowedCreationTime } ?: true)
it !in resultExcluded && (postsRepo.getPostCreationTime(it) ?.let { it < allowedCreationTime } ?: true)
}
return when (prefer) {
val resultPosts: PostId = when (prefer) {
Prefer.Max,
Prefer.Min -> posts.firstOrNull()
Prefer.Random -> posts.randomOrNull()
} ?: otherwise ?.select(ratingsRepo, postsRepo, exclude, now)
} ?: otherwise ?.select(ratingsRepo, postsRepo, resultExcluded, now, latestChosenRepo) ?: return null
val postsToKeep = uniqueCount ?.let {
(excludedByRepo + resultPosts).takeLast(it)
} ?: return resultPosts
val postsToRemoveFromKeep = excludedByRepo.filter { it !in postsToKeep }
latestChosenRepo.unset(postsToRemoveFromKeep)
val postsToAdd = postsToKeep.filter { it !in excludedByRepo }
latestChosenRepo.set(
postsToAdd.associateWith { DateTime.now() }
)
return resultPosts
}
@Serializable(Prefer.Serializer::class)
sealed interface Prefer {
val type: String
@Serializable(Serializer::class)
object Max : Prefer { override val type: String = "max" }
data object Max : Prefer { override val type: String = "max" }
@Serializable(Serializer::class)
object Min : Prefer { override val type: String = "min" }
data object Min : Prefer { override val type: String = "min" }
@Serializable(Serializer::class)
object Random : Prefer { override val type: String = "random" }
data object Random : Prefer { override val type: String = "random" }
object Serializer : KSerializer<Prefer> {
override val descriptor: SerialDescriptor = String.serializer().descriptor

View File

@@ -1,7 +1,7 @@
package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.DateTime
import com.soywiz.klock.Time
import korlibs.time.DateTime
import korlibs.time.Time
import kotlinx.serialization.Serializable
@Serializable

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.*
import korlibs.time.*
import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor

View File

@@ -1,14 +1,33 @@
package dev.inmo.plaguposter.ratings.selector
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
import korlibs.time.DateTime
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
import org.koin.core.qualifier.qualifier
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) }
single<Selector> { DefaultSelector(get(), get(), get()) }
single<KeyValueRepo<PostId, DateTime>>(qualifier("latestChosenRepo")) {
ExposedKeyValueRepo(
get(),
{ text("post_id") },
{ double("date_time") },
"LatestChosenRepo"
).withMapper(
{ string },
{ unixMillis },
{ PostId(this) },
{ DateTime(this) }
)
}
single<Selector> { DefaultSelector(get(), get(), get(), get(qualifier("latestChosenRepo"))) }
}
}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,9 +14,5 @@ kotlin {
api project(":plaguposter.posts.panel")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -0,0 +1,164 @@
package dev.inmo.plaguposter.ratings.source.buttons
import korlibs.time.DateFormat
import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.d
import dev.inmo.kslog.common.i
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.SimplePagination
import dev.inmo.micro_utils.pagination.utils.paginate
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.ratings.utils.postsByRatings
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.formatting.makeLinkToMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.urlButton
import dev.inmo.tgbotapi.types.ChatIdentifier
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.utils.row
const val RootButtonsShowRatingData = "ratings_buttons_show"
const val RootButtonsShowRatingPageData = "ratings_buttons_show_page"
const val RootButtonsToPageData = "ratings_buttons_to_page"
suspend fun RatingsRepo.buildRootButtons(
pagination: Pagination = FirstPagePagination(16),
rowSize: Int = 4
): InlineKeyboardMarkup {
val postsByRatings = postsByRatings().toList().paginate(pagination)
return inlineKeyboard {
if (postsByRatings.pagesNumber > 1) {
row {
if (postsByRatings.page > 0) {
dataButton("<", "$RootButtonsToPageData ${postsByRatings.page - 1} ${postsByRatings.size}")
}
dataButton("${postsByRatings.page}: \uD83D\uDD04", "$RootButtonsToPageData ${postsByRatings.page} ${postsByRatings.size}")
if (postsByRatings.pagesNumber - postsByRatings.page > 1) {
dataButton(">", "$RootButtonsToPageData ${postsByRatings.page + 1} ${postsByRatings.size}")
}
}
}
postsByRatings.results.chunked(rowSize).map {
row {
it.forEach { (rating, posts) ->
dataButton("${rating.double}: ${posts.size}", "$RootButtonsShowRatingData ${rating.double}")
}
}
}
}
}
val defaultPostCreationTimeFormat: DateFormat = DateFormat("dd.MM.yy HH:mm")
suspend fun RatingsRepo.buildRatingButtons(
postsRepo: ReadPostsRepo,
rating: Rating,
pagination: Pagination = FirstPagePagination(8),
rowSize: Int = 2,
postCreationTimeFormat: DateFormat = defaultPostCreationTimeFormat
): InlineKeyboardMarkup {
val postsByRatings = getPosts(rating .. rating, true).keys.paginate(pagination)
TagLogger("RatingsButtonsBuilder").i { postsByRatings.results }
return inlineKeyboard {
if (postsByRatings.pagesNumber > 1) {
row {
if (postsByRatings.page > 0) {
dataButton("<", "$RootButtonsShowRatingPageData ${postsByRatings.page - 1} ${postsByRatings.size} ${rating.double}")
}
dataButton("${postsByRatings.page}: \uD83D\uDD04", "$RootButtonsShowRatingPageData ${postsByRatings.page} ${postsByRatings.size} ${rating.double}")
if (postsByRatings.pagesNumber - postsByRatings.page > 1) {
dataButton(">", "$RootButtonsShowRatingPageData ${postsByRatings.page + 1} ${postsByRatings.size} ${rating.double}")
}
}
}
postsByRatings.results.chunked(rowSize).forEach {
row {
it.forEach { postId ->
val firstMessageInfo = postsRepo.getFirstMessageInfo(postId) ?: return@forEach
val postCreationTime = postsRepo.getPostCreationTime(postId) ?: return@forEach
urlButton(
postCreationTime.format(postCreationTimeFormat),
makeLinkToMessage(
firstMessageInfo.chatId,
firstMessageInfo.messageId
)
)
}
}
}
row {
dataButton("↩️", "$RootButtonsToPageData 0 16")
}
}
}
suspend fun BehaviourContext.includeRootNavigationButtonsHandler(
allowedChats: Set<ChatIdentifier>,
ratingsRepo: RatingsRepo,
postsRepo: ReadPostsRepo
) {
suspend fun registerPageQueryListener(
dataPrefix: String,
onPageUpdate: suspend (pagination: Pagination, additionalParams: Array<String>) -> InlineKeyboardMarkup?
) {
onMessageDataCallbackQuery(
initialFilter = { it.message.chat.id in allowedChats }
) {
val args = it.data.split(" ").takeIf { it.size >= 3 } ?: return@onMessageDataCallbackQuery
val (prefix, pageRaw, sizeRaw) = args
if (prefix == dataPrefix) {
runCatchingSafely {
val page = pageRaw.toIntOrNull() ?: return@runCatchingSafely
val size = sizeRaw.toIntOrNull() ?: return@runCatchingSafely
edit(
it.message,
onPageUpdate(SimplePagination(page, size), args.drop(3).toTypedArray()) ?: return@runCatchingSafely
)
}
answer(it)
}
}
}
suspend fun registerPageQueryListener(
dataPrefix: String,
onPageUpdate: suspend (pagination: Pagination) -> InlineKeyboardMarkup?
) = registerPageQueryListener(dataPrefix) { pagination, _ ->
onPageUpdate(pagination)
}
registerPageQueryListener(
RootButtonsToPageData,
ratingsRepo::buildRootButtons
)
registerPageQueryListener(
RootButtonsShowRatingPageData
) { pagination, params ->
params.firstOrNull() ?.toDoubleOrNull() ?.let { rating ->
ratingsRepo.buildRatingButtons(postsRepo, Rating(rating), pagination)
}
}
onMessageDataCallbackQuery(
initialFilter = { it.message.chat.id in allowedChats }
) {
val (prefix, ratingRaw) = it.data.split(" ").takeIf { it.size == 2 } ?: return@onMessageDataCallbackQuery
if (prefix == RootButtonsShowRatingData) {
runCatchingSafely {
val rating = ratingRaw.toDoubleOrNull() ?: return@runCatchingSafely
edit(it.message, ratingsRepo.buildRatingButtons(postsRepo, Rating(rating)))
}
answer(it)
}
}
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.fullyCached
import dev.inmo.plaguposter.common.ShortMessageInfo
import dev.inmo.tgbotapi.types.PollIdentifier
import kotlinx.coroutines.CoroutineScope
class CachedPollsToMessagesInfoRepo(
private val repo: PollsToMessagesInfoRepo,
private val scope: CoroutineScope,
private val kvCache: FullKVCache<PollIdentifier, ShortMessageInfo> = FullKVCache()
) : PollsToMessagesInfoRepo, KeyValueRepo<PollIdentifier, ShortMessageInfo> by repo.fullyCached(kvCache, scope)

View File

@@ -0,0 +1,14 @@
package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.fullyCached
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.PollIdentifier
import kotlinx.coroutines.CoroutineScope
class CachedPollsToPostsIdsRepo(
private val repo: PollsToPostsIdsRepo,
private val scope: CoroutineScope,
private val kvCache: FullKVCache<PollIdentifier, PostId> = FullKVCache()
) : PollsToPostsIdsRepo, KeyValueRepo<PollIdentifier, PostId> by repo.fullyCached(kvCache, scope)

View File

@@ -11,15 +11,17 @@ import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.micro_utils.repos.set
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.*
import dev.inmo.plaguposter.inlines.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.ratings.source.buttons.buildRootButtons
import dev.inmo.plaguposter.ratings.source.buttons.includeRootNavigationButtonsHandler
import dev.inmo.plaguposter.ratings.source.models.*
import dev.inmo.plaguposter.ratings.source.repos.*
import dev.inmo.plaguposter.ratings.utils.postsByRatings
@@ -34,10 +36,13 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.types.ReplyParameters
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.message.textsources.bold
import dev.inmo.tgbotapi.types.message.textsources.regular
import dev.inmo.tgbotapi.utils.buildEntities
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
@@ -63,8 +68,29 @@ object Plugin : Plugin {
get<Json>().decodeFromJsonElement(Config.serializer(), params["ratingsPolls"] ?: error("Unable to load config for rating polls in $params"))
}
single<RatingsVariants>(ratingVariantsQualifier) { get<Config>().variants }
single<PollsToPostsIdsRepo> { ExposedPollsToPostsIdsRepo(database) }
single<PollsToMessagesInfoRepo> { ExposedPollsToMessagesInfoRepo(database) }
single { ExposedPollsToPostsIdsRepo(database) }
single<PollsToPostsIdsRepo> {
val base = get<ExposedPollsToPostsIdsRepo>()
if (useCache) {
CachedPollsToPostsIdsRepo(base, get())
} else {
base
}
}
single { ExposedPollsToMessagesInfoRepo(database) }
single<PollsToMessagesInfoRepo> {
val base = get<ExposedPollsToMessagesInfoRepo>()
if (useCache) {
CachedPollsToMessagesInfoRepo(base, get())
} else {
base
}
}
single<VariantTransformer> {
val ratingsSettings = get<RatingsVariants>(ratingVariantsQualifier)
VariantTransformer {
@@ -97,17 +123,21 @@ object Plugin : Plugin {
}
val post = postsRepo.getById(postId) ?: return false
ratingsRepo.set(postId, Rating(0.0))
for (content in post.content) {
runCatchingSafely {
val sent = send(
content.chatId,
config.ratingOfferText,
config.variants.keys.toList(),
replyToMessageId = content.messageId
replyParameters = ReplyParameters(content.chatId, content.messageId)
)
pollsToPostsIdsRepo.set(sent.content.poll.id, postId)
pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short())
}.getOrNull() ?: continue
delay(500L)
panelApi ?.forceRefresh(postId)
return true
}
@@ -133,12 +163,13 @@ object Plugin : Plugin {
}
}
postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { postId ->
ratingsRepo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { postId ->
detachPoll(postId)
}
if (config.autoAttach) {
postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) {
delay(500L)
attachPoll(it.id)
}
}
@@ -212,7 +243,7 @@ object Plugin : Plugin {
}
}
onCommand("ratings", requireOnlyCommandInMessage = true) {
if (it.chat.id == chatConfig.sourceChatId) {
if (it.chat.id in chatConfig.allSourceChatIds) {
val ratings = ratingsRepo.postsByRatings().toList().sortedByDescending { it.first }
val textSources = buildEntities {
+ "Ratings amount: " + bold("${ratings.sumOf { it.second.size }}") + "\n\n"
@@ -220,13 +251,23 @@ object Plugin : Plugin {
+ "" + bold("% 3.1f".format(it.first.double)) + ": " + bold(it.second.size.toString()) + "\n"
}
}
val keyboard = flatInlineKeyboard {
dataButton("Interactive mode", "ratings_interactive")
}
runCatchingSafely {
edit(it, textSources)
edit(it, textSources, replyMarkup = keyboard)
}.onFailure { _ ->
reply(it, textSources)
reply(it, textSources, replyMarkup = keyboard)
}
}
}
includeRootNavigationButtonsHandler(chatConfig.allSourceChatIds, ratingsRepo, postsRepo)
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id in chatConfig.allSourceChatIds }) {
edit(
it.message,
ratingsRepo.buildRootButtons()
)
}
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(

View File

@@ -4,10 +4,12 @@ import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.common.ShortMessageInfo
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.PollIdentifier
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
import org.jetbrains.exposed.sql.statements.*
class ExposedPollsToMessagesInfoRepo(
database: Database
@@ -17,10 +19,11 @@ class ExposedPollsToMessagesInfoRepo(
) {
override val keyColumn = text("poll_id")
private val chatIdColumn = long("chat_id")
private val threadIdColumn = long("thread_id").nullable().default(null)
private val messageIdColumn = long("message_id")
override val selectById: SqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectByValue: SqlExpressionBuilder.(ShortMessageInfo) -> Op<Boolean> = {
chatIdColumn.eq(it.chatId.chatId).and(
override val selectById: ISqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectByValue: ISqlExpressionBuilder.(ShortMessageInfo) -> Op<Boolean> = {
chatIdColumn.eq(it.chatId.chatId).and(it.chatId.threadId ?.let { threadIdColumn.eq(it) } ?: threadIdColumn.isNull()).and(
messageIdColumn.eq(it.messageId)
)
}
@@ -28,7 +31,7 @@ class ExposedPollsToMessagesInfoRepo(
get() = get(keyColumn)
override val ResultRow.asObject: ShortMessageInfo
get() = ShortMessageInfo(
get(chatIdColumn).let(::ChatId),
IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)),
get(messageIdColumn)
)
@@ -36,14 +39,13 @@ class ExposedPollsToMessagesInfoRepo(
initTable()
}
override fun update(k: PollIdentifier, v: ShortMessageInfo, it: UpdateStatement) {
override fun update(k: PollIdentifier, v: ShortMessageInfo, it: UpdateBuilder<Int>) {
it[chatIdColumn] = v.chatId.chatId
it[threadIdColumn] = v.chatId.threadId
it[messageIdColumn] = v.messageId
}
override fun insert(k: PollIdentifier, v: ShortMessageInfo, it: InsertStatement<Number>) {
override fun insertKey(k: PollIdentifier, v: ShortMessageInfo, it: InsertStatement<Number>) {
it[keyColumn] = k
it[chatIdColumn] = v.chatId.chatId
it[messageIdColumn] = v.messageId
}
}

View File

@@ -5,16 +5,15 @@ import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.PollIdentifier
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.statements.*
class ExposedPollsToPostsIdsRepo(
database: Database
) : PollsToPostsIdsRepo, AbstractExposedKeyValueRepo<PollIdentifier, PostId>(database, "polls_to_posts") {
override val keyColumn = text("poll_id")
val postIdColumn = text("postId")
override val selectById: SqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectByValue: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { postIdColumn.eq(it.string) }
override val selectById: ISqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectByValue: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { postIdColumn.eq(it.string) }
override val ResultRow.asKey: PollIdentifier
get() = get(keyColumn)
override val ResultRow.asObject: PostId
@@ -24,12 +23,11 @@ class ExposedPollsToPostsIdsRepo(
initTable()
}
override fun update(k: PollIdentifier, v: PostId, it: UpdateStatement) {
override fun update(k: PollIdentifier, v: PostId, it: UpdateBuilder<Int>) {
it[postIdColumn] = v.string
}
override fun insert(k: PollIdentifier, v: PostId, it: InsertStatement<Number>) {
override fun insertKey(k: PollIdentifier, v: PostId, it: InsertStatement<Number>) {
it[keyColumn] = k
it[postIdColumn] = v.string
}
}

View File

@@ -0,0 +1,61 @@
package dev.inmo.plaguposter.ratings.repo
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating
import kotlinx.coroutines.CoroutineScope
class CachedRatingsRepo(
private val base: RatingsRepo,
private val scope: CoroutineScope,
private val kvCache: FullKVCache<PostId, Rating> = FullKVCache()
) : RatingsRepo, KeyValueRepo<PostId, Rating> by FullKeyValueCacheRepo(base, kvCache, scope) {
override suspend fun getPosts(
range: ClosedRange<Rating>,
reversed: Boolean,
count: Int?,
exclude: List<PostId>
): Map<PostId, Rating> {
val result = mutableMapOf<PostId, Rating>()
doForAllWithNextPaging {
kvCache.keys(it).also {
it.results.forEach {
val rating = get(it) ?: return@forEach
if (it !in exclude && rating in range) {
result[it] = rating
}
}
}
}
return result.toMap()
}
override suspend fun getPostsWithRatingGreaterEq(
then: Rating,
reversed: Boolean,
count: Int?,
exclude: List<PostId>
): Map<PostId, Rating> = getPosts(
then .. Rating(Double.MAX_VALUE),
reversed,
count,
exclude
)
override suspend fun getPostsWithRatingLessEq(
then: Rating,
reversed: Boolean,
count: Int?,
exclude: List<PostId>
): Map<PostId, Rating> = getPosts(
Rating(Double.MIN_VALUE) .. then,
reversed,
count,
exclude
)
}

View File

@@ -1,8 +1,10 @@
package dev.inmo.plaguposter.ratings
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithBinds
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.useCache
import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.exposed.ExposedRatingsRepo
@@ -16,11 +18,16 @@ import org.koin.dsl.binds
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
single { ExposedRatingsRepo(database) } binds arrayOf(
RatingsRepo::class,
ReadRatingsRepo::class,
WriteRatingsRepo::class,
)
single { ExposedRatingsRepo(database) }
singleWithBinds<RatingsRepo> {
val base = get<ExposedRatingsRepo>()
if (useCache) {
CachedRatingsRepo(base, get())
} else {
base
}
}
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {

View File

@@ -1,13 +1,13 @@
package dev.inmo.plaguposter.ratings.exposed
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateStatement
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction
class ExposedRatingsRepo (
@@ -18,20 +18,23 @@ class ExposedRatingsRepo (
) {
override val keyColumn = text("post_id")
val ratingsColumn = double("rating")
override val selectById: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
override val selectByValue: SqlExpressionBuilder.(Rating) -> Op<Boolean> = { ratingsColumn.eq(it.double) }
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
override val selectByValue: ISqlExpressionBuilder.(Rating) -> Op<Boolean> = { ratingsColumn.eq(it.double) }
override val ResultRow.asKey: PostId
get() = get(keyColumn).let(::PostId)
override val ResultRow.asObject: Rating
get() = get(ratingsColumn).let(::Rating)
override fun update(k: PostId, v: Rating, it: UpdateStatement) {
init {
initTable()
}
override fun update(k: PostId, v: Rating, it: UpdateBuilder<Int>) {
it[ratingsColumn] = v.double
}
override fun insert(k: PostId, v: Rating, it: InsertStatement<Number>) {
override fun insertKey(k: PostId, v: Rating, it: InsertStatement<Number>) {
it[keyColumn] = k.string
it[ratingsColumn] = v.double
}
private fun Query.optionallyLimit(limit: Int?) = if (limit == null) {

View File

@@ -1,4 +1,4 @@
FROM adoptopenjdk/openjdk11
FROM bellsoft/liberica-openjdk-alpine:19
USER 1000

View File

@@ -15,10 +15,14 @@ dependencies {
api project(":plaguposter.posts_registrar")
api project(":plaguposter.triggers.command")
api project(":plaguposter.triggers.selector_with_timer")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.triggers.timer.disablers.autoposts")
api project(":plaguposter.triggers.timer.disablers.ratings")
api project(":plaguposter.ratings")
api project(":plaguposter.ratings.source")
api project(":plaguposter.ratings.selector")
api project(":plaguposter.ratings.gc")
api project(":plaguposter.posts.gc")
api project(":plaguposter.inlines")
api libs.psql
@@ -29,6 +33,6 @@ application {
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

@@ -1,72 +0,0 @@
{
"database": {
"url": "jdbc:postgresql://127.0.0.1:8091/test",
"username": "test",
"password": "test",
"driver": "org.postgresql.Driver"
},
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [
"dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plaguposter.inlines.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin",
"dev.inmo.plaguposter.posts.panel.Plugin"
],
"posts": {
"chats": {
"targetChat": 12345678,
"cacheChat": 12345678,
"sourceChat": 12345678
}
},
"ratingsPolls": {
"variants": {
"Круть": 2,
"Ок": 1,
"Не ок": -1,
"Совсем не ок": -2,
"Посмотреть результаты": 0
},
"autoAttach": true,
"ratingOfferText": "What do you think about it?"
},
"selector": {
"items": [
{
"time": {
"from": "00:00",
"to": "23:59"
},
"rating": {
"prefer": "max"
}
},
{
"time": {
"from": "23:59",
"to": "00:00"
},
"rating": {
"prefer": "max"
}
}
]
},
"timer_trigger": {
"krontab": "0 30 2/4 * *"
},
"panel": {
"textPrefix": "Post management:",
"buttonsPerRow": 2,
"parseMode": "MarkdownV2",
"deleteButtonText": "Delete"
},
"publish_command": {
"panelButtonText": "Publish"
}
}

View File

@@ -14,8 +14,8 @@ function assert_success() {
}
app=plaguposter
version="`grep ../gradle.properties -e "^version=" | grep -e "[0-9.]*" -o`"
server=docker.io/insanusmokrassar
version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
server=insanusmokrassar
assert_success ../gradlew build
assert_success sudo docker build -t $app:"$version" .

25
runner/nonsudo_deploy.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
function send_notification() {
echo "$1"
}
function assert_success() {
"${@}"
local status=${?}
if [ ${status} -ne 0 ]; then
send_notification "### Error ${status} at: ${BASH_LINENO[*]} ###"
exit ${status}
fi
}
app=plaguposter
version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
server=insanusmokrassar
assert_success ../gradlew build
assert_success docker build -t $app:"$version" .
assert_success docker tag $app:"$version" $server/$app:$version
assert_success docker tag $app:"$version" $server/$app:latest
assert_success docker push $server/$app:$version
assert_success docker push $server/$app:latest

View File

@@ -1,5 +0,0 @@
DATA_PATH=.
PG_USER=test_user
PG_PASSWORD=test_password
PG_DB=test_db

View File

@@ -1,131 +1,85 @@
{
"database": {
"url": "jdbc:postgresql://postgres/test_db",
"username": "test_user",
"password": "test_password",
"url": "jdbc:postgresql://postgres:5432/test",
"username": "test",
"password": "test",
"driver": "org.postgresql.Driver"
},
"botToken": "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi",
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [
"dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plaguposter.ratings.gc.Plugin",
"dev.inmo.plaguposter.inlines.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin"
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plagubot.plugins.inline.queries.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin",
"dev.inmo.plaguposter.posts.panel.Plugin",
"dev.inmo.plaguposter.common.CommonPlugin",
"dev.inmo.plaguposter.triggers.timer.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.ratings.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.autoposts.Plugin",
"dev.inmo.plaguposter.posts.gc.Plugin"
],
"posts": {
"chats": {
"targetChat": -1001234567890,
"cacheChat": -1001234567890,
"sourceChat": -1001234567890
},
"autoRemoveMessages": true
"targetChat": 12345678,
"cacheChat": 12345678,
"sourceChat": 12345678,
"targetChats": [12345678],
"_note": "You must set targetChat or targetChats with at least one object"
}
},
"ratingsPolls": {
"variants": {
"Круть": 2,
"Ок": 1,
"Не ок": -1,
"Совсем не ок": -2,
"Посмотреть результаты": 0
"Cool": 2,
"Ok": 1,
"Not ok": -1,
"Inappropriate": -2,
"Results": 0
},
"autoAttach": true,
"ratingOfferText": "How do you like it?"
"ratingOfferText": "What do you think about it?"
},
"selector": {
"items": [
{
"time": {
"from": "23:00",
"from": "00:00",
"to": "23:59"
},
"rating": {
"min": -1.0,
"max": 2.0,
"prefer": "max",
"otherwise": {
"rating": {
"min": 2.0,
"prefer": "min",
"postAge": 86400
}
},
"postAge": 86400
"uniqueCount": 1
}
},
{
"time": {
"from": "00:00",
"to": "06:59"
},
"rating": {
"min": -1.0,
"max": 2.0,
"prefer": "max",
"otherwise": {
"rating": {
"min": 2.0,
"prefer": "min",
"postAge": 86400
}
},
"postAge": 86400
}
},
{
"time": {
"from": "07:00",
"to": "12:00"
},
"rating": {
"min": 1.0,
"prefer": "min",
"otherwise": {
"rating": {
"max": 1.0,
"prefer": "max",
"postAge": 86400
}
},
"postAge": 86400
}
},
{
"time": {
"from": "12:00",
"to": "16:00"
},
"rating": {
"min": 2.0,
"prefer": "min",
"otherwise": {
"rating": {
"max": 2.0,
"prefer": "max",
"postAge": 86400
}
},
"postAge": 86400
}
},
{
"time": {
"from": "16:00",
"to": "23:00"
"from": "23:59",
"to": "00:00"
},
"rating": {
"prefer": "max",
"postAge": 86400
"uniqueCount": 1
}
}
]
},
"timer_trigger": {
"krontab": "0 30 */5 * *"
"krontab": "0 30 2/4 * *",
"retryOnPostFailureTimes": 0,
"_note": "retryOnPostFailureTimes will retry to publish one or several posts if posting has been failed"
},
"panel": {
"textPrefix": "Post management:",
"buttonsPerRow": 2,
"parseMode": "MarkdownV2",
"deleteButtonText": "Delete"
},
"publish_command": {
"panelButtonText": "Publish"
},
"gc": {
"autoclear": {
@@ -133,6 +87,11 @@
"autoClearKrontab": "0 0 0 * *",
"skipPostAge": 86400
},
"immediateDrop": -2
"immediateDrop": -6
},
"messagesChecker": {
"krontab": "0 0 0 * *",
"throttlingMillis": 1000,
"doFullCheck": false
}
}

View File

@@ -2,22 +2,24 @@ version: "3.4"
services:
plaguposter_postgres:
image: postgres
image: postgres:15.4-bullseye
container_name: "plaguposter_postgres"
restart: "unless-stopped"
environment:
POSTGRES_USER: "${PG_USER}"
POSTGRES_PASSWORD: "${PG_PASSWORD}"
POSTGRES_DB: "${PG_DB}"
POSTGRES_USER: "test"
POSTGRES_PASSWORD: "test"
POSTGRES_DB: "test"
volumes:
- "${DATA_PATH}/db/:/var/lib/postgresql/"
- "./db/:/var/lib/postgresql/data"
- "/etc/timezone:/etc/timezone:ro"
plaguposter:
image: insanusmokrassar/plaguposter
image: insanusmokrassar/plaguposter:latest
container_name: "plaguposter"
restart: "unless-stopped"
volumes:
- "${DATA_PATH}/config.json:/config.json"
links:
- "plaguposter_postgres:postgres"
- "./config.json:/config.json"
- "/etc/timezone:/etc/timezone:ro"
depends_on:
- "plaguposter_postgres"
links:
- "plaguposter_postgres:postgres"

View File

@@ -4,6 +4,7 @@ String[] includes = [
":common",
":posts",
":posts:panel",
":posts:gc",
":posts_registrar",
":ratings",
":ratings:source",
@@ -11,6 +12,9 @@ String[] includes = [
":ratings:gc",
":triggers:command",
":triggers:selector_with_timer",
":triggers:timer",
":triggers:timer:disablers:ratings",
":triggers:timer:disablers:autoposts",
":inlines",
// ":settings",
":runner"
@@ -26,5 +30,3 @@ includes.each { originalName ->
project.name = projectName
project.projectDir = new File(projectDirectory)
}
enableFeaturePreview("VERSION_CATALOGS")

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -13,9 +12,5 @@ kotlin {
api project(":plaguposter.common")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -1,7 +1,6 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -16,9 +15,5 @@ kotlin {
api project(":plaguposter.posts.panel")
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -1,29 +1,22 @@
package dev.inmo.plaguposter.triggers.command
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.inlines.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.posts.sending.PostPublisher
import dev.inmo.plaguposter.ratings.selector.Selector
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitTextMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.*
@@ -33,9 +26,7 @@ import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.textsources.regular
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
@@ -78,8 +69,15 @@ object Plugin : Plugin {
}
}
val postId = messageInReply ?.let {
postsRepo.getIdByChatAndMessage(messageInReply.chat.id, messageInReply.messageId)
} ?: selector ?.take(1) ?.firstOrNull()
postsRepo.getIdByChatAndMessage(messageInReply.chat.id, messageInReply.messageId) ?: let { _ ->
reply(
it,
"Unable to find any post related to the message in reply"
)
return@onCommand
}
} ?: selector ?.takeOneOrNull()
if (postId == null) {
reply(
it,

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"
@@ -16,9 +16,5 @@ kotlin {
api libs.krontab
}
}
jvmMain {
dependencies {
}
}
}
}

View File

@@ -0,0 +1,8 @@
package dev.inmo.plaguposter.triggers.selector_with_timer
import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId
fun interface AutopostFilter {
suspend fun check(postId: PostId, dateTime: DateTime): Boolean
}

View File

@@ -1,14 +1,43 @@
package dev.inmo.plaguposter.triggers.selector_with_timer
import korlibs.time.DateFormat
import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.toSchedule
import dev.inmo.krontab.utils.asFlow
import dev.inmo.krontab.utils.asFlowWithDelays
import dev.inmo.krontab.utils.asFlowWithoutDelays
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.firstIndex
import dev.inmo.micro_utils.pagination.lastIndexExclusive
import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
import dev.inmo.plaguposter.posts.sending.PostPublisher
import dev.inmo.plaguposter.ratings.selector.Selector
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.formatting.makeLinkToMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.urlButton
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.utils.row
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.take
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
@@ -16,15 +45,22 @@ import org.koin.core.Koin
import org.koin.core.module.Module
object Plugin : Plugin {
@Serializable
private const val pageCallbackDataQueryPrefix = "publishing_autoschedule page"
private const val pageCallbackDataQuerySize = 5
@Serializable
internal data class Config(
@SerialName("krontab")
val krontabTemplate: KrontabTemplate
val krontabTemplate: KrontabTemplate,
val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy",
val retryOnPostFailureTimes: Int = 0
) {
@Transient
val krontab by lazy {
krontabTemplate.toSchedule()
}
@Transient
val format: DateFormat = DateFormat(dateTimeFormat)
}
override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["timer_trigger"] ?: return@single null) }
@@ -34,9 +70,117 @@ object Plugin : Plugin {
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val publisher = koin.get<PostPublisher>()
val selector = koin.get<Selector>()
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) {
selector.take(now = it).forEach { postId ->
publisher.publish(postId)
val filters = koin.getAll<AutopostFilter>().distinct()
val chatConfig = koin.get<ChatConfig>()
val postsRepo = koin.get<ReadPostsRepo>()
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(
OfferTemplate(
"Autoschedule buttons",
listOf(
Format(
"/autoschedule_panel"
)
),
"Show autoscheduling publishing info"
)
)
}
val krontab = koin.get<Config>().krontab
val retryOnPostFailureTimes = koin.get<Config>().retryOnPostFailureTimes
val dateTimeFormat = koin.get<Config>().format
krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime ->
var leftRetries = retryOnPostFailureTimes
do {
val success = runCatching {
selector.takeOneOrNull(now = dateTime) ?.let { postId ->
if (filters.all { it.check(postId, dateTime) }) {
publisher.publish(postId)
} else {
false
}
} ?: false
}.getOrElse {
false
}
if (success) {
break;
}
leftRetries--;
} while (leftRetries > 0)
}
suspend fun buildPage(pagination: Pagination = FirstPagePagination(size = pageCallbackDataQuerySize)): InlineKeyboardMarkup {
return inlineKeyboard {
row {
if (pagination.page > 1) {
dataButton("⬅️", "${pageCallbackDataQueryPrefix}0")
}
if (pagination.page > 0) {
dataButton("◀️", "${pageCallbackDataQueryPrefix}${pagination.page - 1}")
}
dataButton("\uD83D\uDD04 ${pagination.page}", "${pageCallbackDataQueryPrefix}${pagination.page}")
dataButton("▶️", "${pageCallbackDataQueryPrefix}${pagination.page + 1}")
}
val selected = mutableListOf<PostId>()
krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime ->
val postId = selector.takeOneOrNull(now = dateTime, exclude = selected) ?.also { postId ->
if (filters.all { it.check(postId, dateTime) }) {
selected.add(postId)
} else {
return@collectIndexed
}
}
val post = postsRepo.getFirstMessageInfo(postId ?: return@collectIndexed)
if (i < pagination.firstIndex || post == null) {
return@collectIndexed
}
row {
urlButton(
dateTime.local.format(dateTimeFormat),
makeLinkToMessage(post.chatId, post.messageId)
)
}
}
}
}
onCommand("autoschedule_panel", initialFilter = { chatConfig.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
val keyboard = buildPage()
runCatchingSafely {
edit(it, replyMarkup = keyboard) {
+"Your schedule:"
}
}.onFailure { _ ->
send(it.chat, replyMarkup = keyboard) {
+"Your schedule:"
}
}
}
onMessageDataCallbackQuery(
Regex("^$pageCallbackDataQueryPrefix\\d+"),
initialFilter = { chatConfig.allSourceChatIds.any { sourceChatId -> it.message.sameChat(sourceChatId) } }
) {
val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ ->
answer(it)
return@onMessageDataCallbackQuery
}
runCatchingSafely {
edit(
it.message,
replyMarkup = buildPage(Pagination(page, size = pageCallbackDataQuerySize))
)
}.onFailure { _ ->
answer(it)
}
}
}

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.posts")
api project(":plaguposter.posts.panel")
}
}
}
}

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.triggers.selector_with_timer")
}
}
}
}

View File

@@ -0,0 +1 @@
package dev.inmo.plaguposter.triggers.timer.disablers.autoposts

View File

@@ -0,0 +1,27 @@
package dev.inmo.plaguposter.triggers.timer.disablers.autoposts
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.triggers.selector_with_timer.AutopostFilter
import dev.inmo.plaguposter.triggers.timer.TimersRepo
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
singleWithRandomQualifier<AutopostFilter> {
val timersRepo = get<TimersRepo>()
AutopostFilter { _, dateTime ->
val result = timersRepo.keys(dateTime, FirstPagePagination(1))
result.results.isEmpty()
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.plaguposter.triggers.timer.disablers.autoposts"/>

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.ratings")
}
}
}
}

View File

@@ -0,0 +1 @@
package dev.inmo.plaguposter.triggers.timer.disablers.ratings

View File

@@ -0,0 +1,26 @@
package dev.inmo.plaguposter.triggers.timer.disablers.ratings
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.triggers.timer.TimersRepo
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
singleWithRandomQualifier(createdAtStart = true) {
val timersRepo = get<TimersRepo>()
val ratingsRepo = get<RatingsRepo>()
val scope = get<CoroutineScope>()
timersRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) {
ratingsRepo.unset(it.first)
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.plaguposter.triggers.timer.disablers.ratings"/>

View File

@@ -0,0 +1,287 @@
package dev.inmo.plaguposter.triggers.timer
import korlibs.time.DateFormat
import korlibs.time.DateTime
import korlibs.time.DateTimeTz
import korlibs.time.Month
import korlibs.time.Year
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.row
object ButtonsBuilder {
private const val changeTimeData = "timer_time_hint"
private const val changeDateData = "timer_date_hint"
private const val changeHoursDataPrefix = "timer_h"
private const val changeMinutesDataPrefix = "timer_m"
private const val changeDayDataPrefix = "timer_d"
private const val changeMonthDataPrefix = "timer_M"
private const val changeYearDataPrefix = "timer_y"
private const val changeDateDataPrefix = "timer_s"
private const val cancelDateData = "timer_c"
private const val deleteDateDataPrefix = "timer_r"
val datePrintFormat = DateFormat("HH:mm, dd.MM.yyyy, zzz")
fun buildTimerButtons(
postId: PostId,
dateTime: DateTimeTz,
exists: Boolean
) = inlineKeyboard {
val unixMillis = dateTime.utc.unixMillisLong
row {
dataButton("Time (hh:mm):", changeTimeData)
dataButton(dateTime.hours.toString(), "$changeHoursDataPrefix $postId $unixMillis")
dataButton(dateTime.minutes.toString(), "$changeMinutesDataPrefix $postId $unixMillis")
}
row {
dataButton("Date (dd.mm.yyyy):", changeDateData)
dataButton("${dateTime.dayOfMonth}", "$changeDayDataPrefix $postId $unixMillis")
dataButton("${dateTime.month1}", "$changeMonthDataPrefix $postId $unixMillis")
dataButton("${dateTime.yearInt}", "$changeYearDataPrefix $postId $unixMillis")
}
row {
if (exists) {
dataButton("\uD83D\uDDD1", "$deleteDateDataPrefix $postId")
}
dataButton(UnsuccessfulSymbol, cancelDateData)
dataButton(SuccessfulSymbol, "$changeDateDataPrefix $postId $unixMillis")
}
}
fun buildTimerTextSources(
currentDateTime: DateTime,
previousTime: DateTime?
) = buildEntities {
previousTime ?.let {
+ "Previous timer time: " + bold(it.local.toString(datePrintFormat)) + "\n"
}
+"Currently editing time: " + bold(currentDateTime.local.toString(datePrintFormat))
}
suspend fun BehaviourContext.includeKeyboardHandling(
timersRepo: TimersRepo,
onSavePublishingTime: suspend (PostId, DateTime) -> Boolean
) {
fun buildKeyboard(
prefix: String,
postId: PostId,
values: Iterable<Int>,
min: DateTime = nearestAvailableTimerTime(),
dateConverter: (Int) -> DateTimeTz
): InlineKeyboardMarkup {
return inlineKeyboard {
values.chunked(6).forEach {
row {
it.forEach {
dataButton(it.toString(), "$prefix $postId ${dateConverter(it).utc.unixMillisLong.coerceAtLeast(min.unixMillisLong)}")
}
}
}
}
}
suspend fun buildStandardDataCallbackQuery(
name: String,
prefix: String,
possibleValues: (DateTimeTz) -> Iterable<Int>,
dateTimeConverter: (Int, DateTimeTz) -> DateTimeTz
) {
val setPrefix = "${prefix}s"
onMessageDataCallbackQuery(Regex("$prefix .+")) {
val (_, rawPostId, rawDateTimeMillis) = it.data.split(" ")
val currentMillis = rawDateTimeMillis.toLongOrNull() ?: return@onMessageDataCallbackQuery
val currentDateTime = DateTime(currentMillis)
val currentDateTimeLocal = DateTime(currentMillis).local
val postId = PostId(rawPostId)
val previousTime = timersRepo.get(postId)
edit (
it.message.withContentOrNull() ?: return@onMessageDataCallbackQuery,
replyMarkup = buildKeyboard(
setPrefix,
postId,
possibleValues(currentDateTimeLocal)
) {
dateTimeConverter(it, currentDateTimeLocal)
}
) {
+buildTimerTextSources(currentDateTime, previousTime) + "\n"
+"You are about to edit $name"
}
}
onMessageDataCallbackQuery(Regex("$setPrefix .+")) {
val (_, rawPostId, rawDateTimeMillis) = it.data.split(" ")
val currentMillis = rawDateTimeMillis.toLongOrNull() ?: return@onMessageDataCallbackQuery
val currentDateTime = DateTime(currentMillis)
val postId = PostId(rawPostId)
val previousTime = timersRepo.get(postId)
edit(
it.message.withContentOrNull() ?: return@onMessageDataCallbackQuery,
replyMarkup = buildTimerButtons(
postId,
currentDateTime.local,
timersRepo.contains(postId)
)
) {
+buildTimerTextSources(currentDateTime, previousTime)
}
}
}
fun DateTimeTz.dateEq(other: DateTimeTz) = yearInt == other.yearInt && month0 == other.month0 && dayOfMonth == other.dayOfMonth
buildStandardDataCallbackQuery(
"hour",
changeHoursDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.dateEq(it)) {
now.hours .. 23
} else {
0 .. 23
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(hours = newValue),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"minute",
changeMinutesDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.dateEq(it) && now.hours >= it.hours) {
now.minutes until 60
} else {
0 until 60
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(minutes = newValue),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"day",
changeDayDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.yearInt == it.yearInt && now.month0 == it.month0) {
now.dayOfMonth .. it.month.days(it.year)
} else {
1 .. it.month.days(it.year)
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(dayOfMonth = newValue),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"month",
changeMonthDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.year == it.year) {
now.month1 .. 12
} else {
1 .. 12
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(month = Month(newValue)),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"year",
changeYearDataPrefix,
{
val now = nearestAvailableTimerTime().local
(now.year.year .. (now.year.year + 5))
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(year = Year(newValue)),
oldDateTime.offset
)
}
onMessageDataCallbackQuery(changeTimeData) {
answer(it, "Use the buttons to the right to set post publishing time (hh:mm)", showAlert = true)
}
onMessageDataCallbackQuery(changeDateData) {
answer(it, "Use the buttons to the right to set post publishing date (dd.MM.yyyy)", showAlert = true)
}
onMessageDataCallbackQuery(Regex("$changeDateDataPrefix .*")) {
val (_, rawPostId, rawDateTimeMillis) = it.data.split(" ")
val currentMillis = rawDateTimeMillis.toLongOrNull() ?: return@onMessageDataCallbackQuery
val currentDateTime = DateTime(currentMillis)
val postId = PostId(rawPostId)
val success = runCatchingSafely {
onSavePublishingTime(postId, currentDateTime)
}.getOrElse { false }
answer(
it,
if (success) "Successfully set timer" else "Unable to set timer"
)
it.message.delete(this)
}
onMessageDataCallbackQuery(Regex("$deleteDateDataPrefix .*")) {
val (_, rawPostId) = it.data.split(" ")
val postId = PostId(rawPostId)
val success = runCatchingSafely {
timersRepo.unset(postId)
true
}.getOrElse { false }
answer(
it,
if (success) "Successfully unset timer" else "Unable to unset timer"
)
it.message.delete(this)
}
onMessageDataCallbackQuery(cancelDateData) {
delete(it.message)
}
}
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.plaguposter.triggers.timer
import korlibs.time.DateTime
import korlibs.time.minutes
fun nearestAvailableTimerTime() = (DateTime.now() + 1.minutes).copyDayOfMonth(
milliseconds = 0,
seconds = 0
)

View File

@@ -0,0 +1 @@
package dev.inmo.plaguposter.triggers.timer

Some files were not shown because too many files have changed in this diff Show More