155 Commits

Author SHA1 Message Date
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
47f9e09bd2 updates 2022-10-03 00:51:51 +06:00
1d88423b03 add opportunity to finish post creating by command 2022-10-03 00:20:38 +06:00
3634b9a2a2 start implement #4 2022-10-02 22:45:37 +06:00
4b7c2451cd update dependencies 2022-10-02 22:43:26 +06:00
1cd7c07d29 start 0.0.4 2022-10-02 22:41:45 +06:00
9c4b49df22 small hotfix 2022-09-16 21:20:01 +06:00
963d33a452 small hotfix 2022-09-16 21:18:22 +06:00
7a05b56af2 small hotfix 2022-09-16 21:14:38 +06:00
39d49f5d58 Merge pull request #3 from InsanusMokrassar/0.0.3
0.0.3
2022-09-16 21:12:52 +06:00
b9700a8fd9 make force refresh button optional 2022-09-16 21:11:30 +06:00
79c195f534 small updates 2022-09-16 16:53:57 +06:00
91221d865c update dependencies 2022-09-16 15:34:08 +06:00
5808f45677 start 0.0.3 2022-09-16 15:32:48 +06:00
29d6d11d87 update sample config 2022-09-15 02:27:06 +06:00
609c6b97fc Merge pull request #2 from InsanusMokrassar/0.0.2
0.0.2
2022-09-15 02:22:44 +06:00
107 changed files with 2491 additions and 836 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

78
CHANGELOG.md Normal file
View File

@@ -0,0 +1,78 @@
# PlaguPoster
## 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 { dependencies {
classpath libs.android.tools.build
classpath libs.android.dexcount
classpath libs.kotlin.gradle.plugin classpath libs.kotlin.gradle.plugin
classpath libs.kotlin.serialization.plugin classpath libs.kotlin.serialization.plugin
classpath libs.kotlin.dokka.plugin classpath libs.kotlin.dokka.plugin
@@ -20,8 +18,9 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
} }
} }
apply from: "./extensions.gradle" 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 { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@@ -12,7 +11,9 @@ kotlin {
dependencies { dependencies {
api libs.tgbotapi api libs.tgbotapi
api libs.microutils.repos.common api libs.microutils.repos.common
api libs.microutils.repos.cache
api libs.kslog api libs.kslog
api libs.microutils.koin
} }
} }
jvmMain { jvmMain {

View File

@@ -1,15 +1,43 @@
package dev.inmo.plaguposter.common 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ChatConfig( data class ChatConfig(
@SerialName("targetChat") @SerialName("targetChat")
val targetChatId: ChatId, @Serializable(FullChatIdentifierSerializer::class)
val targetChatId: IdChatIdentifier? = null,
@SerialName("sourceChat") @SerialName("sourceChat")
val sourceChatId: ChatId, @Serializable(FullChatIdentifierSerializer::class)
val sourceChatId: IdChatIdentifier?,
@SerialName("cacheChat") @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.filters.CommonMessageFilterExcludeMediaGroups
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter 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.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.BotCommand import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.message.abstracts.* import dev.inmo.tgbotapi.types.message.abstracts.*
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
val FirstSourceIsCommandsFilter = SimpleFilter<Message> { val FirstSourceIsCommandsFilter = SimpleFilter<Message> {
it is ContentMessage<*> && it.content.textContentOrNull() ?.textSources ?.firstOrNull { it.contentMessageOrNull() ?.withContentOrNull<TextContent>() ?.content ?.textSources ?.firstOrNull() is BotCommandTextSource
it is BotCommandTextSource
} != null
} }

View File

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

View File

@@ -1,13 +1,16 @@
package dev.inmo.plaguposter.common package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId 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.MessageIdentifier
import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.abstracts.Message
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ShortMessageInfo( data class ShortMessageInfo(
val chatId: ChatId, @Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier,
val messageId: MessageIdentifier 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" 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 # Project data
group=dev.inmo group=dev.inmo
version=0.0.2 version=0.5.3
android_code_version=2

View File

@@ -1,26 +1,18 @@
[versions] [versions]
kotlin = "1.7.10" kotlin = "1.9.22"
kotlin-serialization = "1.4.0" kotlin-serialization = "1.6.2"
plagubot = "2.3.1" plagubot = "8.1.1"
tgbotapi = "3.2.1" tgbotapi = "10.0.1"
microutils = "0.12.13" microutils = "0.20.34"
kslog = "0.5.2" kslog = "1.3.2"
krontab = "0.8.1" krontab = "2.2.7"
tgbotapi-libraries = "0.5.3" plagubot-plugins = "0.18.1"
psql = "42.3.6" dokka = "1.9.10"
dexcount = "3.1.0" psql = "42.6.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 = "32"
android-buildTools = "32.0.0"
[libraries] [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-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-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
kotlin-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", 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" } tgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "tgbotapi" }
plagubot-plugin = { module = "dev.inmo:plagubot.plugin", version.ref = "plagubot" } plagubot-plugin = { module = "dev.inmo:plagubot.plugin", version.ref = "plagubot" }
plagubot-bot = { module = "dev.inmo:plagubot.bot", 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-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-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" }
microutils-repos-cache = { module = "dev.inmo:micro_utils.repos.cache", 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 # 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-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
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] [plugins]

View File

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

View File

@@ -1,7 +1,6 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,6 +14,7 @@ kotlin {
} }
jvmMain { jvmMain {
dependencies { 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 package dev.inmo.plaguposter.inlines
import dev.inmo.micro_utils.pagination.Pagination import dev.inmo.kslog.common.TagLogger
import dev.inmo.micro_utils.pagination.utils.paginate import dev.inmo.kslog.common.w
import dev.inmo.plagubot.Plugin 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.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onBaseInlineQuery import kotlinx.serialization.json.JsonObject
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 org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
object Plugin : Plugin { private val actualPlugin = dev.inmo.plagubot.plugins.inline.queries.Plugin
@Serializable
internal data class Config( object Plugin : Plugin by actualPlugin {
val preset: List<OfferTemplate> private val log = TagLogger("InlinePlugin")
)
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["inlines"] ?: return@single Config(emptyList())) } single { actualPlugin }
single { InlineTemplatesRepo(getOrNull<Config>() ?.preset ?.toMutableSet() ?: mutableSetOf()) }
} }
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val templatesRepo = koin.get<InlineTemplatesRepo>() log.w {
onBaseInlineQuery { query -> "Built-in inline plugin has been deprecated. Use \"${actualPlugin::class.qualifiedName}\" instead"
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
)
}
} }
onCommand("help", requireOnlyCommandInMessage = true) { with(actualPlugin) {
reply( setupBotPlugin(koin)
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"))
)
)
} }
} }
} }

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.version = "$version"
project.group = "$group" project.group = "$group"
// apply from: "$publishGradlePath" apply from: "$publishGradlePath"
kotlin { kotlin {
jvm { jvm {
compilations.main { compilations.main {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "17"
} }
} }
} }
@@ -34,6 +34,6 @@ kotlin {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }

