37 Commits

Author SHA1 Message Date
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
59 changed files with 1353 additions and 62 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 }}

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

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

@@ -23,4 +23,4 @@ allprojects {
}
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 {
api libs.tgbotapi
api libs.microutils.repos.common
api libs.microutils.repos.cache
api libs.kslog
api libs.microutils.koin
}
}
jvmMain {

View File

@@ -1,15 +1,27 @@
package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ChatConfig(
@SerialName("targetChat")
val targetChatId: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
val targetChatId: IdChatIdentifier,
@SerialName("sourceChat")
val sourceChatId: ChatId,
@Serializable(FullChatIdentifierSerializer::class)
val sourceChatId: IdChatIdentifier,
@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

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

@@ -6,10 +6,24 @@ 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>()

View File

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

31
github_release.gradle Normal file
View File

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

View File

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

View File

@@ -1,15 +1,15 @@
[versions]
kotlin = "1.7.21"
kotlin = "1.7.22"
kotlin-serialization = "1.4.1"
plagubot = "3.1.3"
tgbotapi = "4.1.2"
microutils = "0.14.2"
kslog = "0.5.3"
krontab = "0.8.3"
tgbotapi-libraries = "0.6.3"
plagubot-plugins = "0.6.1"
plagubot = "3.5.0"
tgbotapi = "5.2.1"
microutils = "0.16.11"
kslog = "0.5.4"
krontab = "0.8.5"
tgbotapi-libraries = "0.8.2"
plagubot-plugins = "0.8.1"
dokka = "1.7.20"

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.getAllDistinct
import dev.inmo.micro_utils.repos.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.id
import dev.inmo.micro_utils.repos.set
@@ -12,6 +15,7 @@ import dev.inmo.micro_utils.repos.value
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.common.useCache
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.repos.PostsMessages
import dev.inmo.plaguposter.posts.repo.PostsRepo
@@ -93,7 +97,12 @@ object Plugin : Plugin {
val chatsConfig = koin.get<ChatConfig>()
val config = koin.getOrNull<Config>() ?: Config()
val api = koin.get<PanelButtonsAPI>()
val postsMessages = PostsMessages(koin.get(), koin.get())
val basePostsMessages = PostsMessages(koin.get(), koin.get())
val postsMessages = if (koin.useCache) {
basePostsMessages.cached(FullKVCache(), koin.get())
} else {
basePostsMessages
}
postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) {
val firstContent = it.content.first()

View File

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

View File

@@ -10,4 +10,5 @@ import dev.inmo.tgbotapi.types.MessageIdentifier
interface ReadPostsRepo : ReadCRUDRepo<RegisteredPost, PostId> {
suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId?
suspend fun getPostCreationTime(postId: PostId): DateTime?
suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo?
}

View File

@@ -17,8 +17,8 @@ import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class PostPublisher(
private val bot: TelegramBot,
private val postsRepo: PostsRepo,
private val cachingChatId: ChatId,
private val targetChatId: ChatId,
private val cachingChatId: IdChatIdentifier,
private val targetChatId: IdChatIdentifier,
private val deleteAfterPosting: Boolean = true
) {
suspend fun publish(postId: PostId) {

View File

@@ -4,6 +4,7 @@ import dev.inmo.kslog.common.logger
import dev.inmo.kslog.common.w
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithBinds
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.SuccessfulSymbol
@@ -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.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.common.useCache
import dev.inmo.plaguposter.posts.cached.CachedPostsRepo
import dev.inmo.plaguposter.posts.repo.*
import dev.inmo.plaguposter.posts.sending.PostPublisher
import dev.inmo.tgbotapi.extensions.api.delete
@@ -44,11 +47,16 @@ object Plugin : Plugin {
}
single { get<Json>().decodeFromJsonElement(Config.serializer(), configJson) }
single { get<Config>().chats }
single { ExposedPostsRepo(database) } binds arrayOf(
PostsRepo::class,
ReadPostsRepo::class,
WritePostsRepo::class,
)
single { ExposedPostsRepo(database) }
singleWithBinds<PostsRepo> {
val base = get<ExposedPostsRepo>()
if (useCache) {
CachedPostsRepo(base, get())
} else {
base
}
}
single {
val config = get<Config>()
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.targetChatId, config.deleteAfterPublishing)

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

@@ -38,9 +38,11 @@ class ExposedPostsRepo(
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) }
override val selectByIds: ISqlExpressionBuilder.(List<PostId>) -> Op<Boolean> = { idColumn.inList(it.map { it.string }) }
override val ResultRow.asId: PostId
get() = PostId(get(idColumn))
override val ResultRow.asObject: RegisteredPost
get() {
val id = PostId(get(idColumn))
val id = asId
return RegisteredPost(
id,
DateTime(get(createdColumn)),
@@ -163,4 +165,10 @@ class ExposedPostsRepo(
override suspend fun getPostCreationTime(postId: PostId): DateTime? = transaction(database) {
select { selectById(postId) }.limit(1).firstOrNull() ?.get(createdColumn) ?.let(::DateTime)
}
override suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo? = transaction(database) {
with(contentRepo) {
select { postIdColumn.eq(postId.string) }.limit(1).firstOrNull() ?.asObject
}
}
}

View File

@@ -65,7 +65,7 @@ object Plugin : Plugin {
val newMessagesInfo = firstOf {
add {
listOf(
waitContentMessage().filter {
waitAnyContentMessage().filter {
it.chat.id == state.context && it.content.textContentOrNull() ?.text != "/finish_post"
}.take(1).first()
)

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.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.ratings.source.buttons.buildRootButtons
import dev.inmo.plaguposter.ratings.source.buttons.includeRootNavigationButtonsHandler
import dev.inmo.plaguposter.ratings.source.models.*
import dev.inmo.plaguposter.ratings.source.repos.*
import dev.inmo.plaguposter.ratings.utils.postsByRatings
@@ -34,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.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.message.textsources.bold
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"))
}
single<RatingsVariants>(ratingVariantsQualifier) { get<Config>().variants }
single<PollsToPostsIdsRepo> { ExposedPollsToPostsIdsRepo(database) }
single<PollsToMessagesInfoRepo> { ExposedPollsToMessagesInfoRepo(database) }
single { ExposedPollsToPostsIdsRepo(database) }
single<PollsToPostsIdsRepo> {
val base = get<ExposedPollsToPostsIdsRepo>()
if (useCache) {
CachedPollsToPostsIdsRepo(base, get())
} else {
base
}
}
single { ExposedPollsToMessagesInfoRepo(database) }
single<PollsToMessagesInfoRepo> {
val base = get<ExposedPollsToMessagesInfoRepo>()
if (useCache) {
CachedPollsToMessagesInfoRepo(base, get())
} else {
base
}
}
single<VariantTransformer> {
val ratingsSettings = get<RatingsVariants>(ratingVariantsQualifier)
VariantTransformer {
@@ -98,6 +122,7 @@ object Plugin : Plugin {
}
val post = postsRepo.getById(postId) ?: return false
ratingsRepo.set(postId, Rating(0.0))
for (content in post.content) {
runCatchingSafely {
val sent = send(
@@ -137,7 +162,7 @@ object Plugin : Plugin {
}
}
postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { postId ->
ratingsRepo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { postId ->
detachPoll(postId)
}
@@ -225,13 +250,23 @@ object Plugin : Plugin {
+ "" + bold("% 3.1f".format(it.first.double)) + ": " + bold(it.second.size.toString()) + "\n"
}
}
val keyboard = flatInlineKeyboard {
dataButton("Interactive mode", "ratings_interactive")
}
runCatchingSafely {
edit(it, textSources)
edit(it, textSources, replyMarkup = keyboard)
}.onFailure { _ ->
reply(it, textSources)
reply(it, textSources, replyMarkup = keyboard)
}
}
}
includeRootNavigationButtonsHandler(setOf(chatConfig.sourceChatId), ratingsRepo, postsRepo)
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id == chatConfig.sourceChatId }) {
edit(
it.message,
ratingsRepo.buildRootButtons()
)
}
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package dev.inmo.plaguposter.ratings.exposed
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating
@@ -24,6 +25,10 @@ class ExposedRatingsRepo (
override val ResultRow.asObject: Rating
get() = get(ratingsColumn).let(::Rating)
init {
initTable()
}
override fun update(k: PostId, v: Rating, it: UpdateBuilder<Int>) {
it[ratingsColumn] = v.double
}

View File

@@ -15,6 +15,9 @@ dependencies {
api project(":plaguposter.posts_registrar")
api project(":plaguposter.triggers.command")
api project(":plaguposter.triggers.selector_with_timer")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.triggers.timer.disablers.autoposts")
api project(":plaguposter.triggers.timer.disablers.ratings")
api project(":plaguposter.ratings")
api project(":plaguposter.ratings.source")
api project(":plaguposter.ratings.selector")

View File

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

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=" | grep -e "[0-9.]*" -o`"
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:selector_with_timer",
":triggers:selector_with_scheduling",
":triggers:timer",
":triggers:timer:disablers:ratings",
":triggers:timer:disablers:autoposts",
":inlines",
// ":settings",
":runner"

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) {
val publisher = koin.get<PostPublisher>()
val selector = koin.get<Selector>()
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) {
selector.take(now = it).forEach { postId ->
publisher.publish(postId)
val filters = koin.getAll<AutopostFilter>().distinct()
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { dateTime ->
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"/>