17 Commits
0.0.5 ... 0.0.7

26 changed files with 463 additions and 109 deletions

View File

@@ -18,6 +18,7 @@ allprojects {
mavenLocal()
mavenCentral()
google()
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
}
}

View File

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

View File

@@ -0,0 +1,20 @@
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 org.koin.core.Koin
object CommonPlugin : Plugin {
private val Log = logger
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

@@ -10,5 +10,5 @@ android.enableJetifier=true
# Project data
group=dev.inmo
version=0.0.5
android_code_version=5
version=0.0.7
android_code_version=7

View File

@@ -1,15 +1,17 @@
[versions]
kotlin = "1.7.20"
kotlin = "1.7.22"
kotlin-serialization = "1.4.1"
plagubot = "2.4.0"
tgbotapi = "3.3.0"
microutils = "0.13.1"
kslog = "0.5.2"
krontab = "0.8.1"
tgbotapi-libraries = "0.5.6"
plagubot-plugins = "0.5.0"
plagubot = "3.2.0"
tgbotapi = "4.2.1"
microutils = "0.16.0"
kslog = "0.5.4"
krontab = "0.8.4"
tgbotapi-libraries = "0.6.5"
plagubot-plugins = "0.6.4"
dokka = "1.7.20"
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-bot = { module = "dev.inmo:plagubot.bot", version.ref = "plagubot" }
plagubot-plugins-inline-queries = { module = "dev.inmo:plagubot.plugins.inline.queries", version.ref = "plagubot-plugins" }
plagubot-plugins-inline-buttons = { module = "dev.inmo:plagubot.plugins.inline.buttons", version.ref = "plagubot-plugins" }
microutils-repos-common = { module = "dev.inmo:micro_utils.repos.common", version.ref = "microutils" }
microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" }
microutils-repos-cache = { module = "dev.inmo:micro_utils.repos.cache", version.ref = "microutils" }
@@ -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-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
kotlin-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "kotlin" }
kotlin-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
[plugins]

View File

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

View File

@@ -4,12 +4,20 @@ import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.w
import dev.inmo.plagubot.Plugin
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.module.Module
private val actualPlugin = dev.inmo.plagubot.plugins.inline.queries.Plugin
object Plugin : Plugin by actualPlugin {
private val log = TagLogger("InlinePlugin")
override fun Module.setupDI(database: Database, params: JsonObject) {
single { actualPlugin }
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
log.w {
"Built-in inline plugin has been deprecated. Use \"${actualPlugin::class.qualifiedName}\" instead"

View File

@@ -14,5 +14,10 @@ kotlin {
api libs.microutils.koin
}
}
jvmMain {
dependencies {
api libs.plagubot.plugins.inline.queries
}
}
}
}

View File

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

View File

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

View File

