Compare commits

58 Commits

Author SHA1 Message Date
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
75 changed files with 1629 additions and 161 deletions

View File

@@ -1,16 +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: 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 11
uses: actions/setup-java@v1
with:
java-version: 11
- 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 }}

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

@@ -0,0 +1,28 @@
name: Docker
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- 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

8
CHANGELOG.md Normal file
View File

@@ -0,0 +1,8 @@
# PlaguPoster
## 0.0.10
## 0.0.9
* Update depedencies

View File

@@ -18,8 +18,9 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
} }
} }
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

@@ -11,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,27 @@
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 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,
@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
) {
fun check(chatId: IdChatIdentifier) = when (chatId) {
targetChatId,
sourceChatId,
cacheChatId -> true
else -> false
}
}

View File

@@ -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,34 @@
package dev.inmo.plaguposter.common
import dev.inmo.kslog.common.i
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 chat info: ${getChat(config.targetChatId)}" }
Log.iS { "Source chat info: ${getChat(config.sourceChatId)}" }
Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" }
}
}

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.5 version=0.1.0
android_code_version=5

View File

@@ -1,15 +1,17 @@
[versions] [versions]
kotlin = "1.7.20" kotlin = "1.8.10"
kotlin-serialization = "1.4.1" kotlin-serialization = "1.5.0"
plagubot = "2.4.0" plagubot = "5.0.0"
tgbotapi = "3.3.0" tgbotapi = "7.0.0"
microutils = "0.13.1" microutils = "0.17.5"
kslog = "0.5.2" kslog = "1.0.0"
krontab = "0.8.1" krontab = "0.9.0"
tgbotapi-libraries = "0.5.6" tgbotapi-libraries = "0.10.0"
plagubot-plugins = "0.5.0" plagubot-plugins = "0.10.0"
dokka = "1.8.10"
psql = "42.5.0" psql = "42.5.0"
@@ -26,6 +28,7 @@ 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-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" }
@@ -39,7 +42,7 @@ psql = { module = "org.postgresql:postgresql", version.ref = "psql" }
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-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -4,12 +4,20 @@ import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.w import dev.inmo.kslog.common.w
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module
private val actualPlugin = dev.inmo.plagubot.plugins.inline.queries.Plugin private val actualPlugin = dev.inmo.plagubot.plugins.inline.queries.Plugin
object Plugin : Plugin by actualPlugin { object Plugin : Plugin by actualPlugin {
private val log = TagLogger("InlinePlugin") private val log = TagLogger("InlinePlugin")
override fun Module.setupDI(database: Database, params: JsonObject) {
single { actualPlugin }
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
log.w { log.w {
"Built-in inline plugin has been deprecated. Use \"${actualPlugin::class.qualifiedName}\" instead" "Built-in inline plugin has been deprecated. Use \"${actualPlugin::class.qualifiedName}\" instead"

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 {
jvm { jvm {

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,7 +1,7 @@
project.version = "$version" project.version = "$version"
project.group = "$group" project.group = "$group"
// apply from: "$publishGradlePath" apply from: "$publishGradlePath"
kotlin { kotlin {
jvm() jvm()

View File

@@ -14,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

@@ -4,10 +4,18 @@ import com.benasher44.uuid.uuid4
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.getAllDistinct import dev.inmo.micro_utils.koin.getAllDistinct
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.deleteById import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.micro_utils.repos.id
import dev.inmo.micro_utils.repos.set import dev.inmo.micro_utils.repos.set
import dev.inmo.micro_utils.repos.unset
import dev.inmo.micro_utils.repos.value
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
@@ -15,18 +23,23 @@ 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.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.types.ChatId import dev.inmo.tgbotapi.types.IdChatIdentifier
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.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.ParseMode import dev.inmo.tgbotapi.types.message.ParseMode
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.italic
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@@ -69,9 +82,13 @@ object Plugin : Plugin {
} }
) )
PanelButtonsAPI( PanelButtonsAPI(
getAllDistinct<PanelButtonBuilder>() + builtInButtons, emptyMap(),
config.rootButtonText config.rootButtonText
) ).apply {
(getAllDistinct<PanelButtonBuilder>() + builtInButtons).forEach {
add(it)
}
}
} }
} }
@@ -80,7 +97,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.cached(FullKVCache(), koin.get())
} else {
basePostsMessages
}
postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) { postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) {
val firstContent = it.content.first() val firstContent = it.content.first()
@@ -108,7 +130,7 @@ object Plugin : Plugin {
suspend fun refreshPostMessage( 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
@@ -183,5 +205,59 @@ object Plugin : Plugin {
val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions
refreshPostMessage(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,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

@@ -4,9 +4,11 @@ import com.soywiz.klock.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

@@ -12,12 +12,13 @@ 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.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 targetChatId: IdChatIdentifier,
private val deleteAfterPosting: Boolean = true private val deleteAfterPosting: Boolean = true
) { ) {
suspend fun publish(postId: PostId) { suspend fun publish(postId: PostId) {
@@ -37,14 +38,26 @@ class PostPublisher(
sortedMessagesContents.forEach { (_, contents) -> sortedMessagesContents.forEach { (_, contents) ->
contents.singleOrNull() ?.also { contents.singleOrNull() ?.also {
bot.copyMessage(targetChatId, it.chatId, it.messageId) runCatching {
bot.copyMessage(targetChatId, it.chatId, it.messageId)
}.onFailure { _ ->
runCatching {
bot.forwardMessage(
it.chatId,
targetChatId,
it.messageId
)
}.onSuccess {
bot.copyMessage(targetChatId, it)
}
}
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) bot.copyMessage(targetChatId, forwardedMessage)
} }
} }
resultContents.singleOrNull() ?.also { resultContents.singleOrNull() ?.also {

View File

@@ -4,6 +4,7 @@ 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
@@ -13,6 +14,8 @@ import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plagubot.plugins.inline.queries.models.Format 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.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.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
@@ -44,11 +47,16 @@ 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.targetChatId, config.deleteAfterPublishing)

View File

@@ -0,0 +1,49 @@
package dev.inmo.plaguposter.posts.cached
import com.soywiz.klock.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.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: FullKVCache<PostId, RegisteredPost> = FullKVCache()
) : PostsRepo, CRUDRepo<RegisteredPost, PostId, NewPost> by FullCRUDCacheRepo(
parentRepo,
kvCache,
scope,
{ 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

@@ -3,15 +3,19 @@ package dev.inmo.plaguposter.posts.exposed
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import com.soywiz.klock.DateTime import com.soywiz.klock.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.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.statements.* import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -32,11 +36,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)),
@@ -75,17 +81,21 @@ class ExposedPostsRepo(
return id return id
} }
override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) { override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {}
id ?: error("Unable to find post id in update")
with(contentRepo) { private fun updateContent(post: RegisteredPost) {
deleteWhere { postIdColumn.eq(id.string) } transaction(database) {
value.content.forEach { contentInfo -> with(contentRepo) {
insert { deleteWhere { postIdColumn.eq(post.id.string) }
it[postIdColumn] = id.string post.content.forEach { contentInfo ->
it[chatIdColumn] = contentInfo.chatId.chatId insert {
it[messageIdColumn] = contentInfo.messageId it[postIdColumn] = post.id.string
it[groupColumn] = contentInfo.group it[chatIdColumn] = contentInfo.chatId.chatId
it[orderColumn] = contentInfo.order it[threadIdColumn] = contentInfo.chatId.threadId
it[messageIdColumn] = contentInfo.messageId
it[groupColumn] = contentInfo.group
it[orderColumn] = contentInfo.order
}
} }
} }
} }
@@ -96,6 +106,22 @@ class ExposedPostsRepo(
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 {
@@ -104,7 +130,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 {
@@ -124,10 +150,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)
} }
} }
@@ -135,4 +165,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

@@ -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

@@ -21,13 +21,12 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat 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.formatting.regular
import dev.inmo.tgbotapi.extensions.utils.mediaGroupMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
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.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import dev.inmo.tgbotapi.types.message.content.MessageContent import dev.inmo.tgbotapi.types.message.content.MessageContent
import dev.inmo.tgbotapi.utils.regular
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
@@ -43,7 +42,7 @@ object Plugin : Plugin {
val messageToDelete = send( val messageToDelete = send(
state.context, state.context,
buildEntities { dev.inmo.tgbotapi.utils.buildEntities {
if (state.messages.isNotEmpty()) { if (state.messages.isNotEmpty()) {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post") regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
} else { } else {
@@ -65,18 +64,11 @@ object Plugin : Plugin {
val newMessagesInfo = firstOf { val newMessagesInfo = firstOf {
add { add {
listOf( listOf(
waitContentMessage( waitAnyContentMessage().filter {
includeMediaGroups = false
).filter {
it.chat.id == state.context && it.content.textContentOrNull() ?.text != "/finish_post" it.chat.id == state.context && it.content.textContentOrNull() ?.text != "/finish_post"
}.take(1).first() }.take(1).first()
) )
} }
add {
waitMediaGroupMessages().filter {
it.first().chat.id == state.context
}.take(1).first()
}
add { add {
val finishPressed = waitMessageDataCallbackQuery().filter { val finishPressed = waitMessageDataCallbackQuery().filter {
it.message.sameMessage(messageToDelete) && it.data == buttonUuid it.message.sameMessage(messageToDelete) && it.data == buttonUuid
@@ -95,8 +87,8 @@ object Plugin : Plugin {
state.context, state.context,
state.messages state.messages
) )
}.map { }.flatMap {
PostContentInfo.fromMessage(it, state.messages.size) PostContentInfo.fromMessage(it)
} }
RegistrationState.InProcess( RegistrationState.InProcess(
@@ -121,25 +113,9 @@ object Plugin : Plugin {
} }
onContentMessage( onContentMessage(
initialFilter = { it.chat.id == config.sourceChatId && it.mediaGroupMessageOrNull() ?.mediaGroupId == null && !FirstSourceIsCommandsFilter(it) } initialFilter = { it.chat.id == config.sourceChatId && !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(

82
publish.gradle Normal file
View File

@@ -0,0 +1,82 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
classifier = '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)
}
}
}

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

@@ -0,0 +1,164 @@
package dev.inmo.plaguposter.ratings.source.buttons
import com.soywiz.klock.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,15 @@
package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.cache.KeyValueCacheRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.cached
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.cached(kvCache, scope)

View File

@@ -0,0 +1,15 @@
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.cached
import dev.inmo.plaguposter.common.ShortMessageInfo
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.cached(kvCache, scope)

View File

@@ -20,6 +20,8 @@ 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.plaguposter.ratings.utils.postsByRatings
@@ -34,6 +36,7 @@ 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.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.bold
import dev.inmo.tgbotapi.types.message.textsources.regular import dev.inmo.tgbotapi.types.message.textsources.regular
@@ -64,8 +67,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 {
@@ -98,6 +122,7 @@ 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(
@@ -137,7 +162,7 @@ object Plugin : Plugin {
} }
} }
postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { postId -> ratingsRepo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { postId ->
detachPoll(postId) detachPoll(postId)
} }
@@ -225,13 +250,23 @@ object Plugin : Plugin {
+ "" + bold("% 3.1f".format(it.first.double)) + ": " + bold(it.second.size.toString()) + "\n" + "" + bold("% 3.1f".format(it.first.double)) + ": " + bold(it.second.size.toString()) + "\n"
} }
} }
val keyboard = flatInlineKeyboard {
dataButton("Interactive mode", "ratings_interactive")
}
runCatchingSafely { runCatchingSafely {
edit(it, textSources) edit(it, textSources, replyMarkup = keyboard)
}.onFailure { _ -> }.onFailure { _ ->
reply(it, textSources) reply(it, textSources, replyMarkup = keyboard)
} }
} }
} }
includeRootNavigationButtonsHandler(setOf(chatConfig.sourceChatId), ratingsRepo, postsRepo)
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id == chatConfig.sourceChatId }) {
edit(
it.message,
ratingsRepo.buildRootButtons()
)
}
koin.getOrNull<InlineTemplatesRepo>() ?.apply { koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate( addTemplate(

View File

@@ -4,8 +4,11 @@ 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.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
import org.jetbrains.exposed.sql.statements.* import org.jetbrains.exposed.sql.statements.*
class ExposedPollsToMessagesInfoRepo( class ExposedPollsToMessagesInfoRepo(
@@ -16,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)
) )
} }
@@ -27,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)
) )
@@ -37,6 +41,7 @@ class ExposedPollsToMessagesInfoRepo(
override fun update(k: PollIdentifier, v: ShortMessageInfo, it: UpdateBuilder<Int>) { 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
} }

View File

@@ -12,8 +12,8 @@ class ExposedPollsToPostsIdsRepo(
) : 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

View File

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

View File

@@ -1,8 +1,10 @@
package dev.inmo.plaguposter.ratings 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,6 +1,7 @@
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
@@ -17,13 +18,17 @@ 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)
init {
initTable()
}
override fun update(k: PostId, v: Rating, it: UpdateBuilder<Int>) { override fun update(k: PostId, v: Rating, it: UpdateBuilder<Int>) {
it[ratingsColumn] = v.double it[ratingsColumn] = v.double
} }

View File

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

View File

@@ -15,6 +15,9 @@ 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")

View File

@@ -7,15 +7,19 @@
}, },
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678", "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [ "plugins": [
"dev.inmo.plagubot.plugins.inline.queries.Plugin",
"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.triggers.selector_with_timer.Plugin",
"dev.inmo.plagubot.plugins.inline.queries.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin", "dev.inmo.plaguposter.triggers.command.Plugin",
"dev.inmo.plaguposter.posts.panel.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"
], ],
"posts": { "posts": {
"chats": { "chats": {

View File

@@ -14,7 +14,7 @@ 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=docker.io/insanusmokrassar
assert_success ../gradlew build assert_success ../gradlew build

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

@@ -12,6 +12,9 @@ String[] includes = [
":triggers:command", ":triggers:command",
":triggers:selector_with_timer", ":triggers:selector_with_timer",
":triggers:selector_with_scheduling", ":triggers:selector_with_scheduling",
":triggers:timer",
":triggers:timer:disablers:ratings",
":triggers:timer:disablers:autoposts",
":inlines", ":inlines",
// ":settings", // ":settings",
":runner" ":runner"

View File

@@ -1,6 +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"

View File

@@ -1,12 +1,9 @@
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.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
@@ -16,14 +13,10 @@ 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,7 +69,14 @@ 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 { _ ->
reply(
it,
"Unable to find any post related to the message in reply"
)
return@onCommand
}
} ?: selector ?.take(1) ?.firstOrNull() } ?: selector ?.take(1) ?.firstOrNull()
if (postId == null) { if (postId == null) {
reply( reply(

View File

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

View File

@@ -34,9 +34,12 @@ 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 -> koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { dateTime ->
publisher.publish(postId) selector.take(now = dateTime).forEach { postId ->
if (filters.all { it.check(postId, dateTime) }) {
publisher.publish(postId)
}
} }
} }
} }

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 com.soywiz.klock.DateFormat
import com.soywiz.klock.DateTime
import com.soywiz.klock.DateTimeTz
import com.soywiz.klock.Month
import com.soywiz.klock.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 com.soywiz.klock.DateTime
import com.soywiz.klock.minutes
fun nearestAvailableTimerTime() = (DateTime.now() + 1.minutes).copyDayOfMonth(
milliseconds = 0,
seconds = 0
)

View File

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

View File

@@ -0,0 +1,28 @@
package dev.inmo.plaguposter.triggers.timer
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.models.RegisteredPost
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton
class TimerPanelButton(
private val timersRepo: TimersRepo
) : PanelButtonBuilder {
override val weight: Int
get() = 0
override suspend fun buildButton(post: RegisteredPost): InlineKeyboardButton? {
val publishingTime = timersRepo.get(post.id)
return CallbackDataInlineKeyboardButton(
"${ if (publishingTime == null) UnsuccessfulSymbol else SuccessfulSymbol }",
"$timerSetPrefix ${post.id}"
)
}
companion object {
const val timerSetPrefix = "timer_set_init"
}
}

View File

@@ -0,0 +1,57 @@
package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.sending.PostPublisher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class TimersHandler(
private val timersRepo: TimersRepo,
private val publisher: PostPublisher,
private val scope: CoroutineScope
) {
private var currentPostAndJob: Pair<PostId, Job>? = null
private val currentJobMutex = Mutex()
init {
(flowOf(Unit) + timersRepo.onNewValue + timersRepo.onValueRemoved).subscribeSafelyWithoutExceptions(scope) {
refreshPublishingJob()
}
}
private suspend fun refreshPublishingJob() {
val minimal = timersRepo.getMinimalDateTimePost()
currentJobMutex.withLock {
if (minimal ?.first == currentPostAndJob ?.first) {
return@withLock
}
currentPostAndJob ?.second ?.cancel()
currentPostAndJob = minimal ?.let { (postId, dateTime) ->
postId to scope.launchSafelyWithoutExceptions {
val now = DateTime.now()
val span = dateTime - now
delay(span.millisecondsLong)
publisher.publish(postId)
timersRepo.unset(postId)
refreshPublishingJob()
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId
interface TimersRepo : KeyValueRepo<PostId, DateTime> {
suspend fun getMinimalDateTimePost(): Pair<PostId, DateTime>?
}

View File

@@ -0,0 +1,80 @@
package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds
import dev.inmo.micro_utils.repos.set
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
import dev.inmo.plaguposter.triggers.timer.repo.ExposedTimersRepo
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
import org.koin.dsl.binds
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
single { ExposedTimersRepo(get(), get(), get()) } binds arrayOf(TimersRepo::class)
single(createdAtStart = true) { TimersHandler(get(), get(), get()) }
singleWithRandomQualifierAndBinds { TimerPanelButton(get()) }
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val timersRepo = koin.get<TimersRepo>()
val chatsConfig = koin.get<ChatConfig>()
val panelApi = koin.get<PanelButtonsAPI>()
val scope = koin.get<CoroutineScope>()
with(ButtonsBuilder) {
includeKeyboardHandling(timersRepo) { postId, dateTime ->
timersRepo.set(postId, dateTime)
true
}
}
timersRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) {
panelApi.forceRefresh(it.first)
}
timersRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) {
panelApi.forceRefresh(it)
}
onMessageDataCallbackQuery(
Regex("${TimerPanelButton.timerSetPrefix} [^\\s]+"),
initialFilter = {
chatsConfig.check(it.message.chat.id)
}
) {
val (_, postIdRaw) = it.data.split(" ")
val postId = PostId(postIdRaw)
val now = nearestAvailableTimerTime()
val exists = timersRepo.get(postId)
val textSources = ButtonsBuilder.buildTimerTextSources(now, exists)
val buttons = ButtonsBuilder.buildTimerButtons(
postId,
now.local,
exists != null
)
reply(
it.message,
textSources,
replyMarkup = buttons
)
answer(it)
}
}
}

View File

@@ -0,0 +1,62 @@
package dev.inmo.plaguposter.triggers.timer.repo
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.common.firstNotNull
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.pagination.paginate
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.triggers.timer.TimersRepo
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ISqlExpressionBuilder
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.statements.UpdateBuilder
import org.jetbrains.exposed.sql.transactions.transaction
class ExposedTimersRepo(
database: Database,
postsRepo: PostsRepo,
scope: CoroutineScope
) : TimersRepo, AbstractExposedKeyValueRepo<PostId, DateTime>(
database,
"timers"
) {
override val keyColumn = text("post_id")
private val dateTimeColumn = long("date_time")
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
override val selectByValue: ISqlExpressionBuilder.(DateTime) -> Op<Boolean> = { dateTimeColumn.eq(it.unixMillisLong) }
override val ResultRow.asKey: PostId
get() = PostId(get(keyColumn))
override val ResultRow.asObject: DateTime
get() = DateTime(get(dateTimeColumn))
val postsRepoListeningJob = postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(scope) {
unset(it)
}
init {
initTable()
}
override fun update(k: PostId, v: DateTime, it: UpdateBuilder<Int>) {
it[dateTimeColumn] = v.unixMillisLong
}
override fun insertKey(k: PostId, v: DateTime, it: InsertStatement<Number>) {
it[keyColumn] = k.string
}
override suspend fun getMinimalDateTimePost(): Pair<PostId, DateTime>? = transaction(database) {
selectAll().orderBy(dateTimeColumn).limit(1).firstOrNull() ?.let {
PostId(it[keyColumn]) to DateTime(it[dateTimeColumn])
}
}
}

View File

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