View File

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

View File

@@ -1,24 +1,15 @@
project.version = "$version" project.version = "$version"
project.group = "$group" project.group = "$group"
// apply from: "$publishGradlePath" apply from: "$publishGradlePath"
kotlin { kotlin {
jvm { jvm()
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js (IR) { js (IR) {
browser() browser()
nodejs() nodejs()
} }
android {
publishAllLibraryVariants()
}
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
@@ -43,19 +34,7 @@ kotlin {
implementation libs.kotlin.test.junit 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 { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" 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 { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,5 +14,10 @@ kotlin {
api libs.microutils.koin 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.plaguposter.posts.models.RegisteredPost
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton
fun interface PanelButtonBuilder { interface PanelButtonBuilder {
val weight: Int
suspend fun buildButton(post: RegisteredPost): InlineKeyboardButton? 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 import kotlinx.coroutines.flow.MutableSharedFlow
class PanelButtonsAPI( class PanelButtonsAPI(
private val preset: List<PanelButtonBuilder>, private val preset: Map<Int, List<PanelButtonBuilder>>,
private val rootPanelButtonText: String private val rootPanelButtonText: String
) { ) {
private val _buttons = mutableSetOf<PanelButtonBuilder>().also { private val _buttonsMap = mutableMapOf<Int, MutableList<PanelButtonBuilder>>().also {
it.addAll(preset) it.putAll(preset.map { it.key to it.value.toMutableList() })
} }
internal val buttonsBuilders: List<PanelButtonBuilder> internal val buttonsBuilders: List<PanelButtonBuilder>
get() = _buttons.toList() get() = _buttonsMap.toList().sortedBy { it.first }.flatMap { it.second }
internal val forceRefreshFlow = MutableSharedFlow<PostId>() internal val forceRefreshFlow = MutableSharedFlow<PostId>()
val RootPanelButtonBuilder = PanelButtonBuilder { val RootPanelButtonBuilder = PanelButtonBuilder {
@@ -22,8 +22,13 @@ class PanelButtonsAPI(
) )
} }
fun add(button: PanelButtonBuilder) = _buttons.add(button) fun add(button: PanelButtonBuilder, weight: Int = button.weight) = _buttonsMap.getOrPut(weight) { mutableListOf() }.add(button)
fun remove(button: PanelButtonBuilder) = _buttons.remove(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) { suspend fun forceRefresh(postId: PostId) {
forceRefreshFlow.emit(postId) forceRefreshFlow.emit(postId)
} }

View File

@@ -1,32 +1,43 @@
package dev.inmo.plaguposter.posts.panel package dev.inmo.plaguposter.posts.panel
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.getAllDistinct import dev.inmo.micro_utils.koin.getAllDistinct
import dev.inmo.micro_utils.repos.deleteById import dev.inmo.micro_utils.repos.*
import dev.inmo.micro_utils.repos.set 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.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig 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.models.PostId
import dev.inmo.plaguposter.posts.panel.repos.PostsMessages import dev.inmo.plaguposter.posts.panel.repos.PostsMessages
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit 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.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery 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.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage 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.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.MessageIdentifier 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.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.ParseMode import dev.inmo.tgbotapi.types.message.ParseMode
import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.types.message.content.TextContent import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.italic
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@@ -41,7 +52,8 @@ object Plugin : Plugin {
val parseMode: ParseMode? = null, val parseMode: ParseMode? = null,
val buttonsPerRow: Int = 4, val buttonsPerRow: Int = 4,
val deleteButtonText: String? = null, val deleteButtonText: String? = null,
val rootButtonText: String = "Return to panel" val rootButtonText: String = "◀️",
val refreshButtonText: String? = "\uD83D\uDD04"
) )
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
params["panel"] ?.let { element -> params["panel"] ?.let { element ->
@@ -57,12 +69,24 @@ object Plugin : Plugin {
"delete ${it.id.string}" "delete ${it.id.string}"
) )
} }
},
config.refreshButtonText ?.let { text ->
PanelButtonBuilder {
CallbackDataInlineKeyboardButton(
text,
"refresh ${it.id.string}"
)
}
} }
) )
PanelButtonsAPI( PanelButtonsAPI(
getAllDistinct<PanelButtonBuilder>() + builtInButtons, emptyMap(),
config.rootButtonText config.rootButtonText
) ).apply {
(getAllDistinct<PanelButtonBuilder>() + builtInButtons).forEach {
add(it)
}
}
} }
} }
@@ -71,7 +95,12 @@ object Plugin : Plugin {
val chatsConfig = koin.get<ChatConfig>() val chatsConfig = koin.get<ChatConfig>()
val config = koin.getOrNull<Config>() ?: Config() val config = koin.getOrNull<Config>() ?: Config()
val api = koin.get<PanelButtonsAPI>() 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) { postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) {
val firstContent = it.content.first() val firstContent = it.content.first()
@@ -84,7 +113,7 @@ object Plugin : Plugin {
firstContent.chatId, firstContent.chatId,
text = config.text, text = config.text,
parseMode = config.parseMode, parseMode = config.parseMode,
replyToMessageId = firstContent.messageId, replyParameters = ReplyParameters(firstContent.chatId, firstContent.messageId),
replyMarkup = InlineKeyboardMarkup(buttons), replyMarkup = InlineKeyboardMarkup(buttons),
disableNotification = true disableNotification = true
).also { sentMessage -> ).also { sentMessage ->
@@ -97,9 +126,9 @@ object Plugin : Plugin {
delete(chatId, messageId) delete(chatId, messageId)
} }
suspend fun updatePost( suspend fun refreshPostMessage(
postId: PostId, postId: PostId,
chatId: ChatId, chatId: IdChatIdentifier,
messageId: MessageIdentifier messageId: MessageIdentifier
) { ) {
val post = postsRepo.getById(postId) ?: return val post = postsRepo.getById(postId) ?: return
@@ -108,24 +137,26 @@ object Plugin : Plugin {
builder.buildButton(post) builder.buildButton(post)
}.takeIf { it.isNotEmpty() } }.takeIf { it.isNotEmpty() }
} }
edit( editMessageText(
chatId, chatId,
messageId, messageId,
text = config.text,
replyMarkup = InlineKeyboardMarkup(buttons) replyMarkup = InlineKeyboardMarkup(buttons)
) )
} }
onMessageDataCallbackQuery ( onMessageDataCallbackQuery (
initialFilter = { 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) val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId)
updatePost(postId, it.message.chat.id, it.message.messageId) refreshPostMessage(postId, it.message.chat.id, it.message.messageId)
answer(it)
} }
onMessageDataCallbackQuery( onMessageDataCallbackQuery(
initialFilter = { initialFilter = {
it.data.startsWith("delete ") && it.message.chat.id == chatsConfig.sourceChatId it.data.startsWith("delete ") && it.message.chat.id in chatsConfig.allSourceChatIds
} }
) { query -> ) { query ->
val postId = query.data.removePrefix("delete ").let(::PostId) val postId = query.data.removePrefix("delete ").let(::PostId)
@@ -140,6 +171,7 @@ object Plugin : Plugin {
api.RootPanelButtonBuilder.buildButton(post) ?.let(::add) api.RootPanelButtonBuilder.buildButton(post) ?.let(::add)
} }
) )
answer(query)
val pushedButton = waitMessageDataCallbackQuery().first { val pushedButton = waitMessageDataCallbackQuery().first {
it.message.sameMessage(query.message) it.message.sameMessage(query.message)
@@ -149,10 +181,81 @@ object Plugin : Plugin {
postsRepo.deleteById(postId) postsRepo.deleteById(postId)
} }
} }
onMessageDataCallbackQuery(
initialFilter = {
it.data.startsWith("refresh ") && it.message.chat.id in chatsConfig.allSourceChatIds
}
) { query ->
val postId = query.data.removePrefix("refresh ").let(::PostId)
val (chatId, messageId) = postsMessages.get(postId) ?: return@onMessageDataCallbackQuery
runCatchingSafely {
refreshPostMessage(
postId,
chatId,
messageId
)
}
answer(query)
}
api.forceRefreshFlow.subscribeSafelyWithoutExceptions(this) { api.forceRefreshFlow.subscribeSafelyWithoutExceptions(this) {
val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions
updatePost(it, chatId, messageId) 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.micro_utils.repos.mappers.withMapper
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.ChatId 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.MessageIdentifier
import kotlinx.serialization.builtins.PairSerializer import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
private val ChatIdToMessageSerializer = PairSerializer(ChatId.serializer(), MessageIdentifier.serializer()) private val ChatIdToMessageSerializer = PairSerializer(FullChatIdentifierSerializer, MessageIdentifier.serializer())
fun PostsMessages( fun PostsMessages(
database: Database, database: Database,
json: Json json: Json
): KeyValueRepo<PostId, Pair<ChatId, MessageIdentifier>> = ExposedKeyValueRepo<String, String>( ): KeyValueRepo<PostId, Pair<IdChatIdentifier, MessageIdentifier>> = ExposedKeyValueRepo<String, String>(
database, database,
{ text("postId") }, { text("postId") },
{ text("chatToMessage") }, { text("chatToMessage") },
@@ -25,5 +27,5 @@ fun PostsMessages(
{ string }, { string },
{ json.encodeToString(ChatIdToMessageSerializer, this) }, { json.encodeToString(ChatIdToMessageSerializer, this) },
{ PostId(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 package dev.inmo.plaguposter.posts.models
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.plaguposter.common.DateTimeSerializer import dev.inmo.plaguposter.common.DateTimeSerializer
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -1,25 +1,39 @@
package dev.inmo.plaguposter.posts.models 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.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage 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 import kotlinx.serialization.Serializable
@Serializable @Serializable
data class PostContentInfo( data class PostContentInfo(
val chatId: ChatId, @Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier,
val messageId: MessageIdentifier, val messageId: MessageIdentifier,
val group: String?, val group: String?,
val order: Int val order: Int
) { ) {
companion object { companion object {
fun fromMessage(message: ContentMessage<*>, order: Int) = PostContentInfo( private fun fromMessage(message: ContentMessage<*>, order: Int) = PostContentInfo(
message.chat.id, message.chat.id,
message.messageId, message.messageId,
message.mediaGroupMessageOrNull() ?.mediaGroupId, message.possiblyMediaGroupMessageOrNull() ?.mediaGroupId,
order 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 @JvmInline
value class PostId( value class PostId(
val string: String val string: String
) ) {
override fun toString(): String = string
}

View File

@@ -1,12 +1,14 @@
package dev.inmo.plaguposter.posts.repo 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.micro_utils.repos.ReadCRUDRepo
import dev.inmo.plaguposter.posts.models.* import dev.inmo.plaguposter.posts.models.*
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.MessageIdentifier
interface ReadPostsRepo : ReadCRUDRepo<RegisteredPost, PostId> { 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 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.api.send.send
import dev.inmo.tgbotapi.extensions.utils.* import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class PostPublisher( class PostPublisher(
private val bot: TelegramBot, private val bot: TelegramBot,
private val postsRepo: PostsRepo, private val postsRepo: PostsRepo,
private val cachingChatId: ChatId, private val cachingChatId: IdChatIdentifier,
private val targetChatId: ChatId, private val targetChatIds: List<IdChatIdentifier>,
private val deleteAfterPosting: Boolean = true private val deleteAfterPosting: Boolean = true
) { ) {
suspend fun publish(postId: PostId) { suspend fun publish(postId: PostId): Boolean {
val messagesInfo = postsRepo.getById(postId) ?: let { val messagesInfo = postsRepo.getById(postId) ?: let {
logger.w { "Unable to get post with id $postId for publishing" } logger.w { "Unable to get post with id $postId for publishing" }
return return false
} }
val sortedMessagesContents = messagesInfo.content.groupBy { it.group }.flatMap { (group, list) -> val sortedMessagesContents = messagesInfo.content.groupBy { it.group }.flatMap { (group, list) ->
if (group == null) { if (group == null) {
@@ -34,27 +34,54 @@ class PostPublisher(
listOf(list.first().order to list) listOf(list.first().order to list)
} }
}.sortedBy { it.first } }.sortedBy { it.first }
var haveSentMessages = false
sortedMessagesContents.forEach { (_, contents) -> sortedMessagesContents.forEach { (_, contents) ->
contents.singleOrNull() ?.also { 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 return@forEach
} }
val resultContents = contents.mapNotNull { val resultContents = contents.mapNotNull {
it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null) it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null)
}.sortedBy { it.first }.mapNotNull { (_, it) -> }.sortedBy { it.first }.mapNotNull { (_, forwardedMessage) ->
it.withContentOrNull<MediaGroupContent>() ?: null.also { _ -> forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ ->
bot.copyMessage(targetChatId, it) targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, forwardedMessage)
haveSentMessages = true
}
} }
} }
resultContents.singleOrNull() ?.also { resultContents.singleOrNull() ?.also {
bot.copyMessage(targetChatId, it) targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, it)
haveSentMessages = true
}
return@forEach return@forEach
} ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach { } ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach {
bot.send( targetChatIds.forEach { targetChatId ->
targetChatId, bot.send(
it.map { it.content.toMediaGroupMemberTelegramMedia() } targetChatId,
) it.map { it.content.toMediaGroupMemberTelegramMedia() }
)
haveSentMessages = true
}
} }
} }
@@ -62,5 +89,6 @@ class PostPublisher(
postsRepo.deleteById(postId) 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.kslog.common.w
import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithBinds
import dev.inmo.micro_utils.repos.deleteById import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.SuccessfulSymbol import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo
import dev.inmo.plaguposter.common.ChatConfig import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.inlines.models.Format import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo 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.repo.*
import dev.inmo.plaguposter.posts.sending.PostPublisher import dev.inmo.plaguposter.posts.sending.PostPublisher
import dev.inmo.tgbotapi.extensions.api.delete import dev.inmo.tgbotapi.extensions.api.delete
@@ -26,7 +29,6 @@ import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.binds
object Plugin : Plugin { object Plugin : Plugin {
@Serializable @Serializable
@@ -44,14 +46,19 @@ object Plugin : Plugin {
} }
single { get<Json>().decodeFromJsonElement(Config.serializer(), configJson) } single { get<Json>().decodeFromJsonElement(Config.serializer(), configJson) }
single { get<Config>().chats } single { get<Config>().chats }
single { ExposedPostsRepo(database) } binds arrayOf( single { ExposedPostsRepo(database) }
PostsRepo::class, singleWithBinds<PostsRepo> {
ReadPostsRepo::class, val base = get<ExposedPostsRepo>()
WritePostsRepo::class,
) if (useCache) {
CachedPostsRepo(base, get())
} else {
base
}
}
single { single {
val config = get<Config>() 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.micro_utils.repos.exposed.*
import dev.inmo.plaguposter.posts.models.* import dev.inmo.plaguposter.posts.models.*
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
internal class ExposedContentInfoRepo( internal class ExposedContentInfoRepo(
@@ -13,13 +14,14 @@ internal class ExposedContentInfoRepo(
) : ExposedRepo, Table(name = "posts_content") { ) : ExposedRepo, Table(name = "posts_content") {
val postIdColumn = text("post_id").references(postIdColumnReference, ReferenceOption.CASCADE, ReferenceOption.CASCADE) val postIdColumn = text("post_id").references(postIdColumnReference, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
val chatIdColumn = long("chat_id") val chatIdColumn = long("chat_id")
val threadIdColumn = long("thread_id").nullable().default(null)
val messageIdColumn = long("message_id") val messageIdColumn = long("message_id")
val groupColumn = text("group").nullable() val groupColumn = text("group").nullable()
val orderColumn = integer("order") val orderColumn = integer("order")
val ResultRow.asObject val ResultRow.asObject
get() = PostContentInfo( get() = PostContentInfo(
ChatId(get(chatIdColumn)), IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)),
get(messageIdColumn), get(messageIdColumn),
get(groupColumn), get(groupColumn),
get(orderColumn) get(orderColumn)

View File

@@ -1,19 +1,22 @@
package dev.inmo.plaguposter.posts.exposed package dev.inmo.plaguposter.posts.exposed
import com.benasher44.uuid.uuid4 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.KeyValuesRepo
import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.plaguposter.posts.models.* import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.MessageIdentifier
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.statements.UpdateStatement import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
class ExposedPostsRepo( class ExposedPostsRepo(
@@ -21,8 +24,9 @@ class ExposedPostsRepo(
) : PostsRepo, AbstractExposedCRUDRepo<RegisteredPost, PostId, NewPost>( ) : PostsRepo, AbstractExposedCRUDRepo<RegisteredPost, PostId, NewPost>(
tableName = "posts" tableName = "posts"
) { ) {
val idColumn = text("id").clientDefault { uuid4().toString() } val idColumn = text("id")
val createdColumn = double("datetime").default(0.0) val createdColumn = double("datetime").default(0.0)
val latestUpdateColumn = double("latest_update").default(0.0)
private val contentRepo by lazy { private val contentRepo by lazy {
ExposedContentInfoRepo( ExposedContentInfoRepo(
@@ -33,11 +37,13 @@ class ExposedPostsRepo(
override val primaryKey: PrimaryKey = PrimaryKey(idColumn) override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
override val selectById: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) } override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) }
override val selectByIds: SqlExpressionBuilder.(List<PostId>) -> Op<Boolean> = { idColumn.inList(it.map { 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 override val ResultRow.asObject: RegisteredPost
get() { get() {
val id = PostId(get(idColumn)) val id = asId
return RegisteredPost( return RegisteredPost(
id, id,
DateTime(get(createdColumn)), DateTime(get(createdColumn)),
@@ -59,18 +65,6 @@ class ExposedPostsRepo(
override fun InsertStatement<Number>.asObject(value: NewPost): RegisteredPost { override fun InsertStatement<Number>.asObject(value: NewPost): RegisteredPost {
val id = PostId(get(idColumn)) 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( return RegisteredPost(
id, id,
DateTime(get(createdColumn)), DateTime(get(createdColumn)),
@@ -82,25 +76,55 @@ class ExposedPostsRepo(
) )
} }
override fun update(id: PostId, value: NewPost, it: UpdateStatement) { override fun createAndInsertId(value: NewPost, it: InsertStatement<Number>): PostId {
with(contentRepo) { val id = PostId(uuid4().toString())
deleteWhere { postIdColumn.eq(id.string) } it[idColumn] = id.string
value.content.forEach { contentInfo -> return id
insert { }
it[postIdColumn] = id.string
it[chatIdColumn] = contentInfo.chatId.chatId override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {
it[messageIdColumn] = contentInfo.messageId it[latestUpdateColumn] = DateTime.now().unixMillis
it[groupColumn] = contentInfo.group }
it[orderColumn] = contentInfo.order
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>) { override fun insert(value: NewPost, it: InsertStatement<Number>) {
super.insert(value, it)
it[createdColumn] = DateTime.now().unixMillis 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>) { override suspend fun deleteById(ids: List<PostId>) {
onBeforeDelete(ids) onBeforeDelete(ids)
val posts = ids.mapNotNull { val posts = ids.mapNotNull {
@@ -109,7 +133,7 @@ class ExposedPostsRepo(
val existsIds = posts.keys.toList() val existsIds = posts.keys.toList()
transaction(db = database) { transaction(db = database) {
val deleted = deleteWhere(null, null) { val deleted = deleteWhere(null, null) {
selectByIds(existsIds) selectByIds(it, existsIds)
} }
with(contentRepo) { with(contentRepo) {
deleteWhere { 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) { return transaction(database) {
with(contentRepo) { 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) } ?.let(::PostId)
} }
} }
@@ -140,4 +168,10 @@ class ExposedPostsRepo(
override suspend fun getPostCreationTime(postId: PostId): DateTime? = transaction(database) { override suspend fun getPostCreationTime(postId: PostId): DateTime? = transaction(database) {
select { selectById(postId) }.limit(1).firstOrNull() ?.get(createdColumn) ?.let(::DateTime) 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 { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@@ -13,9 +12,5 @@ kotlin {
api project(":plaguposter.posts") 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.micro_utils.fsm.common.State
import dev.inmo.plaguposter.posts.models.PostContentInfo import dev.inmo.plaguposter.posts.models.PostContentInfo
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
interface RegistrationState : State { interface RegistrationState : State {
override val context: ChatId override val context: IdChatIdentifier
@Serializable @Serializable
data class InProcess( data class InProcess(
override val context: ChatId, @Serializable(FullChatIdentifierSerializer::class)
override val context: IdChatIdentifier,
val messages: List<PostContentInfo> val messages: List<PostContentInfo>
) : RegistrationState ) : RegistrationState
@Serializable @Serializable
data class Finish( data class Finish(
override val context: ChatId, @Serializable(FullChatIdentifierSerializer::class)
override val context: IdChatIdentifier,
val messages: List<PostContentInfo> val messages: List<PostContentInfo>
) : RegistrationState ) : 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.micro_utils.repos.create
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.* import dev.inmo.plaguposter.common.*
import dev.inmo.plaguposter.inlines.models.Format import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.posts.models.* import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.registrar.state.RegistrationState import dev.inmo.plaguposter.posts.registrar.state.RegistrationState
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
@@ -18,83 +18,95 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* 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.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.formatting.regular
import dev.inmo.tgbotapi.extensions.utils.mediaGroupMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.types.buttons.* import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage import dev.inmo.tgbotapi.utils.regular
import dev.inmo.tgbotapi.types.message.content.MessageContent import kotlinx.coroutines.async
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.core.Koin import org.koin.core.Koin
@Serializable @Serializable
object Plugin : Plugin { object Plugin : Plugin {
@Serializable
data class Config(
val useInlineFinishingOpportunity: Boolean = true
)
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
val config = koin.get<ChatConfig>() val config = koin.get<ChatConfig>()
val postsRepo = koin.get<PostsRepo>() val postsRepo = koin.get<PostsRepo>()
strictlyOn {state: RegistrationState.InProcess -> strictlyOn { state: RegistrationState.InProcess ->
val buttonUuid = "finish" val buttonUuid = "finish"
val messageToDelete = send( val suggestionMessageDeferred = async {
state.context, send(
buildEntities { state.context,
if (state.messages.isNotEmpty()) { dev.inmo.tgbotapi.utils.buildEntities {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post") 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 { } 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 { add {
listOf( val receivedMessage = waitAnyContentMessage().filter {
waitContentMessage( it.sameChat(state.context)
includeMediaGroups = false }.first()
).filter {
it.chat.id == state.context when {
}.take(1).first() receivedMessage.content.textContentOrNull() ?.text == "/finish_post" -> {
) val messageToDelete = suggestionMessageDeferred.await()
} edit(messageToDelete, "Ok, finishing your request")
add { RegistrationState.Finish(
waitMediaGroupMessages().filter { state.context,
it.first().chat.id == state.context state.messages
}.take(1).first() )
}
else -> {
RegistrationState.InProcess(
state.context,
state.messages + PostContentInfo.fromMessage(receivedMessage)
).also {
runCatchingSafely {
suggestionMessageDeferred.cancel()
}
runCatchingSafely {
delete(suggestionMessageDeferred.await())
}
}
}
}
} }
add { add {
val messageToDelete = suggestionMessageDeferred.await()
val finishPressed = waitMessageDataCallbackQuery().filter { val finishPressed = waitMessageDataCallbackQuery().filter {
it.message.sameMessage(messageToDelete) && it.data == buttonUuid it.message.sameMessage(messageToDelete) && it.data == buttonUuid
}.first() }.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( edit(messageToDelete, "Ok, finishing your request")
state.context, RegistrationState.Finish(
state.messages + newMessagesInfo state.context,
).also { state.messages
delete(messageToDelete) )
}
} }
} }
@@ -107,36 +119,28 @@ object Plugin : Plugin {
null 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())) startChain(RegistrationState.InProcess(it.chat.id, emptyList()))
} }
onContentMessage( 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)))) startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it)))
}
onMediaGroup(
initialFilter = { it.first().chat.id == config.sourceChatId }
) {
startChain(
RegistrationState.Finish(
it.first().chat.id,
it.map {
PostContentInfo.fromMessage(
it,
0
)
}
)
)
} }
koin.getOrNull<InlineTemplatesRepo>() ?.apply { koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate( addTemplate(
OfferTemplate( OfferTemplate(
"Start post", "Start post creating",
listOf(Format("/start_post")) listOf(Format("/start_post")),
"Use this command to start creating of complex post with several messages"
)
)
addTemplate(
OfferTemplate(
"Finish post creating",
listOf(Format("/finish_post")),
"Finish creating of complex post"
) )
) )
} }

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package dev.inmo.plaguposter.ratings.selector 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.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.repo.RatingsRepo import dev.inmo.plaguposter.ratings.repo.RatingsRepo
@@ -9,13 +10,14 @@ import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
class DefaultSelector ( class DefaultSelector (
private val config: SelectorConfig, private val config: SelectorConfig,
private val ratingsRepo: RatingsRepo, private val ratingsRepo: RatingsRepo,
private val postsRepo: PostsRepo private val postsRepo: PostsRepo,
private val latestChosenRepo: KeyValueRepo<PostId, DateTime>
) : Selector { ) : 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>() val result = mutableListOf<PostId>()
do { 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) result.add(selected)
} while (result.size < n) } while (result.size < n)

View File

@@ -1,8 +1,13 @@
package dev.inmo.plaguposter.ratings.selector package dev.inmo.plaguposter.ratings.selector
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
interface Selector { 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 package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import com.soywiz.klock.seconds import korlibs.time.seconds
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.pagination.getAll import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.common.DateTimeSerializer import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
@@ -25,17 +23,23 @@ data class RatingConfig(
val max: Rating? = null, val max: Rating? = null,
val prefer: Prefer = Prefer.Random, val prefer: Prefer = Prefer.Random,
val otherwise: RatingConfig? = null, val otherwise: RatingConfig? = null,
val postAge: Seconds? = null val postAge: Seconds? = null,
val uniqueCount: Int? = null
) { ) {
suspend fun select( suspend fun select(
ratingsRepo: RatingsRepo, ratingsRepo: RatingsRepo,
postsRepo: PostsRepo, postsRepo: PostsRepo,
exclude: List<PostId>, exclude: List<PostId>,
now: DateTime now: DateTime,
latestChosenRepo: KeyValueRepo<PostId, DateTime>
): PostId? { ): PostId? {
var reversed: Boolean = false var reversed: Boolean = false
var count: Int? = null var count: Int? = null
val allowedCreationTime = now - (postAge ?: 0).seconds 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) { when (prefer) {
Prefer.Max -> { Prefer.Max -> {
@@ -59,40 +63,53 @@ data class RatingConfig(
ratingsRepo.getAllByWithNextPaging { keys(it) } ratingsRepo.getAllByWithNextPaging { keys(it) }
} }
else -> { else -> {
ratingsRepo.getPostsWithRatingLessEq(max, exclude = exclude).keys ratingsRepo.getPostsWithRatingLessEq(max, exclude = resultExcluded).keys
} }
} }
} }
else -> { else -> {
when (max) { when (max) {
null -> { null -> {
ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = exclude).keys ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = resultExcluded).keys
} }
else -> { else -> {
ratingsRepo.getPosts(min .. max, reversed, count, exclude = exclude).keys ratingsRepo.getPosts(min .. max, reversed, count, exclude = resultExcluded).keys
} }
} }
} }
}.filter { }.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.Max,
Prefer.Min -> posts.firstOrNull() Prefer.Min -> posts.firstOrNull()
Prefer.Random -> posts.randomOrNull() 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) @Serializable(Prefer.Serializer::class)
sealed interface Prefer { sealed interface Prefer {
val type: String val type: String
@Serializable(Serializer::class) @Serializable(Serializer::class)
object Max : Prefer { override val type: String = "max" } data object Max : Prefer { override val type: String = "max" }
@Serializable(Serializer::class) @Serializable(Serializer::class)
object Min : Prefer { override val type: String = "min" } data object Min : Prefer { override val type: String = "min" }
@Serializable(Serializer::class) @Serializable(Serializer::class)
object Random : Prefer { override val type: String = "random" } data object Random : Prefer { override val type: String = "random" }
object Serializer : KSerializer<Prefer> { object Serializer : KSerializer<Prefer> {
override val descriptor: SerialDescriptor = String.serializer().descriptor override val descriptor: SerialDescriptor = String.serializer().descriptor

View File

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

View File

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

View File

@@ -1,14 +1,33 @@
package dev.inmo.plaguposter.ratings.selector 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.plagubot.Plugin
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
import korlibs.time.DateTime
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.core.qualifier.qualifier
object Plugin : Plugin { object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) } 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 { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@@ -15,9 +14,5 @@ kotlin {
api project(":plaguposter.posts.panel") 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,17 +11,20 @@ import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.micro_utils.repos.set import dev.inmo.micro_utils.repos.set
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.* import dev.inmo.plaguposter.common.*
import dev.inmo.plaguposter.inlines.models.Format import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plaguposter.inlines.models.OfferTemplate import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo 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.models.*
import dev.inmo.plaguposter.ratings.source.repos.* import dev.inmo.plaguposter.ratings.source.repos.*
import dev.inmo.plaguposter.ratings.utils.postsByRatings
import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit import dev.inmo.tgbotapi.extensions.api.edit.edit
@@ -33,9 +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.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton 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.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.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.message.textsources.bold
import dev.inmo.tgbotapi.types.message.textsources.regular import dev.inmo.tgbotapi.types.message.textsources.regular
import kotlinx.coroutines.flow.filter import dev.inmo.tgbotapi.utils.buildEntities
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@@ -61,8 +68,29 @@ object Plugin : Plugin {
get<Json>().decodeFromJsonElement(Config.serializer(), params["ratingsPolls"] ?: error("Unable to load config for rating polls in $params")) get<Json>().decodeFromJsonElement(Config.serializer(), params["ratingsPolls"] ?: error("Unable to load config for rating polls in $params"))
} }
single<RatingsVariants>(ratingVariantsQualifier) { get<Config>().variants } 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> { single<VariantTransformer> {
val ratingsSettings = get<RatingsVariants>(ratingVariantsQualifier) val ratingsSettings = get<RatingsVariants>(ratingVariantsQualifier)
VariantTransformer { VariantTransformer {
@@ -79,6 +107,7 @@ object Plugin : Plugin {
val postsRepo = koin.get<PostsRepo>() val postsRepo = koin.get<PostsRepo>()
val config = koin.get<Config>() val config = koin.get<Config>()
val panelApi = koin.getOrNull<PanelButtonsAPI>() val panelApi = koin.getOrNull<PanelButtonsAPI>()
val chatConfig = koin.get<ChatConfig>()
onPollUpdates (markerFactory = { it.id }) { poll -> onPollUpdates (markerFactory = { it.id }) { poll ->
val postId = pollsToPostsIdsRepo.get(poll.id) ?: return@onPollUpdates val postId = pollsToPostsIdsRepo.get(poll.id) ?: return@onPollUpdates
@@ -94,17 +123,21 @@ object Plugin : Plugin {
} }
val post = postsRepo.getById(postId) ?: return false val post = postsRepo.getById(postId) ?: return false
ratingsRepo.set(postId, Rating(0.0))
for (content in post.content) { for (content in post.content) {
runCatchingSafely { runCatchingSafely {
val sent = send( val sent = send(
content.chatId, content.chatId,
config.ratingOfferText, config.ratingOfferText,
config.variants.keys.toList(), config.variants.keys.toList(),
replyToMessageId = content.messageId replyParameters = ReplyParameters(content.chatId, content.messageId)
) )
pollsToPostsIdsRepo.set(sent.content.poll.id, postId) pollsToPostsIdsRepo.set(sent.content.poll.id, postId)
pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short()) pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short())
}.getOrNull() ?: continue }.getOrNull() ?: continue
delay(500L)
panelApi ?.forceRefresh(postId) panelApi ?.forceRefresh(postId)
return true return true
} }
@@ -130,12 +163,13 @@ object Plugin : Plugin {
} }
} }
postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { postId -> ratingsRepo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { postId ->
detachPoll(postId) detachPoll(postId)
} }
if (config.autoAttach) { if (config.autoAttach) {
postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) { postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) {
delay(500L)
attachPoll(it.id) attachPoll(it.id)
} }
} }
@@ -208,6 +242,32 @@ object Plugin : Plugin {
} }
} }
} }
onCommand("ratings", requireOnlyCommandInMessage = true) {
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"
ratings.forEach {
+ "" + 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, replyMarkup = keyboard)
}.onFailure { _ ->
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 { koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate( addTemplate(
@@ -273,6 +333,7 @@ object Plugin : Plugin {
attachPoll(postId) attachPoll(postId)
} }
} }
answer(it)
} }
} }
} }

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

@@ -0,0 +1,15 @@
package dev.inmo.plaguposter.ratings.utils
import dev.inmo.micro_utils.pagination.utils.getAll
import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
suspend fun RatingsRepo.postsByRatings(): Map<Rating, List<PostId>> {
return getAll { keys(it) }.groupBy {
it.second
}.map {
it.key to it.value.map { it.first }
}.toMap()
}

View File

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

View File

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

View File

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

View File

@@ -1,80 +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.ratings.gc.Plugin",
"dev.inmo.plaguposter.inlines.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin"
],
"posts": {
"targetChat": 12345678,
"cacheChat": 12345678,
"sourceChat": 12345678
},
"ratingsPolls": {
"variants": {
"Ok": 1,
"Not ok": -1
},
"autoAttach": true,
"ratingOfferText": "What do you think about it?"
},
"selector": {
"items": [
{
"time": {
"from": "00:00",
"to": "00:00"
},
"rating": {
"min": 0.0,
"max": 1.0,
"prefer": "random"
}
},
{
"time": {
"from": "00:00",
"to": "00:00"
},
"rating": {
"min": 0.0,
"max": 1.0,
"prefer": "min"
}
},
{
"time": {
"from": "00:00",
"to": "00:00"
},
"rating": {
"min": 0.0,
"max": 1.0,
"prefer": "max"
}
}
]
},
"timer_trigger": {
"krontab": "0 30 2/4 * *"
},
"gc": {
"autoclear": {
"rating": -2,
"autoClearKrontab": "0 0 0 * *",
"skipPostAge": 86400
}
}
}

View File

@@ -14,8 +14,8 @@ function assert_success() {
} }
app=plaguposter app=plaguposter
version="`grep ../gradle.properties -e "^version=" | grep -e "[0-9.]*" -o`" version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
server=docker.io/insanusmokrassar server=insanusmokrassar
assert_success ../gradlew build assert_success ../gradlew build
assert_success sudo docker build -t $app:"$version" . 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": { "database": {
"url": "jdbc:postgresql://postgres/test_db", "url": "jdbc:postgresql://postgres:5432/test",
"username": "test_user", "username": "test",
"password": "test_password", "password": "test",
"driver": "org.postgresql.Driver" "driver": "org.postgresql.Driver"
}, },
"botToken": "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi", "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [ "plugins": [
"dev.inmo.plaguposter.posts.Plugin", "dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin", "dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin", "dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin", "dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin", "dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plaguposter.ratings.gc.Plugin", "dev.inmo.plaguposter.ratings.gc.Plugin",
"dev.inmo.plaguposter.inlines.Plugin", "dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plaguposter.triggers.command.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": { "posts": {
"chats": { "chats": {
"targetChat": -1001234567890, "targetChat": 12345678,
"cacheChat": -1001234567890, "cacheChat": 12345678,
"sourceChat": -1001234567890 "sourceChat": 12345678,
}, "targetChats": [12345678],
"autoRemoveMessages": true "_note": "You must set targetChat or targetChats with at least one object"
}
}, },
"ratingsPolls": { "ratingsPolls": {
"variants": { "variants": {
"Круть": 2, "Cool": 2,
"Ок": 1, "Ok": 1,
"Не ок": -1, "Not ok": -1,
"Совсем не ок": -2, "Inappropriate": -2,
"Посмотреть результаты": 0 "Results": 0
}, },
"autoAttach": true, "autoAttach": true,
"ratingOfferText": "How do you like it?" "ratingOfferText": "What do you think about it?"
}, },
"selector": { "selector": {
"items": [ "items": [
{ {
"time": { "time": {
"from": "23:00", "from": "00:00",
"to": "23:59" "to": "23:59"
}, },
"rating": { "rating": {
"min": -1.0,
"max": 2.0,
"prefer": "max", "prefer": "max",
"otherwise": { "uniqueCount": 1
"rating": {
"min": 2.0,
"prefer": "min",
"postAge": 86400
}
},
"postAge": 86400
} }
}, },
{ {
"time": { "time": {
"from": "00:00", "from": "23:59",
"to": "06:59" "to": "00:00"
},
"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"
}, },
"rating": { "rating": {
"prefer": "max", "prefer": "max",
"postAge": 86400 "uniqueCount": 1
} }
} }
] ]
}, },
"timer_trigger": { "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": { "gc": {
"autoclear": { "autoclear": {
@@ -133,6 +87,11 @@
"autoClearKrontab": "0 0 0 * *", "autoClearKrontab": "0 0 0 * *",
"skipPostAge": 86400 "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: services:
plaguposter_postgres: plaguposter_postgres:
image: postgres image: postgres:15.4-bullseye
container_name: "plaguposter_postgres" container_name: "plaguposter_postgres"
restart: "unless-stopped" restart: "unless-stopped"
environment: environment:
POSTGRES_USER: "${PG_USER}" POSTGRES_USER: "test"
POSTGRES_PASSWORD: "${PG_PASSWORD}" POSTGRES_PASSWORD: "test"
POSTGRES_DB: "${PG_DB}" POSTGRES_DB: "test"
volumes: volumes:
- "${DATA_PATH}/db/:/var/lib/postgresql/" - "./db/:/var/lib/postgresql/data"
- "/etc/timezone:/etc/timezone:ro"
plaguposter: plaguposter:
image: insanusmokrassar/plaguposter image: insanusmokrassar/plaguposter:latest
container_name: "plaguposter" container_name: "plaguposter"
restart: "unless-stopped" restart: "unless-stopped"
volumes: volumes:
- "${DATA_PATH}/config.json:/config.json" - "./config.json:/config.json"
links: - "/etc/timezone:/etc/timezone:ro"
- "plaguposter_postgres:postgres"
depends_on: depends_on:
- "plaguposter_postgres" - "plaguposter_postgres"
links:
- "plaguposter_postgres:postgres"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "org.jetbrains.kotlin.multiplatform" id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization" id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
} }
apply from: "$mppProjectWithSerializationPresetPath" apply from: "$mppProjectWithSerializationPresetPath"
@@ -16,9 +16,5 @@ kotlin {
api libs.krontab 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 package dev.inmo.plaguposter.triggers.selector_with_timer
import korlibs.time.DateFormat
import dev.inmo.krontab.KrontabTemplate import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.toSchedule 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.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.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.posts.sending.PostPublisher
import dev.inmo.plaguposter.ratings.selector.Selector 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.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.FlowPreview
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.take
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@@ -16,15 +45,22 @@ import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
object Plugin : Plugin { object Plugin : Plugin {
@Serializable private const val pageCallbackDataQueryPrefix = "publishing_autoschedule page"
private const val pageCallbackDataQuerySize = 5
@Serializable
internal data class Config( internal data class Config(
@SerialName("krontab") @SerialName("krontab")
val krontabTemplate: KrontabTemplate val krontabTemplate: KrontabTemplate,
val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy",
val retryOnPostFailureTimes: Int = 0
) { ) {
@Transient @Transient
val krontab by lazy { val krontab by lazy {
krontabTemplate.toSchedule() krontabTemplate.toSchedule()
} }
@Transient
val format: DateFormat = DateFormat(dateTimeFormat)
} }
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["timer_trigger"] ?: return@single null) } 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) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val publisher = koin.get<PostPublisher>() val publisher = koin.get<PostPublisher>()
val selector = koin.get<Selector>() val selector = koin.get<Selector>()
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { val filters = koin.getAll<AutopostFilter>().distinct()
selector.take(now = it).forEach { postId -> val chatConfig = koin.get<ChatConfig>()
publisher.publish(postId) 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
)

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