@@ -5,9 +5,13 @@ import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.getAllDistinct
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.micro_utils.repos.id
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.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.repos.PostsMessages
import dev.inmo.plaguposter.posts.repo.PostsRepo
@@ -15,18 +19,23 @@ import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.edit.text.editMessageText
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.command
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.ParseMode
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.italic
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
@@ -69,9 +78,13 @@ object Plugin : Plugin {
}
)
PanelButtonsAPI(
getAllDistinct<PanelButtonBuilder>() + builtInButtons,
emptyMap(),
config.rootButtonText
)
).apply {
(getAllDistinct<PanelButtonBuilder>() + builtInButtons).forEach {
add(it)
}
}
}
}
@@ -108,7 +121,7 @@ object Plugin : Plugin {
suspend fun refreshPostMessage(
postId: PostId,
chatId: ChatId,
chatId: IdChatIdentifier,
messageId: MessageIdentifier
) {
val post = postsRepo.getById(postId) ?: return
@@ -183,5 +196,59 @@ object Plugin : Plugin {
val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions
refreshPostMessage(it, chatId, messageId)
}
command("panel") {
val reply = it.replyTo
if (reply == null) {
runCatchingSafely {
edit(
it,
it.content.textSources + buildEntities {
+"${UnsuccessfulSymbol}\n" + bold("Result") + ": " + italic("You should reply post content to trigger panel retrieving")
}
)
}.onFailure { _ ->
reply(
it,
buildEntities {
bold("Result") + ": " + italic("You should reply post content to trigger panel retrieving")
}
)
}
return@command
}
val postId = postsRepo.getIdByChatAndMessage(reply.chat.id, reply.messageId)
if (postId == null) {
runCatchingSafely {
edit(
it,
it.content.textSources + buildEntities {
+"${UnsuccessfulSymbol}\n" + bold("Result") + ": " + italic("Unable to find post related to replied message")
}
)
}.onFailure { _ ->
reply(
it,
buildEntities {
bold("Result") + ": " + italic("Unable to find post related to replied message")
}
)
}
return@command
}
postsMessages.get(postId) ?.let {
runCatchingSafely { delete(it.id, it.value) }
postsMessages.unset(postId)
}
refreshPostMessage(postId, it.chat.id, it.messageId)
postsMessages.set(postId, it.chat.id to it.messageId)
}
}
}

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class PostPublisher(
private val bot: TelegramBot,
@@ -37,14 +38,26 @@ class PostPublisher(
sortedMessagesContents.forEach { (_, contents) ->
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
}
val resultContents = contents.mapNotNull {
it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null)
}.sortedBy { it.first }.mapNotNull { (_, it) ->
it.withContentOrNull<MediaGroupContent>() ?: null.also { _ ->
bot.copyMessage(targetChatId, it)
}.sortedBy { it.first }.mapNotNull { (_, forwardedMessage) ->
forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ ->
bot.copyMessage(targetChatId, forwardedMessage)
}
}
resultContents.singleOrNull() ?.also {

View File

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

View File

@@ -3,15 +3,19 @@ package dev.inmo.plaguposter.posts.exposed
import com.benasher44.uuid.uuid4
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.transactions.transaction
@@ -32,11 +36,13 @@ class ExposedPostsRepo(
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
override val selectById: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) }
override val selectByIds: SqlExpressionBuilder.(List<PostId>) -> Op<Boolean> = { idColumn.inList(it.map { it.string }) }
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { idColumn.eq(it.string) }
override val selectByIds: ISqlExpressionBuilder.(List<PostId>) -> Op<Boolean> = { idColumn.inList(it.map { it.string }) }
override val ResultRow.asId: PostId
get() = PostId(get(idColumn))
override val ResultRow.asObject: RegisteredPost
get() {
val id = PostId(get(idColumn))
val id = asId
return RegisteredPost(
id,
DateTime(get(createdColumn)),
@@ -75,17 +81,21 @@ class ExposedPostsRepo(
return id
}
override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {
id ?: error("Unable to find post id in update")
with(contentRepo) {
deleteWhere { postIdColumn.eq(id.string) }
value.content.forEach { contentInfo ->
insert {
it[postIdColumn] = id.string
it[chatIdColumn] = contentInfo.chatId.chatId
it[messageIdColumn] = contentInfo.messageId
it[groupColumn] = contentInfo.group
it[orderColumn] = contentInfo.order
override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {}
private fun updateContent(post: RegisteredPost) {
transaction(database) {
with(contentRepo) {
deleteWhere { postIdColumn.eq(post.id.string) }
post.content.forEach { contentInfo ->
insert {
it[postIdColumn] = post.id.string
it[chatIdColumn] = contentInfo.chatId.chatId
it[threadIdColumn] = contentInfo.chatId.threadId
it[messageIdColumn] = contentInfo.messageId
it[groupColumn] = contentInfo.group
it[orderColumn] = contentInfo.order
}
}
}
}
@@ -96,6 +106,22 @@ class ExposedPostsRepo(
it[createdColumn] = DateTime.now().unixMillis
}
override suspend fun onAfterCreate(values: List<Pair<NewPost, RegisteredPost>>): List<RegisteredPost> {
return values.map {
val actual = it.second.copy(content = it.first.content)
updateContent(actual)
actual
}
}
override suspend fun onAfterUpdate(value: List<UpdatedValuePair<NewPost, RegisteredPost>>): List<RegisteredPost> {
return value.map {
val actual = it.second.copy(content = it.first.content)
updateContent(actual)
actual
}
}
override suspend fun deleteById(ids: List<PostId>) {
onBeforeDelete(ids)
val posts = ids.mapNotNull {
@@ -104,7 +130,7 @@ class ExposedPostsRepo(
val existsIds = posts.keys.toList()
transaction(db = database) {
val deleted = deleteWhere(null, null) {
selectByIds(existsIds)
selectByIds(it, existsIds)
}
with(contentRepo) {
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) {
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)
}
}
@@ -135,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

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

View File

@@ -22,12 +22,12 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities
import dev.inmo.tgbotapi.extensions.utils.formatting.regular
import dev.inmo.tgbotapi.extensions.utils.mediaGroupMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import dev.inmo.tgbotapi.types.message.content.MessageContent
import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.flow.*
import kotlinx.serialization.Serializable
import org.koin.core.Koin
@@ -43,7 +43,7 @@ object Plugin : Plugin {
val messageToDelete = send(
state.context,
buildEntities {
dev.inmo.tgbotapi.utils.buildEntities {
if (state.messages.isNotEmpty()) {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
} else {
@@ -65,18 +65,11 @@ object Plugin : Plugin {
val newMessagesInfo = firstOf {
add {
listOf(
waitContentMessage(
includeMediaGroups = false
).filter {
waitContentMessage().filter {
it.chat.id == state.context && it.content.textContentOrNull() ?.text != "/finish_post"
}.take(1).first()
)
}
add {
waitMediaGroupMessages().filter {
it.first().chat.id == state.context
}.take(1).first()
}
add {
val finishPressed = waitMessageDataCallbackQuery().filter {
it.message.sameMessage(messageToDelete) && it.data == buttonUuid
@@ -95,8 +88,8 @@ object Plugin : Plugin {
state.context,
state.messages
)
}.map {
PostContentInfo.fromMessage(it, state.messages.size)
}.flatMap {
PostContentInfo.fromMessage(it)
}
RegistrationState.InProcess(
@@ -121,25 +114,9 @@ object Plugin : Plugin {
}
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))))
}
onMediaGroup(
initialFilter = { it.first().chat.id == config.sourceChatId }
) {
startChain(
RegistrationState.Finish(
it.first().chat.id,
it.map {
PostContentInfo.fromMessage(
it,
0
)
}
)
)
startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it)))
}
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(

View File

@@ -0,0 +1,160 @@
package dev.inmo.plaguposter.ratings.source.buttons
import com.soywiz.klock.DateFormat
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)
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).map {
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

@@ -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
@@ -225,13 +228,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

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

View File

@@ -12,8 +12,8 @@ class ExposedPollsToPostsIdsRepo(
) : PollsToPostsIdsRepo, AbstractExposedKeyValueRepo<PollIdentifier, PostId>(database, "polls_to_posts") {
override val keyColumn = text("poll_id")
val postIdColumn = text("postId")
override val selectById: SqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectByValue: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { postIdColumn.eq(it.string) }
override val selectById: ISqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
override val selectByValue: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { postIdColumn.eq(it.string) }
override val ResultRow.asKey: PollIdentifier
get() = get(keyColumn)
override val ResultRow.asObject: PostId

View File

@@ -17,8 +17,8 @@ class ExposedRatingsRepo (
) {
override val keyColumn = text("post_id")
val ratingsColumn = double("rating")
override val selectById: SqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
override val selectByValue: SqlExpressionBuilder.(Rating) -> Op<Boolean> = { ratingsColumn.eq(it.double) }
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
override val selectByValue: ISqlExpressionBuilder.(Rating) -> Op<Boolean> = { ratingsColumn.eq(it.double) }
override val ResultRow.asKey: PostId
get() = get(keyColumn).let(::PostId)
override val ResultRow.asObject: Rating

View File

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

View File

@@ -1,12 +1,9 @@
package dev.inmo.plaguposter.triggers.command
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.fsm.common.State
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
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.sending.PostPublisher
import dev.inmo.plaguposter.ratings.selector.Selector
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitTextMessage
import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.*
@@ -33,9 +26,7 @@ import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.textsources.regular
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
@@ -78,7 +69,14 @@ object Plugin : Plugin {
}
}
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()
if (postId == null) {
reply(