From 468e35a77f68f0a8821d90f0a8ffc5e06fb3f486 Mon Sep 17 00:00:00 2001 From: madhead Date: Tue, 18 May 2021 01:42:48 +0200 Subject: [PATCH 01/15] Redirect users to the PM chat for quizojis --- quizoji/build.gradle.kts | 17 ++++ .../bot/quizoji/QuizojiUpdateProcessor.kt | 28 ++++++ .../bot/quizoji/QuizojiUpdateProcessorTest.kt | 89 +++++++++++++++++++ runners/lambda/build.gradle.kts | 1 + .../bot/runners/lambda/config/pipeline.kt | 7 ++ settings.gradle.kts | 1 + 6 files changed, 143 insertions(+) create mode 100644 quizoji/build.gradle.kts create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt create mode 100644 quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt diff --git a/quizoji/build.gradle.kts b/quizoji/build.gradle.kts new file mode 100644 index 00000000..eca0b752 --- /dev/null +++ b/quizoji/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.core) + api(libs.tgbotapi.core) + implementation(libs.log4j.api) + implementation(libs.tgbotapi.extensions.api) + implementation(libs.tgbotapi.extensions.utils) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.mockk) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.log4j.core) +} diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt new file mode 100644 index 00000000..1317b5f4 --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt @@ -0,0 +1,28 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.core.UpdateProcessor +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery +import dev.inmo.tgbotapi.extensions.utils.asInlineQueryUpdate +import dev.inmo.tgbotapi.types.update.abstracts.Update +import org.apache.logging.log4j.LogManager + +class QuizojiUpdateProcessor( + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(QuizojiUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val inlineQuery = update.asInlineQueryUpdate()?.data ?: return + + if (inlineQuery.query.equals("quizoji", ignoreCase = true)) { + bot.answerInlineQuery( + inlineQuery = inlineQuery, + switchPmText = "Create new quizoji", + switchPmParameter = "quizoji", + ) + } + } +} diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt new file mode 100644 index 00000000..e0bc9dbf --- /dev/null +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt @@ -0,0 +1,89 @@ +package by.jprof.telegram.bot.quizoji + +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery +import dev.inmo.tgbotapi.types.InlineQueries.query.BaseInlineQuery +import dev.inmo.tgbotapi.types.update.InlineQueryUpdate +import dev.inmo.tgbotapi.types.update.MessageUpdate +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class QuizojiUpdateProcessorTest { + @MockK(relaxed = true) + private lateinit var bot: RequestsExecutor + + lateinit var sut: QuizojiUpdateProcessor + + @BeforeEach + fun setUp() { + sut = QuizojiUpdateProcessor( + bot = bot, + ) + } + + @Test + fun processNonInilineQuery() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1L, + data = mockk() + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processAlienInilineQuery() = runBlocking { + sut.process( + InlineQueryUpdate( + updateId = 1L, + data = BaseInlineQuery( + id = "1", + from = mockk(), + query = "alien", + offset = "", + ) + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processQuizojiInilineQuery() = runBlocking { + val inlineQuery = BaseInlineQuery( + id = "1", + from = mockk(), + query = "quizoji", + offset = "", + ) + + sut.process( + InlineQueryUpdate( + updateId = 1L, + data = inlineQuery + ) + ) + + coVerify(exactly = 1) { + bot.answerInlineQuery( + inlineQuery = inlineQuery, + switchPmText = "Create new quizoji", + switchPmParameter = "quizoji", + ) + } + + clearAllMocks() + } +} diff --git a/runners/lambda/build.gradle.kts b/runners/lambda/build.gradle.kts index 8767733b..632e611f 100644 --- a/runners/lambda/build.gradle.kts +++ b/runners/lambda/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(project.projects.jep) implementation(project.projects.youtube.dynamodb) implementation(project.projects.kotlin.dynamodb) + implementation(project.projects.quizoji) } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt index 2f839064..a8a02d9d 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt @@ -5,6 +5,7 @@ import by.jprof.telegram.bot.core.UpdateProcessor import by.jprof.telegram.bot.jep.JEPUpdateProcessor import by.jprof.telegram.bot.jep.JsoupJEPSummary import by.jprof.telegram.bot.kotlin.KotlinMentionsUpdateProcessor +import by.jprof.telegram.bot.quizoji.QuizojiUpdateProcessor import by.jprof.telegram.bot.youtube.YouTubeUpdateProcessor import org.koin.core.qualifier.named import org.koin.dsl.module @@ -37,4 +38,10 @@ val pipelineModule = module { bot = get(), ) } + + single(named("QuizojiUpdateProcessor")) { + QuizojiUpdateProcessor( + bot = get(), + ) + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 4d1ec3d6..cd45b46b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,4 +15,5 @@ include(":youtube") include(":youtube:dynamodb") include(":kotlin") include(":kotlin:dynamodb") +include(":quizoji") include(":runners:lambda") From 5139d2f8f422b31031b68b6783a92c553ac1dfe4 Mon Sep 17 00:00:00 2001 From: madhead Date: Fri, 25 Jun 2021 18:22:30 +0200 Subject: [PATCH 02/15] Handle /start quizoji --- ...t => QuizojiInlineQueryUpdateProcessor.kt} | 4 +- .../QuizojiStartCommandUpdateProcessor.kt | 34 ++++ ... QuizojiInlineQueryUpdateProcessorTest.kt} | 10 +- .../QuizojiStartCommandUpdateProcessorTest.kt | 182 ++++++++++++++++++ .../bot/runners/lambda/config/pipeline.kt | 13 +- 5 files changed, 233 insertions(+), 10 deletions(-) rename quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/{QuizojiUpdateProcessor.kt => QuizojiInlineQueryUpdateProcessor.kt} (85%) create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt rename quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/{QuizojiUpdateProcessorTest.kt => QuizojiInlineQueryUpdateProcessorTest.kt} (87%) create mode 100644 quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt similarity index 85% rename from quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt rename to quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt index 1317b5f4..27c0b05b 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessor.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt @@ -7,11 +7,11 @@ import dev.inmo.tgbotapi.extensions.utils.asInlineQueryUpdate import dev.inmo.tgbotapi.types.update.abstracts.Update import org.apache.logging.log4j.LogManager -class QuizojiUpdateProcessor( +class QuizojiInlineQueryUpdateProcessor( private val bot: RequestsExecutor, ) : UpdateProcessor { companion object { - private val logger = LogManager.getLogger(QuizojiUpdateProcessor::class.java)!! + private val logger = LogManager.getLogger(QuizojiInlineQueryUpdateProcessor::class.java)!! } override suspend fun process(update: Update) { diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt new file mode 100644 index 00000000..ec002fce --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt @@ -0,0 +1,34 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.core.UpdateProcessor +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPrivateChat +import dev.inmo.tgbotapi.extensions.utils.asPrivateContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.update.abstracts.Update +import org.apache.logging.log4j.LogManager + +class QuizojiStartCommandUpdateProcessor( + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(QuizojiStartCommandUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPrivateContentMessage() ?: return + val chat = message.chat.asPrivateChat() ?: return + val content = message.content.asTextContent() ?: return + + if (content.text != "/start quizoji") { + return + } + + bot.sendMessage( + chat = chat, + text = "Let's create a Quizoji! First, send me the message. It can be anything — a text, photo, video, even a sticker." + ) + } +} diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt similarity index 87% rename from quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt rename to quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt index e0bc9dbf..b0cd51f5 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt @@ -14,15 +14,15 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) -internal class QuizojiUpdateProcessorTest { +internal class QuizojiInlineQueryUpdateProcessorTest { @MockK(relaxed = true) private lateinit var bot: RequestsExecutor - lateinit var sut: QuizojiUpdateProcessor + lateinit var sut: QuizojiInlineQueryUpdateProcessor @BeforeEach fun setUp() { - sut = QuizojiUpdateProcessor( + sut = QuizojiInlineQueryUpdateProcessor( bot = bot, ) } @@ -47,7 +47,7 @@ internal class QuizojiUpdateProcessorTest { InlineQueryUpdate( updateId = 1L, data = BaseInlineQuery( - id = "1", + id = "QuizojiStartCommandUpdateProcessorTest", from = mockk(), query = "alien", offset = "", @@ -63,7 +63,7 @@ internal class QuizojiUpdateProcessorTest { @Test fun processQuizojiInilineQuery() = runBlocking { val inlineQuery = BaseInlineQuery( - id = "1", + id = "QuizojiStartCommandUpdateProcessorTest", from = mockk(), query = "quizoji", offset = "", diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt new file mode 100644 index 00000000..56791381 --- /dev/null +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt @@ -0,0 +1,182 @@ +package by.jprof.telegram.bot.quizoji + +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.types.Bot +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.chat.PrivateChatImpl +import dev.inmo.tgbotapi.types.chat.abstracts.PrivateChat +import dev.inmo.tgbotapi.types.message.PrivateContentMessageImpl +import dev.inmo.tgbotapi.types.message.abstracts.ChannelContentMessage +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.media.AudioContent +import dev.inmo.tgbotapi.types.update.MessageUpdate +import dev.inmo.tgbotapi.types.update.PollUpdate +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class QuizojiStartCommandUpdateProcessorTest { + @MockK(relaxed = true) + private lateinit var bot: RequestsExecutor + + lateinit var sut: QuizojiStartCommandUpdateProcessor + + @BeforeEach + fun setUp() { + sut = QuizojiStartCommandUpdateProcessor( + bot = bot, + ) + } + + @Test + fun processNonMessageUpdate() = runBlocking { + sut.process( + PollUpdate( + updateId = 1, + data = mockk() + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateContentMessage() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = mockk>() + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateChat() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = mockk(), + content = mockk(), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonTextContentMessage() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = mockk(), + content = mockk(), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processWrongCommand() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = mockk(), + content = TextContent( + text = "/start doing your morning exercise" + ), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun process() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = chat, + content = TextContent( + text = "/start quizoji" + ), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Let's create a Quizoji! First, send me the message. It can be anything — a text, photo, video, even a sticker." + ) + } + + clearAllMocks() + } +} diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt index a8a02d9d..1794f340 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt @@ -5,7 +5,8 @@ import by.jprof.telegram.bot.core.UpdateProcessor import by.jprof.telegram.bot.jep.JEPUpdateProcessor import by.jprof.telegram.bot.jep.JsoupJEPSummary import by.jprof.telegram.bot.kotlin.KotlinMentionsUpdateProcessor -import by.jprof.telegram.bot.quizoji.QuizojiUpdateProcessor +import by.jprof.telegram.bot.quizoji.QuizojiInlineQueryUpdateProcessor +import by.jprof.telegram.bot.quizoji.QuizojiStartCommandUpdateProcessor import by.jprof.telegram.bot.youtube.YouTubeUpdateProcessor import org.koin.core.qualifier.named import org.koin.dsl.module @@ -39,8 +40,14 @@ val pipelineModule = module { ) } - single(named("QuizojiUpdateProcessor")) { - QuizojiUpdateProcessor( + single(named("QuizojiInlineQueryUpdateProcessor")) { + QuizojiInlineQueryUpdateProcessor( + bot = get(), + ) + } + + single(named("QuizojiStartCommandUpdateProcessor")) { + QuizojiStartCommandUpdateProcessor( bot = get(), ) } From 5878cc779e7ee6bdc011259ae28fd901aa8fb9d0 Mon Sep 17 00:00:00 2001 From: madhead Date: Sat, 26 Jun 2021 00:41:58 +0200 Subject: [PATCH 03/15] Model for dialogs state --- build.gradle.kts | 1 + dialogs/build.gradle.kts | 8 ++++++++ .../bot/dialogs/dao/DialogStateDAO.kt | 11 ++++++++++ .../telegram/bot/dialogs/model/DialogState.kt | 20 +++++++++++++++++++ .../model/quizoji/WaitingForQuestion.kt | 12 +++++++++++ gradle/libs.versions.toml | 2 ++ settings.gradle.kts | 1 + 7 files changed, 55 insertions(+) create mode 100644 dialogs/build.gradle.kts create mode 100644 dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt create mode 100644 dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt create mode 100644 dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForQuestion.kt diff --git a/build.gradle.kts b/build.gradle.kts index 34852961..e0c36a02 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm").version("1.4.32").apply(false) + kotlin("plugin.serialization").version("1.4.32").apply(false) id("com.github.johnrengelman.shadow").version("6.1.0").apply(false) } diff --git a/dialogs/build.gradle.kts b/dialogs/build.gradle.kts new file mode 100644 index 00000000..d6aa50b9 --- /dev/null +++ b/dialogs/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") +} + +dependencies { + implementation(libs.kotlinx.serialization.core) +} diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt new file mode 100644 index 00000000..c85fa622 --- /dev/null +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.dialogs.dao + +import by.jprof.telegram.bot.dialogs.model.DialogState + +interface DialogStateDAO { + suspend fun get(chatId: Long, userId: Long): DialogState? + + suspend fun save(entity: DialogState) + + suspend fun delete(chatId: Long, userId: Long) +} diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt new file mode 100644 index 00000000..766c44d1 --- /dev/null +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt @@ -0,0 +1,20 @@ +package by.jprof.telegram.bot.dialogs.model + +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass + +interface DialogState { + companion object { + val serializers = SerializersModule { + polymorphic(DialogState::class) { + subclass(WaitingForQuestion::class) + } + } + } + + val chatId: Long + + val userId: Long +} diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForQuestion.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForQuestion.kt new file mode 100644 index 00000000..3bbdf095 --- /dev/null +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForQuestion.kt @@ -0,0 +1,12 @@ +package by.jprof.telegram.bot.dialogs.model.quizoji + +import by.jprof.telegram.bot.dialogs.model.DialogState +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("WaitingForQuestion") +data class WaitingForQuestion( + override val chatId: Long, + override val userId: Long +) : DialogState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c71cb087..8c920fa0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ awssdk = "2.16.43" koin = "3.0.1" +kotlinx-serialization = "1.2.1" jackson = "2.12.3" tgbotapi = "0.33.3" @@ -34,6 +35,7 @@ dynamodb = { group = "software.amazon.awssdk", name = "dynamodb", version.ref = koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } tgbotapi-core = { group = "dev.inmo", name = "tgbotapi.core", version.ref = "tgbotapi" } diff --git a/settings.gradle.kts b/settings.gradle.kts index cd45b46b..bd4957a8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ include(":votes") include(":votes:dynamodb") include(":votes:tgbotapi-extensions") include(":votes:voting-processor") +include(":dialogs") include(":core") include(":jep") include(":youtube") From b0dfa901210128947bcbef9f1fda7a42e77c60ad Mon Sep 17 00:00:00 2001 From: madhead Date: Sat, 26 Jun 2021 00:51:47 +0200 Subject: [PATCH 04/15] Dialogs state: DynamoDB implementation --- .deploy/lambda/lib/JProfByBotStack.ts | 8 ++ .github/workflows/default.yml | 1 + dialogs/dynamodb/build.gradle.kts | 29 +++++++ .../dialogs/dynamodb/dao/DialogStateDAO.kt | 66 ++++++++++++++++ .../dynamodb/dao/DialogStateDAOTest.kt | 77 +++++++++++++++++++ .../dialogs/dynamodb/dao/DialogStateTest.kt | 38 +++++++++ .../test/resources/dialog-states.items.json | 19 +++++ .../test/resources/dialog-states.table.json | 27 +++++++ dialogs/dynamodb/src/test/resources/seed.sh | 8 ++ .../bot/dialogs/dao/DialogStateDAO.kt | 2 +- gradle/libs.versions.toml | 1 + .../telegram/bot/runners/lambda/config/env.kt | 2 + settings.gradle.kts | 1 + 13 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 dialogs/dynamodb/build.gradle.kts create mode 100644 dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt create mode 100644 dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAOTest.kt create mode 100644 dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt create mode 100644 dialogs/dynamodb/src/test/resources/dialog-states.items.json create mode 100644 dialogs/dynamodb/src/test/resources/dialog-states.table.json create mode 100755 dialogs/dynamodb/src/test/resources/seed.sh diff --git a/.deploy/lambda/lib/JProfByBotStack.ts b/.deploy/lambda/lib/JProfByBotStack.ts index 9f9aab0b..e8c4b023 100644 --- a/.deploy/lambda/lib/JProfByBotStack.ts +++ b/.deploy/lambda/lib/JProfByBotStack.ts @@ -25,6 +25,12 @@ export class JProfByBotStack extends cdk.Stack { partitionKey: { name: 'chat', type: dynamodb.AttributeType.NUMBER }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, }); + const dialogStatesTable = new dynamodb.Table(this, 'jprof-by-bot-table-dialog-states', { + tableName: 'jprof-by-bot-table-dialog-states', + partitionKey: { name: 'userId', type: dynamodb.AttributeType.NUMBER }, + sortKey: { name: 'chatId', type: dynamodb.AttributeType.NUMBER }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + }); const layerLibGL = new lambda.LayerVersion(this, 'jprof-by-bot-lambda-layer-libGL', { code: lambda.Code.fromAsset('layers/libGL.zip'), compatibleRuntimes: [lambda.Runtime.JAVA_11], @@ -50,6 +56,7 @@ export class JProfByBotStack extends cdk.Stack { 'TABLE_VOTES': votesTable.tableName, 'TABLE_YOUTUBE_CHANNELS_WHITELIST': youtubeChannelsWhitelistTable.tableName, 'TABLE_KOTLIN_MENTIONS': kotlinMentionsTable.tableName, + 'TABLE_DIALOG_STATES': dialogStatesTable.tableName, 'TOKEN_TELEGRAM_BOT': props.telegramToken, 'TOKEN_YOUTUBE_API': props.youtubeToken, }, @@ -58,6 +65,7 @@ export class JProfByBotStack extends cdk.Stack { votesTable.grantReadWriteData(lambdaWebhook); youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook); kotlinMentionsTable.grantReadWriteData(lambdaWebhook); + dialogStatesTable.grantReadWriteData(lambdaWebhook); const api = new apigateway.RestApi(this, 'jprof-by-bot-api', { restApiName: 'jprof-by-bot-api', diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 78c42847..31a662e4 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -57,6 +57,7 @@ jobs: - run: votes/dynamodb/src/test/resources/seed.sh - run: youtube/dynamodb/src/test/resources/seed.sh - run: kotlin/dynamodb/src/test/resources/seed.sh + - run: dialogs/dynamodb/src/test/resources/seed.sh - run: ./gradlew clean dbTest - uses: actions/upload-artifact@v2 if: always() diff --git a/dialogs/dynamodb/build.gradle.kts b/dialogs/dynamodb/build.gradle.kts new file mode 100644 index 00000000..f4bd4218 --- /dev/null +++ b/dialogs/dynamodb/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.dialogs) + api(libs.dynamodb) + implementation(project.projects.utils.dynamodb) + implementation(libs.kotlinx.coroutines.jdk8) + implementation(libs.kotlinx.serialization.json) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.aws.junit5.dynamo.v2) + testImplementation(project.projects.utils.awsJunit5) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks { + val dbTest by registering(Test::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Runs the DB tests." + shouldRunAfter("test") + outputs.upToDateWhen { false } + useJUnitPlatform { + includeTags("db") + } + } +} diff --git a/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt b/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt new file mode 100644 index 00000000..ae9d9799 --- /dev/null +++ b/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt @@ -0,0 +1,66 @@ +package by.jprof.telegram.bot.dialogs.dynamodb.dao + +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.DialogState +import by.jprof.telegram.bot.utils.dynamodb.toAttributeValue +import by.jprof.telegram.bot.utils.dynamodb.toString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +class DialogStateDAO( + private val dynamoDb: DynamoDbAsyncClient, + private val table: String +) : DialogStateDAO { + override suspend fun get(chatId: Long, userId: Long): DialogState? { + return withContext(Dispatchers.IO) { + dynamoDb.getItem { + it.tableName(table) + it.key( + mapOf( + "userId" to userId.toAttributeValue(), + "chatId" to chatId.toAttributeValue(), + ) + ) + }.await()?.item()?.takeUnless { it.isEmpty() }?.toDialogState() + } + } + + override suspend fun save(dialogState: DialogState) { + withContext(Dispatchers.IO) { + dynamoDb.putItem { + it.tableName(table) + it.item(dialogState.toAttributes()) + }.await() + } + } + + override suspend fun delete(chatId: Long, userId: Long) { + withContext(Dispatchers.IO) { + dynamoDb.deleteItem { + it.tableName(table) + it.key( + mapOf( + "userId" to userId.toAttributeValue(), + "chatId" to chatId.toAttributeValue(), + ) + ) + } + } + } +} + +private val json = Json { serializersModule = DialogState.serializers } + +fun Map.toDialogState(): DialogState = json.decodeFromString(this["value"].toString("value")) + +fun DialogState.toAttributes(): Map = mapOf( + "userId" to this.userId.toAttributeValue(), + "chatId" to this.chatId.toAttributeValue(), + "value" to json.encodeToString(this).toAttributeValue(), +) diff --git a/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAOTest.kt b/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAOTest.kt new file mode 100644 index 00000000..b6716acf --- /dev/null +++ b/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAOTest.kt @@ -0,0 +1,77 @@ +package by.jprof.telegram.bot.dialogs.dynamodb.dao + +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion +import by.jprof.telegram.bot.utils.aws_junit5.Endpoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import me.madhead.aws_junit5.common.AWSClient +import me.madhead.aws_junit5.dynamo.v2.DynamoDB +import org.junit.jupiter.api.* +import org.junit.jupiter.api.extension.ExtendWith +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient + +@Tag("db") +@ExtendWith(DynamoDB::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DialogStateDAOTest { + @AWSClient(endpoint = Endpoint::class) + private lateinit var dynamoDB: DynamoDbAsyncClient + private lateinit var sut: DialogStateDAO + + @BeforeAll + internal fun setup() { + sut = DialogStateDAO(dynamoDB, "dialog-states") + } + + @Test + fun get() = runBlocking { + Assertions.assertEquals( + WaitingForQuestion( + chatId = 1, + userId = 1, + ), + sut.get(1, 1) + ) + } + + @Test + fun save() = runBlocking { + sut.save( + WaitingForQuestion( + chatId = 1, + userId = 2, + ) + ) + + Assertions.assertEquals( + WaitingForQuestion( + chatId = 1, + userId = 2, + ), + sut.get(1, 2) + ) + } + + @Test + fun delete() = runBlocking { + sut.save( + WaitingForQuestion( + chatId = 1, + userId = 3, + ) + ) + sut.delete(1, 3) + + withTimeout(3_000) { + while (null != sut.get(1, 3)) { + delay(100) + } + } + } + + @Test + fun getUnexisting() = runBlocking { + Assertions.assertNull(sut.get(0, 0)) + } +} diff --git a/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt b/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt new file mode 100644 index 00000000..a12881b7 --- /dev/null +++ b/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt @@ -0,0 +1,38 @@ +package by.jprof.telegram.bot.dialogs.dynamodb.dao + +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +internal class DialogStateTest { + @Test + fun waitingForQuestionToAttributes() { + Assertions.assertEquals( + mapOf( + "userId" to AttributeValue.builder().n("2").build(), + "chatId" to AttributeValue.builder().n("1").build(), + "value" to AttributeValue + .builder() + .s("{\"type\":\"WaitingForQuestion\",\"chatId\":1,\"userId\":2}") + .build(), + ), + WaitingForQuestion(1, 2).toAttributes() + ) + } + + @Test + fun attributesToWaitingForQuestionTo() { + Assertions.assertEquals( + WaitingForQuestion(1, 2), + mapOf( + "userId" to AttributeValue.builder().n("2").build(), + "chatId" to AttributeValue.builder().n("1").build(), + "value" to AttributeValue + .builder() + .s("{\"type\":\"WaitingForQuestion\",\"chatId\":1,\"userId\":2}") + .build(), + ).toDialogState() + ) + } +} diff --git a/dialogs/dynamodb/src/test/resources/dialog-states.items.json b/dialogs/dynamodb/src/test/resources/dialog-states.items.json new file mode 100644 index 00000000..6f388918 --- /dev/null +++ b/dialogs/dynamodb/src/test/resources/dialog-states.items.json @@ -0,0 +1,19 @@ +{ + "dialog-states": [ + { + "PutRequest": { + "Item": { + "userId": { + "N": "1" + }, + "chatId": { + "N": "1" + }, + "value": { + "S": "{\n \"userId\": 1,\n \"chatId\": 1,\n \"type\": \"WaitingForQuestion\"\n}\n" + } + } + } + } + ] +} diff --git a/dialogs/dynamodb/src/test/resources/dialog-states.table.json b/dialogs/dynamodb/src/test/resources/dialog-states.table.json new file mode 100644 index 00000000..a1bc8321 --- /dev/null +++ b/dialogs/dynamodb/src/test/resources/dialog-states.table.json @@ -0,0 +1,27 @@ +{ + "TableName": "dialog-states", + "AttributeDefinitions": [ + { + "AttributeName": "chatId", + "AttributeType": "N" + }, + { + "AttributeName": "userId", + "AttributeType": "N" + } + ], + "KeySchema": [ + { + "AttributeName": "userId", + "KeyType": "HASH" + }, + { + "AttributeName": "chatId", + "KeyType": "RANGE" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 1, + "WriteCapacityUnits": 1 + } +} diff --git a/dialogs/dynamodb/src/test/resources/seed.sh b/dialogs/dynamodb/src/test/resources/seed.sh new file mode 100755 index 00000000..c0291db6 --- /dev/null +++ b/dialogs/dynamodb/src/test/resources/seed.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +set -x + +aws --version +aws --endpoint-url "${DYNAMODB_URL}" dynamodb delete-table --table-name dialog-states || true +aws --endpoint-url "${DYNAMODB_URL}" dynamodb create-table --cli-input-json file://dialogs/dynamodb/src/test/resources/dialog-states.table.json +aws --endpoint-url "${DYNAMODB_URL}" dynamodb batch-write-item --request-items file://dialogs/dynamodb/src/test/resources/dialog-states.items.json diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt index c85fa622..fb6c5a66 100644 --- a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/dao/DialogStateDAO.kt @@ -5,7 +5,7 @@ import by.jprof.telegram.bot.dialogs.model.DialogState interface DialogStateDAO { suspend fun get(chatId: Long, userId: Long): DialogState? - suspend fun save(entity: DialogState) + suspend fun save(dialogState: DialogState) suspend fun delete(chatId: Long, userId: Long) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c920fa0..83353d21 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ dynamodb = { group = "software.amazon.awssdk", name = "dynamodb", version.ref = koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } tgbotapi-core = { group = "dev.inmo", name = "tgbotapi.core", version.ref = "tgbotapi" } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt index 2e731bb9..5853dbd0 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt @@ -8,6 +8,7 @@ const val TOKEN_YOUTUBE_API = "TOKEN_YOUTUBE_API" const val TABLE_VOTES = "TABLE_VOTES" const val TABLE_YOUTUBE_CHANNELS_WHITELIST = "TABLE_YOUTUBE_CHANNELS_WHITELIST" const val TABLE_KOTLIN_MENTIONS = "TABLE_KOTLIN_MENTIONS" +const val TABLE_DIALOG_STATES = "TABLE_DIALOG_STATES" val envModule = module { listOf( @@ -16,6 +17,7 @@ val envModule = module { TABLE_VOTES, TABLE_YOUTUBE_CHANNELS_WHITELIST, TABLE_KOTLIN_MENTIONS, + TABLE_DIALOG_STATES, ).forEach { variable -> single(named(variable)) { System.getenv(variable)!! diff --git a/settings.gradle.kts b/settings.gradle.kts index bd4957a8..dc3bcb2d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ include(":votes:dynamodb") include(":votes:tgbotapi-extensions") include(":votes:voting-processor") include(":dialogs") +include(":dialogs:dynamodb") include(":core") include(":jep") include(":youtube") From ff874bc18d23d327d4105ef34e5395f65fc53fec Mon Sep 17 00:00:00 2001 From: madhead Date: Sat, 26 Jun 2021 02:01:49 +0200 Subject: [PATCH 05/15] Save state for /start quizoji command --- quizoji/build.gradle.kts | 1 + .../QuizojiStartCommandUpdateProcessor.kt | 5 ++++ .../QuizojiStartCommandUpdateProcessorTest.kt | 24 ++++++++++++++----- runners/lambda/build.gradle.kts | 1 + .../bot/runners/lambda/config/database.kt | 9 +++++++ .../bot/runners/lambda/config/pipeline.kt | 1 + 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/quizoji/build.gradle.kts b/quizoji/build.gradle.kts index eca0b752..1f9ed494 100644 --- a/quizoji/build.gradle.kts +++ b/quizoji/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project.projects.core) api(libs.tgbotapi.core) + implementation(project.projects.dialogs) implementation(libs.log4j.api) implementation(libs.tgbotapi.extensions.api) implementation(libs.tgbotapi.extensions.utils) diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt index ec002fce..aa8608bc 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt @@ -1,6 +1,8 @@ package by.jprof.telegram.bot.quizoji import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.send.sendMessage import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate @@ -11,6 +13,7 @@ import dev.inmo.tgbotapi.types.update.abstracts.Update import org.apache.logging.log4j.LogManager class QuizojiStartCommandUpdateProcessor( + private val dialogStateDAO: DialogStateDAO, private val bot: RequestsExecutor, ) : UpdateProcessor { companion object { @@ -26,6 +29,8 @@ class QuizojiStartCommandUpdateProcessor( return } + dialogStateDAO.save(WaitingForQuestion(chat.id.chatId, message.user.id.chatId)) + bot.sendMessage( chat = chat, text = "Let's create a Quizoji! First, send me the message. It can be anything — a text, photo, video, even a sticker." diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt index 56791381..d9c0a4a9 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt @@ -1,10 +1,13 @@ package by.jprof.telegram.bot.quizoji +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion import com.soywiz.klock.DateTime import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.send.sendMessage import dev.inmo.tgbotapi.types.Bot import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.CommonUser import dev.inmo.tgbotapi.types.chat.PrivateChatImpl import dev.inmo.tgbotapi.types.chat.abstracts.PrivateChat import dev.inmo.tgbotapi.types.message.PrivateContentMessageImpl @@ -26,11 +29,15 @@ internal class QuizojiStartCommandUpdateProcessorTest { @MockK(relaxed = true) private lateinit var bot: RequestsExecutor + @MockK(relaxed = true) + private lateinit var dialogStateDAO: DialogStateDAO + lateinit var sut: QuizojiStartCommandUpdateProcessor @BeforeEach fun setUp() { sut = QuizojiStartCommandUpdateProcessor( + dialogStateDAO = dialogStateDAO, bot = bot, ) } @@ -44,7 +51,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(dialogStateDAO, bot) wasNot called } clearAllMocks() } @@ -58,7 +65,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(dialogStateDAO, bot) wasNot called } clearAllMocks() } @@ -84,7 +91,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(dialogStateDAO, bot) wasNot called } clearAllMocks() } @@ -110,7 +117,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(dialogStateDAO, bot) wasNot called } clearAllMocks() } @@ -138,7 +145,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(dialogStateDAO, bot) wasNot called } clearAllMocks() } @@ -154,7 +161,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { updateId = 1, data = PrivateContentMessageImpl( messageId = 1, - user = mockk(), + user = CommonUser(id = ChatId(2), "Test"), chat = chat, content = TextContent( text = "/start quizoji" @@ -170,6 +177,11 @@ internal class QuizojiStartCommandUpdateProcessorTest { ) ) + coVerify(exactly = 1) { + dialogStateDAO.save( + WaitingForQuestion(chatId = 1, userId = 2) + ) + } coVerify(exactly = 1) { bot.sendMessage( chat = chat, diff --git a/runners/lambda/build.gradle.kts b/runners/lambda/build.gradle.kts index 632e611f..13fd1e7f 100644 --- a/runners/lambda/build.gradle.kts +++ b/runners/lambda/build.gradle.kts @@ -13,5 +13,6 @@ dependencies { implementation(project.projects.jep) implementation(project.projects.youtube.dynamodb) implementation(project.projects.kotlin.dynamodb) + implementation(project.projects.dialogs.dynamodb) implementation(project.projects.quizoji) } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt index 87df79fe..55330816 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt @@ -1,11 +1,13 @@ package by.jprof.telegram.bot.runners.lambda.config +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO import by.jprof.telegram.bot.kotlin.dao.KotlinMentionsDAO import by.jprof.telegram.bot.votes.dao.VotesDAO import by.jprof.telegram.bot.youtube.dao.YouTubeChannelsWhitelistDAO import org.koin.core.qualifier.named import org.koin.dsl.module import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import by.jprof.telegram.bot.dialogs.dynamodb.dao.DialogStateDAO as DynamoDBDialogStateDAO import by.jprof.telegram.bot.kotlin.dynamodb.dao.KotlinMentionsDAO as DynamoDBKotlinMentionsDAO import by.jprof.telegram.bot.votes.dynamodb.dao.VotesDAO as DynamoDBVotesDAO import by.jprof.telegram.bot.youtube.dynamodb.dao.YouTubeChannelsWhitelistDAO as DynamoDBYouTubeChannelsWhitelistDAO @@ -35,4 +37,11 @@ val databaseModule = module { get(named(TABLE_KOTLIN_MENTIONS)) ) } + + single { + DynamoDBDialogStateDAO( + get(), + get(named(TABLE_DIALOG_STATES)) + ) + } } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt index 1794f340..4c7ba0e8 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt @@ -48,6 +48,7 @@ val pipelineModule = module { single(named("QuizojiStartCommandUpdateProcessor")) { QuizojiStartCommandUpdateProcessor( + dialogStateDAO = get(), bot = get(), ) } From 9c745d98eed3a2876b01881135eb6f65adc2e84d Mon Sep 17 00:00:00 2001 From: madhead Date: Mon, 28 Jun 2021 23:34:29 +0200 Subject: [PATCH 06/15] Additional serializers for tgbotapi model --- settings.gradle.kts | 1 + utils/tgbotapi-serialization/build.gradle.kts | 13 ++++++ .../utils/tgbotapi_serialization/TgBotAPI.kt | 36 +++++++++++++++++ .../serializers/TextContentSerializer.kt | 30 ++++++++++++++ .../surrogates/TextContentSurrogate.kt | 12 ++++++ .../TextContentSerializerTest.kt | 40 +++++++++++++++++++ 6 files changed, 132 insertions(+) create mode 100644 utils/tgbotapi-serialization/build.gradle.kts create mode 100644 utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TgBotAPI.kt create mode 100644 utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/serializers/TextContentSerializer.kt create mode 100644 utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/surrogates/TextContentSurrogate.kt create mode 100644 utils/tgbotapi-serialization/src/test/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TextContentSerializerTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index dc3bcb2d..13e59035 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ rootProject.name = "jprof_by_bot" include(":utils:dynamodb") include(":utils:aws-junit5") +include(":utils:tgbotapi-serialization") include(":votes") include(":votes:dynamodb") include(":votes:tgbotapi-extensions") diff --git a/utils/tgbotapi-serialization/build.gradle.kts b/utils/tgbotapi-serialization/build.gradle.kts new file mode 100644 index 00000000..bd4ee064 --- /dev/null +++ b/utils/tgbotapi-serialization/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") +} + +dependencies { + api(libs.tgbotapi.core) + implementation(libs.kotlinx.serialization.core) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testRuntimeOnly(libs.junit.jupiter.engine) +} diff --git a/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TgBotAPI.kt b/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TgBotAPI.kt new file mode 100644 index 00000000..34c96291 --- /dev/null +++ b/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TgBotAPI.kt @@ -0,0 +1,36 @@ +package by.jprof.telegram.bot.utils.tgbotapi_serialization + +import by.jprof.telegram.bot.utils.tgbotapi_serialization.serializers.TextContentSerializer +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic + +object TgBotAPI { + val module = SerializersModule { + polymorphic(MessageContent::class) { + // subclass(AnimationContent::class, AnimationContentSerializer()) + // subclass(AudioContent::class, AudioContentSerializer()) + // subclass(AudioMediaGroupContent::class, AudioMediaGroupContentSerializer()) + // subclass(ContactContent::class, ContactContentSerializer()) + // subclass(DiceContent::class, DiceContentSerializer()) + // subclass(DocumentContent::class, DocumentContentSerializer()) + // subclass(DocumentMediaGroupContent::class, DocumentMediaGroupContentSerializer()) + // subclass(GameContent::class, GameContentSerializer()) + // subclass(InvoiceContent::class, InvoiceContentSerializer()) + // subclass(LocationContent::class, LocationContentSerializer()) + // subclass(MediaCollectionContent::class, MediaCollectionContentSerializer()) + // subclass(MediaContent::class, MediaContentSerializer()) + // subclass(MediaGroupContent::class, MediaGroupContentSerializer()) + // subclass(PhotoContent::class, PhotoContentSerializer()) + // subclass(PollContent::class, PollContentSerializer()) + // subclass(StickerContent::class, StickerContentSerializer()) + subclass(TextContent::class, TextContentSerializer()) + // subclass(VenueContent::class, VenueContentSerializer()) + // subclass(VideoContent::class, VideoContentSerializer()) + // subclass(VideoNoteContent::class, VideoNoteContentSerializer()) + // subclass(VisualMediaGroupContent::class, VisualMediaGroupContentSerializer()) + // subclass(VoiceContent::class, VoiceContentSerializer()) + } + } +} diff --git a/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/serializers/TextContentSerializer.kt b/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/serializers/TextContentSerializer.kt new file mode 100644 index 00000000..4148da68 --- /dev/null +++ b/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/serializers/TextContentSerializer.kt @@ -0,0 +1,30 @@ +package by.jprof.telegram.bot.utils.tgbotapi_serialization.serializers + +import by.jprof.telegram.bot.utils.tgbotapi_serialization.surrogates.TextContentSurrogate +import dev.inmo.tgbotapi.types.message.content.TextContent +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal class TextContentSerializer : KSerializer { + override val descriptor: SerialDescriptor = TextContentSurrogate.serializer().descriptor + + override fun serialize(encoder: Encoder, value: TextContent) { + val surrogate = TextContentSurrogate( + text = value.text, + textSources = value.textSources, + ) + + encoder.encodeSerializableValue(TextContentSurrogate.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): TextContent { + val surrogate = decoder.decodeSerializableValue(TextContentSurrogate.serializer()) + + return TextContent( + text = surrogate.text, + textSources = surrogate.textSources, + ) + } +} diff --git a/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/surrogates/TextContentSurrogate.kt b/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/surrogates/TextContentSurrogate.kt new file mode 100644 index 00000000..f40e3a5e --- /dev/null +++ b/utils/tgbotapi-serialization/src/main/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/surrogates/TextContentSurrogate.kt @@ -0,0 +1,12 @@ +package by.jprof.telegram.bot.utils.tgbotapi_serialization.surrogates + +import dev.inmo.tgbotapi.types.MessageEntity.textsources.TextSourcesList +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("TextContent") +internal data class TextContentSurrogate( + val text: String, + val textSources: TextSourcesList = emptyList() +) diff --git a/utils/tgbotapi-serialization/src/test/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TextContentSerializerTest.kt b/utils/tgbotapi-serialization/src/test/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TextContentSerializerTest.kt new file mode 100644 index 00000000..640f06d8 --- /dev/null +++ b/utils/tgbotapi-serialization/src/test/kotlin/by/jprof/telegram/bot/utils/tgbotapi_serialization/TextContentSerializerTest.kt @@ -0,0 +1,40 @@ +package by.jprof.telegram.bot.utils.tgbotapi_serialization + +import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource +import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +internal class TextContentSerializerTest { + private val json = Json { serializersModule = TgBotAPI.module } + private val textContent = TextContent( + text = "/start up", + textSources = listOf( + BotCommandTextSource(source = "/start"), + RegularTextSource(source = " up") + ) + ) + private val serialized = + "{\"type\":\"TextContent\",\"text\":\"/start up\",\"textSources\":[{\"type\":\"bot_command\",\"value\":{\"source\":\"/start\"}},{\"type\":\"regular\",\"value\":{\"source\":\" up\"}}]}" + + @Test + fun serialize() { + Assertions.assertEquals( + serialized, + json.encodeToString(textContent) + ) + } + + @Test + fun deserialize() { + Assertions.assertEquals( + textContent, + json.decodeFromString(serialized) + ) + } +} From fb0867f7c9e4cdbb7cd1f444344ff2b5df4ebc82 Mon Sep 17 00:00:00 2001 From: madhead Date: Sun, 27 Jun 2021 01:56:39 +0200 Subject: [PATCH 07/15] Handle question for quizoji --- dialogs/build.gradle.kts | 1 + dialogs/dynamodb/build.gradle.kts | 1 + .../dialogs/dynamodb/dao/DialogStateDAO.kt | 4 +- .../dialogs/dynamodb/dao/DialogStateTest.kt | 36 ++- .../telegram/bot/dialogs/model/DialogState.kt | 2 + .../model/quizoji/WaitingForOptions.kt | 14 ++ gradle/libs.versions.toml | 2 +- .../telegram/bot/jep/JEPUpdateProcessor.kt | 4 +- .../bot/jep/JEPUpdateProcessorTest.kt | 13 +- .../quizoji/QuizojiQuestionUpdateProcessor.kt | 75 ++++++ .../QuizojiStartCommandUpdateProcessor.kt | 4 +- .../QuizojiInlineQueryUpdateProcessorTest.kt | 2 + .../QuizojiQuestionUpdateProcessorTest.kt | 229 ++++++++++++++++++ .../QuizojiStartCommandUpdateProcessorTest.kt | 5 +- .../bot/runners/lambda/config/pipeline.kt | 8 + .../bot/youtube/YouTubeUpdateProcessor.kt | 4 +- 16 files changed, 384 insertions(+), 20 deletions(-) create mode 100644 dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessor.kt create mode 100644 quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessorTest.kt diff --git a/dialogs/build.gradle.kts b/dialogs/build.gradle.kts index d6aa50b9..0bc306b1 100644 --- a/dialogs/build.gradle.kts +++ b/dialogs/build.gradle.kts @@ -4,5 +4,6 @@ plugins { } dependencies { + api(libs.tgbotapi.core) implementation(libs.kotlinx.serialization.core) } diff --git a/dialogs/dynamodb/build.gradle.kts b/dialogs/dynamodb/build.gradle.kts index f4bd4218..756b2079 100644 --- a/dialogs/dynamodb/build.gradle.kts +++ b/dialogs/dynamodb/build.gradle.kts @@ -6,6 +6,7 @@ dependencies { api(project.projects.dialogs) api(libs.dynamodb) implementation(project.projects.utils.dynamodb) + implementation(project.projects.utils.tgbotapiSerialization) implementation(libs.kotlinx.coroutines.jdk8) implementation(libs.kotlinx.serialization.json) diff --git a/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt b/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt index ae9d9799..1428b380 100644 --- a/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt +++ b/dialogs/dynamodb/src/main/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateDAO.kt @@ -4,12 +4,14 @@ import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO import by.jprof.telegram.bot.dialogs.model.DialogState import by.jprof.telegram.bot.utils.dynamodb.toAttributeValue import by.jprof.telegram.bot.utils.dynamodb.toString +import by.jprof.telegram.bot.utils.tgbotapi_serialization.TgBotAPI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.future.await import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.plus import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient import software.amazon.awssdk.services.dynamodb.model.AttributeValue @@ -55,7 +57,7 @@ class DialogStateDAO( } } -private val json = Json { serializersModule = DialogState.serializers } +private val json = Json { serializersModule = DialogState.serializers + TgBotAPI.module } fun Map.toDialogState(): DialogState = json.decodeFromString(this["value"].toString("value")) diff --git a/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt b/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt index a12881b7..bbd527e8 100644 --- a/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt +++ b/dialogs/dynamodb/src/test/kotlin/by/jprof/telegram/bot/dialogs/dynamodb/dao/DialogStateTest.kt @@ -1,13 +1,15 @@ package by.jprof.telegram.bot.dialogs.dynamodb.dao +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion +import dev.inmo.tgbotapi.types.message.content.TextContent import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import software.amazon.awssdk.services.dynamodb.model.AttributeValue internal class DialogStateTest { @Test - fun waitingForQuestionToAttributes() { + fun quizojiWaitingForQuestionToAttributes() { Assertions.assertEquals( mapOf( "userId" to AttributeValue.builder().n("2").build(), @@ -22,7 +24,7 @@ internal class DialogStateTest { } @Test - fun attributesToWaitingForQuestionTo() { + fun quizojiAttributesToWaitingForQuestion() { Assertions.assertEquals( WaitingForQuestion(1, 2), mapOf( @@ -35,4 +37,34 @@ internal class DialogStateTest { ).toDialogState() ) } + + @Test + fun quizojiWaitingForOptionsToAttributes() { + Assertions.assertEquals( + mapOf( + "userId" to AttributeValue.builder().n("2").build(), + "chatId" to AttributeValue.builder().n("1").build(), + "value" to AttributeValue + .builder() + .s("{\"type\":\"WaitingForOptions\",\"chatId\":1,\"userId\":2,\"question\":{\"type\":\"TextContent\",\"text\":\"Question\"}}") + .build(), + ), + WaitingForOptions(1, 2, TextContent("Question")).toAttributes() + ) + } + + @Test + fun quizojiAttributesToWaitingForOptions() { + Assertions.assertEquals( + WaitingForOptions(1, 2, TextContent("Question")), + mapOf( + "userId" to AttributeValue.builder().n("2").build(), + "chatId" to AttributeValue.builder().n("1").build(), + "value" to AttributeValue + .builder() + .s("{\"type\":\"WaitingForOptions\",\"chatId\":1,\"userId\":2,\"question\":{\"type\":\"TextContent\",\"text\":\"Question\"}}") + .build(), + ).toDialogState() + ) + } } diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt index 766c44d1..21f71759 100644 --- a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/DialogState.kt @@ -1,5 +1,6 @@ package by.jprof.telegram.bot.dialogs.model +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic @@ -10,6 +11,7 @@ interface DialogState { val serializers = SerializersModule { polymorphic(DialogState::class) { subclass(WaitingForQuestion::class) + subclass(WaitingForOptions::class) } } } diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt new file mode 100644 index 00000000..c834add5 --- /dev/null +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt @@ -0,0 +1,14 @@ +package by.jprof.telegram.bot.dialogs.model.quizoji + +import by.jprof.telegram.bot.dialogs.model.DialogState +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("WaitingForOptions") +data class WaitingForOptions( + override val chatId: Long, + override val userId: Long, + val question: MessageContent, +) : DialogState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83353d21..fc8f4bc3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ koin = "3.0.1" kotlinx-serialization = "1.2.1" jackson = "2.12.3" -tgbotapi = "0.33.3" +tgbotapi = "0.35.0" jsoup = "1.13.1" diff --git a/jep/src/main/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessor.kt b/jep/src/main/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessor.kt index 9256bd9e..551a5a9f 100644 --- a/jep/src/main/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessor.kt +++ b/jep/src/main/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessor.kt @@ -5,7 +5,6 @@ import by.jprof.telegram.bot.votes.dao.VotesDAO import by.jprof.telegram.bot.votes.model.Votes import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup import by.jprof.telegram.bot.votes.voting_processor.VotingProcessor -import dev.inmo.tgbotapi.CommonAbstracts.justTextSources import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.types.MessageEntity.textsources.TextLinkTextSource @@ -65,8 +64,7 @@ class JEPUpdateProcessor( (message as? ContentMessage<*>)?.let { contentMessage -> (contentMessage.content as? TextContent)?.let { content -> content - .textEntities - .justTextSources() + .textSources .mapNotNull { (it as? URLTextSource)?.source ?: (it as? TextLinkTextSource)?.url } diff --git a/jep/src/test/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessorTest.kt b/jep/src/test/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessorTest.kt index 9d47329d..87870cb0 100644 --- a/jep/src/test/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessorTest.kt +++ b/jep/src/test/kotlin/by/jprof/telegram/bot/jep/JEPUpdateProcessorTest.kt @@ -3,7 +3,6 @@ package by.jprof.telegram.bot.jep import by.jprof.telegram.bot.votes.dao.VotesDAO import by.jprof.telegram.bot.votes.model.Votes import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup -import dev.inmo.tgbotapi.CommonAbstracts.TextPart import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery import dev.inmo.tgbotapi.requests.send.SendTextMessage @@ -92,8 +91,8 @@ internal class JEPUpdateProcessorTest { every { content } returns TextContent( "Hello, world!", listOf( - TextPart(0..2, URLTextSource("https://google.com")), - TextPart(0..2, TextLinkTextSource("google", "https://google.com")), + URLTextSource("https://google.com"), + TextLinkTextSource(" google", "https://google.com") ) ) } @@ -110,8 +109,8 @@ internal class JEPUpdateProcessorTest { every { content } returns TextContent( "Hello, world!", listOf( - TextPart(0..2, URLTextSource("https://openjdk.java.net/jeps/1")), - TextPart(0..2, TextLinkTextSource("JEP 2", "https://openjdk.java.net/jeps/2")), + URLTextSource("https://openjdk.java.net/jeps/1"), + TextLinkTextSource("JEP 2", "https://openjdk.java.net/jeps/2"), ) ) every { chat } returns mockk { @@ -155,8 +154,8 @@ internal class JEPUpdateProcessorTest { every { content } returns TextContent( "Hello, world!", listOf( - TextPart(0..2, URLTextSource("https://openjdk.java.net/jeps/1")), - TextPart(0..2, TextLinkTextSource("JEP 2", "https://openjdk.java.net/jeps/2")), + URLTextSource("https://openjdk.java.net/jeps/1"), + TextLinkTextSource("JEP 2", "https://openjdk.java.net/jeps/2"), ) ) every { chat } returns mockk { diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessor.kt new file mode 100644 index 00000000..2d2fc5fb --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessor.kt @@ -0,0 +1,75 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPrivateChat +import dev.inmo.tgbotapi.extensions.utils.asPrivateContentMessage +import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource +import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2 +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import dev.inmo.tgbotapi.types.update.abstracts.Update +import org.apache.logging.log4j.LogManager +import kotlin.reflect.KClass + +class QuizojiQuestionUpdateProcessor( + private val dialogStateDAO: DialogStateDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(QuizojiQuestionUpdateProcessor::class.java)!! + private val supportedTypes = setOf>( + TextContent::class + ) + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPrivateContentMessage() ?: return + val chat = message.chat.asPrivateChat() ?: return + val state = dialogStateDAO.get(chat.id.chatId, message.user.id.chatId) + + if (state !is WaitingForQuestion) { + return + } + + val content = message.content + + if (content::class !in supportedTypes) { + logger.warn("Unsupported message content: {}", content) + + bot.sendMessage( + chat = chat, + text = "Unsupported question type: ${content::class.simpleName?.replace("Content", "")}" + ) + + return + } + + if ((content as? TextContent)?.textSources?.any { it is BotCommandTextSource } == true) { + logger.warn("Command sent as question: {}", content) + + return + } + + logger.debug("{} provided a question ({}) for his Quizoji", chat.id.chatId, content) + + dialogStateDAO.save( + WaitingForOptions( + chatId = chat.id.chatId, + userId = message.user.id.chatId, + question = content + ) + ) + + bot.sendMessage( + chat = chat, + text = "Now send me the options, one per message\\. When done, send /done\\.\n\n_Up to 8 options are recommended, otherwise the buttons will be wrapped in multiple lines\\._", + parseMode = MarkdownV2 + ) + } +} diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt index aa8608bc..bcf0ff61 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessor.kt @@ -29,11 +29,13 @@ class QuizojiStartCommandUpdateProcessor( return } + logger.debug("{} started new Quizoji", chat.id.chatId) + dialogStateDAO.save(WaitingForQuestion(chat.id.chatId, message.user.id.chatId)) bot.sendMessage( chat = chat, - text = "Let's create a Quizoji! First, send me the message. It can be anything — a text, photo, video, even a sticker." + text = "Let's create a Quizoji! First, send me the question." ) } } diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt index b0cd51f5..c09c26c7 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt @@ -51,6 +51,7 @@ internal class QuizojiInlineQueryUpdateProcessorTest { from = mockk(), query = "alien", offset = "", + chatType = null, ) ) ) @@ -67,6 +68,7 @@ internal class QuizojiInlineQueryUpdateProcessorTest { from = mockk(), query = "quizoji", offset = "", + chatType = null, ) sut.process( diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessorTest.kt new file mode 100644 index 00000000..4c8108f9 --- /dev/null +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiQuestionUpdateProcessorTest.kt @@ -0,0 +1,229 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.DialogState +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForQuestion +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.CommonUser +import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2 +import dev.inmo.tgbotapi.types.chat.PrivateChatImpl +import dev.inmo.tgbotapi.types.chat.abstracts.ChannelChat +import dev.inmo.tgbotapi.types.dice.Dice +import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType +import dev.inmo.tgbotapi.types.message.PrivateContentMessageImpl +import dev.inmo.tgbotapi.types.message.abstracts.ChannelContentMessage +import dev.inmo.tgbotapi.types.message.content.DiceContent +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.update.MessageUpdate +import dev.inmo.tgbotapi.types.update.PollUpdate +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class QuizojiQuestionUpdateProcessorTest { + @MockK(relaxed = true) + private lateinit var bot: RequestsExecutor + + @MockK(relaxed = true) + private lateinit var dialogStateDAO: DialogStateDAO + + lateinit var sut: QuizojiQuestionUpdateProcessor + + @BeforeEach + fun setUp() { + sut = QuizojiQuestionUpdateProcessor( + dialogStateDAO = dialogStateDAO, + bot = bot, + ) + } + + @Test + fun processNonMessageUpdate() = runBlocking { + sut.process( + PollUpdate( + updateId = 1, + data = mockk() + ) + ) + + verify { listOf(dialogStateDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateContentMessage() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = mockk>() + ) + ) + + verify { listOf(dialogStateDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateChat() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = mockk(), + content = mockk(), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + verify { listOf(dialogStateDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonWaitingForQuestion() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(object : DialogState { + override val chatId = 1L + override val userId = 2L + }) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = TextContent( + text = "Test" + ), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processUnsupportedContent() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForQuestion(1, 2)) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = DiceContent(dice = Dice(value = 3, animationType = SlotMachineDiceAnimationType)), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Unsupported question type: Dice" + ) + } + + clearAllMocks() + } + + @Test + fun process() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + val content = TextContent( + text = "Test" + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForQuestion(1, 2)) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = content, + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { + dialogStateDAO.save( + WaitingForOptions( + chatId = 1, + userId = 2, + question = content + ) + ) + } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Now send me the options, one per message\\. When done, send /done\\.\n\n_Up to 8 options are recommended, otherwise the buttons will be wrapped in multiple lines\\._", + parseMode = MarkdownV2 + ) + } + + clearAllMocks() + } +} diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt index d9c0a4a9..4e4ee9da 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiStartCommandUpdateProcessorTest.kt @@ -9,6 +9,7 @@ import dev.inmo.tgbotapi.types.Bot import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.CommonUser import dev.inmo.tgbotapi.types.chat.PrivateChatImpl +import dev.inmo.tgbotapi.types.chat.abstracts.ChannelChat import dev.inmo.tgbotapi.types.chat.abstracts.PrivateChat import dev.inmo.tgbotapi.types.message.PrivateContentMessageImpl import dev.inmo.tgbotapi.types.message.abstracts.ChannelContentMessage @@ -78,7 +79,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { data = PrivateContentMessageImpl( messageId = 1, user = mockk(), - chat = mockk(), + chat = mockk(), content = mockk(), date = DateTime.now(), editDate = null, @@ -185,7 +186,7 @@ internal class QuizojiStartCommandUpdateProcessorTest { coVerify(exactly = 1) { bot.sendMessage( chat = chat, - text = "Let's create a Quizoji! First, send me the message. It can be anything — a text, photo, video, even a sticker." + text = "Let's create a Quizoji! First, send me the question." ) } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt index 4c7ba0e8..232292b6 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt @@ -6,6 +6,7 @@ import by.jprof.telegram.bot.jep.JEPUpdateProcessor import by.jprof.telegram.bot.jep.JsoupJEPSummary import by.jprof.telegram.bot.kotlin.KotlinMentionsUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiInlineQueryUpdateProcessor +import by.jprof.telegram.bot.quizoji.QuizojiQuestionUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiStartCommandUpdateProcessor import by.jprof.telegram.bot.youtube.YouTubeUpdateProcessor import org.koin.core.qualifier.named @@ -52,4 +53,11 @@ val pipelineModule = module { bot = get(), ) } + + single(named("QuizojiQuestionUpdateProcessor")) { + QuizojiQuestionUpdateProcessor( + dialogStateDAO = get(), + bot = get(), + ) + } } diff --git a/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt b/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt index d8ee00ab..8026737c 100644 --- a/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt +++ b/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt @@ -7,7 +7,6 @@ import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup import by.jprof.telegram.bot.votes.voting_processor.VotingProcessor import by.jprof.telegram.bot.youtube.dao.YouTubeChannelsWhitelistDAO import com.google.api.services.youtube.YouTube -import dev.inmo.tgbotapi.CommonAbstracts.justTextSources import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.utils.formatting.boldMarkdownV2 @@ -69,8 +68,7 @@ class YouTubeUpdateProcessor( (message as? ContentMessage<*>)?.let { contentMessage -> (contentMessage.content as? TextContent)?.let { content -> content - .textEntities - .justTextSources() + .textSources .mapNotNull { (it as? URLTextSource)?.source ?: (it as? TextLinkTextSource)?.url } From be1632d3b3db4ceac694aca2ec9cc91c1d408ab1 Mon Sep 17 00:00:00 2001 From: madhead Date: Sun, 27 Jun 2021 16:29:14 +0200 Subject: [PATCH 08/15] Handle options for quizoji --- .../model/quizoji/WaitingForOptions.kt | 1 + .../quizoji/QuizojiOptionUpdateProcessor.kt | 73 +++++ .../QuizojiOptionUpdateProcessorTest.kt | 305 ++++++++++++++++++ .../bot/runners/lambda/config/pipeline.kt | 8 + 4 files changed, 387 insertions(+) create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessor.kt create mode 100644 quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt diff --git a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt index c834add5..2b3dc221 100644 --- a/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt +++ b/dialogs/src/main/kotlin/by/jprof/telegram/bot/dialogs/model/quizoji/WaitingForOptions.kt @@ -11,4 +11,5 @@ data class WaitingForOptions( override val chatId: Long, override val userId: Long, val question: MessageContent, + val options: List = emptyList(), ) : DialogState diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessor.kt new file mode 100644 index 00000000..e040a78d --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessor.kt @@ -0,0 +1,73 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPrivateChat +import dev.inmo.tgbotapi.extensions.utils.asPrivateContentMessage +import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource +import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2 +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.update.abstracts.Update +import org.apache.logging.log4j.LogManager + +class QuizojiOptionUpdateProcessor( + private val dialogStateDAO: DialogStateDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(QuizojiOptionUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPrivateContentMessage() ?: return + val chat = message.chat.asPrivateChat() ?: return + val state = dialogStateDAO.get(chat.id.chatId, message.user.id.chatId) + val content = message.content + + if (state !is WaitingForOptions) { + return + } + + if (content !is TextContent) { + logger.warn("Unsupported option content: {}", content) + + bot.sendMessage( + chat = chat, + text = "Unsupported option type: ${content::class.simpleName?.replace("Content", "")}" + ) + + return + } + + if (content.textSources.any { it is BotCommandTextSource }) { + logger.warn("Command sent as option: {}", content) + + return + } + + logger.debug("{} provided an option ({}) for his Quizoji", chat.id.chatId, content.text) + + dialogStateDAO.save( + WaitingForOptions( + chatId = chat.id.chatId, + userId = message.user.id.chatId, + question = state.question, + options = state.options + content.text + ) + ) + + bot.sendMessage( + chat = chat, + text = if (state.options.size >= 7) { + "Send more options or /done when ready\\." + } else { + "Send more options or /done when ready\\.\n\n_Up to ${7 - state.options.size} more options are recommended\\._" + }, + parseMode = MarkdownV2 + ) + } +} diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt new file mode 100644 index 00000000..3a88c057 --- /dev/null +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt @@ -0,0 +1,305 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.DialogState +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.CommonUser +import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2 +import dev.inmo.tgbotapi.types.chat.PrivateChatImpl +import dev.inmo.tgbotapi.types.chat.abstracts.ChannelChat +import dev.inmo.tgbotapi.types.dice.Dice +import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType +import dev.inmo.tgbotapi.types.message.PrivateContentMessageImpl +import dev.inmo.tgbotapi.types.message.abstracts.ChannelContentMessage +import dev.inmo.tgbotapi.types.message.content.DiceContent +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.update.MessageUpdate +import dev.inmo.tgbotapi.types.update.PollUpdate +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class QuizojiOptionUpdateProcessorTest { + @MockK(relaxed = true) + private lateinit var bot: RequestsExecutor + + @MockK(relaxed = true) + private lateinit var dialogStateDAO: DialogStateDAO + + lateinit var sut: QuizojiOptionUpdateProcessor + + @BeforeEach + fun setUp() { + sut = QuizojiOptionUpdateProcessor( + dialogStateDAO = dialogStateDAO, + bot = bot, + ) + } + + @Test + fun processNonMessageUpdate() = runBlocking { + sut.process( + PollUpdate( + updateId = 1, + data = mockk() + ) + ) + + verify { listOf(dialogStateDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateContentMessage() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = mockk>() + ) + ) + + verify { listOf(dialogStateDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateChat() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = mockk(), + content = mockk(), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + verify { listOf(dialogStateDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonWaitingForOptions() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(object : DialogState { + override val chatId = 1L + override val userId = 2L + }) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = TextContent( + text = "Test" + ), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processUnsupportedContent() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, TextContent("Test"))) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = DiceContent(dice = Dice(value = 3, animationType = SlotMachineDiceAnimationType)), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Unsupported option type: Dice" + ) + } + + clearAllMocks() + } + + @Test + fun process() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + val content = TextContent( + text = "Option" + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, TextContent("Test"))) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = content, + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { + dialogStateDAO.save( + WaitingForOptions( + chatId = 1, + userId = 2, + question = TextContent("Test"), + options = listOf("Option") + ) + ) + } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Send more options or /done when ready\\.\n\n_Up to 7 more options are recommended\\._", + parseMode = MarkdownV2 + ) + } + + clearAllMocks() + } + + @Test + fun processManyOptions() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + val content = TextContent( + text = "Option 8" + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns( + WaitingForOptions( + 1, + 2, + TextContent("Test"), + listOf( + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + "Option 7", + ) + ) + ) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = content, + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { + dialogStateDAO.save( + WaitingForOptions( + chatId = 1, + userId = 2, + question = TextContent("Test"), + options = listOf( + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + "Option 7", + "Option 8", + ) + ) + ) + } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Send more options or /done when ready\\.", + parseMode = MarkdownV2 + ) + } + + clearAllMocks() + } +} diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt index 232292b6..3ab74d52 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt @@ -6,6 +6,7 @@ import by.jprof.telegram.bot.jep.JEPUpdateProcessor import by.jprof.telegram.bot.jep.JsoupJEPSummary import by.jprof.telegram.bot.kotlin.KotlinMentionsUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiInlineQueryUpdateProcessor +import by.jprof.telegram.bot.quizoji.QuizojiOptionUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiQuestionUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiStartCommandUpdateProcessor import by.jprof.telegram.bot.youtube.YouTubeUpdateProcessor @@ -60,4 +61,11 @@ val pipelineModule = module { bot = get(), ) } + + single(named("QuizojiOptionUpdateProcessor")) { + QuizojiOptionUpdateProcessor( + dialogStateDAO = get(), + bot = get(), + ) + } } From bbfe3b94d2cc5fa2ab33ac1a48073b970a4af30a Mon Sep 17 00:00:00 2001 From: madhead Date: Sun, 27 Jun 2021 16:44:45 +0200 Subject: [PATCH 09/15] Model and DAO for quizojis --- .deploy/lambda/lib/JProfByBotStack.ts | 8 +++ .github/workflows/default.yml | 1 + quizoji/dynamodb/build.gradle.kts | 29 ++++++++++ .../bot/quizoji/dynamodb/dao/QuizojiDAO.kt | 54 +++++++++++++++++++ .../quizoji/dynamodb/dao/QuizojiDAOTest.kt | 51 ++++++++++++++++++ .../bot/quizoji/dynamodb/dao/QuizojiTest.kt | 48 +++++++++++++++++ .../src/test/resources/quizoji.items.json | 29 ++++++++++ .../src/test/resources/quizoji.table.json | 19 +++++++ quizoji/dynamodb/src/test/resources/seed.sh | 8 +++ .../telegram/bot/quizoji/dao/QuizojiDAO.kt | 9 ++++ .../telegram/bot/quizoji/model/Quizoji.kt | 9 ++++ .../telegram/bot/runners/lambda/config/env.kt | 2 + settings.gradle.kts | 1 + 13 files changed, 268 insertions(+) create mode 100644 quizoji/dynamodb/build.gradle.kts create mode 100644 quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt create mode 100644 quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt create mode 100644 quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt create mode 100644 quizoji/dynamodb/src/test/resources/quizoji.items.json create mode 100644 quizoji/dynamodb/src/test/resources/quizoji.table.json create mode 100755 quizoji/dynamodb/src/test/resources/seed.sh create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/dao/QuizojiDAO.kt create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt diff --git a/.deploy/lambda/lib/JProfByBotStack.ts b/.deploy/lambda/lib/JProfByBotStack.ts index e8c4b023..9c93b973 100644 --- a/.deploy/lambda/lib/JProfByBotStack.ts +++ b/.deploy/lambda/lib/JProfByBotStack.ts @@ -31,6 +31,12 @@ export class JProfByBotStack extends cdk.Stack { sortKey: { name: 'chatId', type: dynamodb.AttributeType.NUMBER }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, }); + const quizojisTable = new dynamodb.Table(this, 'jprof-by-bot-table-quizojis', { + tableName: 'jprof-by-bot-table-quizojis', + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + }); + const layerLibGL = new lambda.LayerVersion(this, 'jprof-by-bot-lambda-layer-libGL', { code: lambda.Code.fromAsset('layers/libGL.zip'), compatibleRuntimes: [lambda.Runtime.JAVA_11], @@ -57,6 +63,7 @@ export class JProfByBotStack extends cdk.Stack { 'TABLE_YOUTUBE_CHANNELS_WHITELIST': youtubeChannelsWhitelistTable.tableName, 'TABLE_KOTLIN_MENTIONS': kotlinMentionsTable.tableName, 'TABLE_DIALOG_STATES': dialogStatesTable.tableName, + 'TABLE_QUIZOJIS': quizojisTable.tableName, 'TOKEN_TELEGRAM_BOT': props.telegramToken, 'TOKEN_YOUTUBE_API': props.youtubeToken, }, @@ -66,6 +73,7 @@ export class JProfByBotStack extends cdk.Stack { youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook); kotlinMentionsTable.grantReadWriteData(lambdaWebhook); dialogStatesTable.grantReadWriteData(lambdaWebhook); + quizojisTable.grantReadWriteData(lambdaWebhook); const api = new apigateway.RestApi(this, 'jprof-by-bot-api', { restApiName: 'jprof-by-bot-api', diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 31a662e4..aaa3aa64 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -58,6 +58,7 @@ jobs: - run: youtube/dynamodb/src/test/resources/seed.sh - run: kotlin/dynamodb/src/test/resources/seed.sh - run: dialogs/dynamodb/src/test/resources/seed.sh + - run: quizoji/dynamodb/src/test/resources/seed.sh - run: ./gradlew clean dbTest - uses: actions/upload-artifact@v2 if: always() diff --git a/quizoji/dynamodb/build.gradle.kts b/quizoji/dynamodb/build.gradle.kts new file mode 100644 index 00000000..53bd1f65 --- /dev/null +++ b/quizoji/dynamodb/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.quizoji) + api(libs.dynamodb) + implementation(project.projects.utils.dynamodb) + implementation(project.projects.utils.tgbotapiSerialization) + implementation(libs.kotlinx.coroutines.jdk8) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.aws.junit5.dynamo.v2) + testImplementation(project.projects.utils.awsJunit5) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks { + val dbTest by registering(Test::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Runs the DB tests." + shouldRunAfter("test") + outputs.upToDateWhen { false } + useJUnitPlatform { + includeTags("db") + } + } +} diff --git a/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt b/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt new file mode 100644 index 00000000..70b72808 --- /dev/null +++ b/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt @@ -0,0 +1,54 @@ +package by.jprof.telegram.bot.quizoji.dynamodb.dao + +import by.jprof.telegram.bot.quizoji.dao.QuizojiDAO +import by.jprof.telegram.bot.quizoji.model.Quizoji +import by.jprof.telegram.bot.utils.dynamodb.toAttributeValue +import by.jprof.telegram.bot.utils.dynamodb.toString +import by.jprof.telegram.bot.utils.tgbotapi_serialization.TgBotAPI +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +class QuizojiDAO( + private val dynamoDb: DynamoDbAsyncClient, + private val table: String +) : QuizojiDAO { + override suspend fun save(quizoji: Quizoji) { + withContext(Dispatchers.IO) { + dynamoDb.putItem { + it.tableName(table) + it.item(quizoji.toAttributes()) + }.await() + } + } + + override suspend fun get(id: String): Quizoji? { + return withContext(Dispatchers.IO) { + dynamoDb.getItem { + it.tableName(table) + it.key(mapOf("id" to id.toAttributeValue())) + }.await()?.item()?.takeUnless { it.isEmpty() }?.toQuizoji() + } + } +} + +private val json = Json { serializersModule = TgBotAPI.module } + +fun Quizoji.toAttributes(): Map = mapOf( + "id" to this.id.toAttributeValue(), + "question" to json.encodeToString(this.question).toAttributeValue(), + "options" to this.options.map { it.toAttributeValue() }.toAttributeValue(), +) + +fun Map.toQuizoji(): Quizoji = Quizoji( + id = this["id"].toString("id"), + question = json.decodeFromString(this["question"].toString("value")), + options = this["options"]?.l() + ?.mapNotNull { it.s() } + ?: emptyList(), +) diff --git a/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt new file mode 100644 index 00000000..d28a6483 --- /dev/null +++ b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt @@ -0,0 +1,51 @@ +package by.jprof.telegram.bot.quizoji.dynamodb.dao + +import by.jprof.telegram.bot.quizoji.model.Quizoji +import by.jprof.telegram.bot.utils.aws_junit5.Endpoint +import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource +import dev.inmo.tgbotapi.types.message.content.TextContent +import kotlinx.coroutines.runBlocking +import me.madhead.aws_junit5.common.AWSClient +import me.madhead.aws_junit5.dynamo.v2.DynamoDB +import org.junit.jupiter.api.* +import org.junit.jupiter.api.extension.ExtendWith +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient + +@Tag("db") +@ExtendWith(DynamoDB::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuizojiDAOTest { + @AWSClient(endpoint = Endpoint::class) + private lateinit var dynamoDB: DynamoDbAsyncClient + private lateinit var sut: QuizojiDAO + + @BeforeAll + internal fun setup() { + sut = QuizojiDAO(dynamoDB, "quizoji") + } + + @Test + fun save() = runBlocking { + sut.save(quizoji) + } + + @Test + fun get() = runBlocking { + Assertions.assertEquals(quizoji, sut.get("test")) + } + + @Test + fun getUnexisting() = runBlocking { + Assertions.assertNull(sut.get("unexisting")) + } + + private val quizoji + get() = Quizoji( + id = "test", + question = TextContent( + text = "Choose the door", + textSources = listOf(RegularTextSource("Choose the door")), + ), + options = listOf("1", "2", "3"), + ) +} diff --git a/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt new file mode 100644 index 00000000..e772739a --- /dev/null +++ b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt @@ -0,0 +1,48 @@ +package by.jprof.telegram.bot.quizoji.dynamodb.dao + +import by.jprof.telegram.bot.quizoji.model.Quizoji +import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource +import dev.inmo.tgbotapi.types.message.content.TextContent +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +internal class QuizojiTest { + @Test + fun toAttributes() { + Assertions.assertEquals( + attributes, + quizoji.toAttributes() + ) + } + + @Test + fun toQuizoji() { + Assertions.assertEquals( + quizoji, + attributes.toQuizoji() + ) + } + + private val quizoji + get() = Quizoji( + id = "test", + question = TextContent( + text = "Choose the door", + textSources = listOf(RegularTextSource("Choose the door")), + ), + options = listOf("1", "2", "3"), + ) + private val attributes + get() = mapOf( + "id" to AttributeValue.builder().s("test").build(), + "question" to AttributeValue.builder() + .s("{\"type\":\"TextContent\",\"text\":\"Choose the door\",\"textSources\":[{\"type\":\"regular\",\"value\":{\"source\":\"Choose the door\"}}]}") + .build(), + "options" to AttributeValue.builder().l( + AttributeValue.builder().s("1").build(), + AttributeValue.builder().s("2").build(), + AttributeValue.builder().s("3").build(), + ).build(), + ) +} diff --git a/quizoji/dynamodb/src/test/resources/quizoji.items.json b/quizoji/dynamodb/src/test/resources/quizoji.items.json new file mode 100644 index 00000000..3e5b9f05 --- /dev/null +++ b/quizoji/dynamodb/src/test/resources/quizoji.items.json @@ -0,0 +1,29 @@ +{ + "quizoji": [ + { + "PutRequest": { + "Item": { + "id": { + "S": "test" + }, + "question": { + "S": "{\"type\":\"TextContent\",\"text\":\"Choose the door\",\"textSources\":[{\"type\":\"regular\",\"value\":{\"source\":\"Choose the door\"}}]}" + }, + "options": { + "L": [ + { + "S": "1" + }, + { + "S": "2" + }, + { + "S": "3" + } + ] + } + } + } + } + ] +} diff --git a/quizoji/dynamodb/src/test/resources/quizoji.table.json b/quizoji/dynamodb/src/test/resources/quizoji.table.json new file mode 100644 index 00000000..e32e4ca8 --- /dev/null +++ b/quizoji/dynamodb/src/test/resources/quizoji.table.json @@ -0,0 +1,19 @@ +{ + "TableName": "quizoji", + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 1, + "WriteCapacityUnits": 1 + } +} diff --git a/quizoji/dynamodb/src/test/resources/seed.sh b/quizoji/dynamodb/src/test/resources/seed.sh new file mode 100755 index 00000000..abc109f9 --- /dev/null +++ b/quizoji/dynamodb/src/test/resources/seed.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +set -x + +aws --version +aws --endpoint-url "${DYNAMODB_URL}" dynamodb delete-table --table-name quizoji || true +aws --endpoint-url "${DYNAMODB_URL}" dynamodb create-table --cli-input-json file://quizoji/dynamodb/src/test/resources/quizoji.table.json +aws --endpoint-url "${DYNAMODB_URL}" dynamodb batch-write-item --request-items file://quizoji/dynamodb/src/test/resources/quizoji.items.json diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/dao/QuizojiDAO.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/dao/QuizojiDAO.kt new file mode 100644 index 00000000..bd1235e6 --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/dao/QuizojiDAO.kt @@ -0,0 +1,9 @@ +package by.jprof.telegram.bot.quizoji.dao + +import by.jprof.telegram.bot.quizoji.model.Quizoji + +interface QuizojiDAO { + suspend fun save(quizoji: Quizoji) + + suspend fun get(id: String): Quizoji? +} diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt new file mode 100644 index 00000000..15d33892 --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt @@ -0,0 +1,9 @@ +package by.jprof.telegram.bot.quizoji.model + +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent + +data class Quizoji( + val id: String, + val question: MessageContent, + val options: List, +) diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt index 5853dbd0..4236a0e7 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/env.kt @@ -9,6 +9,7 @@ const val TABLE_VOTES = "TABLE_VOTES" const val TABLE_YOUTUBE_CHANNELS_WHITELIST = "TABLE_YOUTUBE_CHANNELS_WHITELIST" const val TABLE_KOTLIN_MENTIONS = "TABLE_KOTLIN_MENTIONS" const val TABLE_DIALOG_STATES = "TABLE_DIALOG_STATES" +const val TABLE_QUIZOJIS = "TABLE_QUIZOJIS" val envModule = module { listOf( @@ -18,6 +19,7 @@ val envModule = module { TABLE_YOUTUBE_CHANNELS_WHITELIST, TABLE_KOTLIN_MENTIONS, TABLE_DIALOG_STATES, + TABLE_QUIZOJIS, ).forEach { variable -> single(named(variable)) { System.getenv(variable)!! diff --git a/settings.gradle.kts b/settings.gradle.kts index 13e59035..ace6981e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,4 +19,5 @@ include(":youtube:dynamodb") include(":kotlin") include(":kotlin:dynamodb") include(":quizoji") +include(":quizoji:dynamodb") include(":runners:lambda") From d59a7aaeb14ab37d46af184629dbd1643b683eea Mon Sep 17 00:00:00 2001 From: madhead Date: Tue, 29 Jun 2021 00:12:53 +0200 Subject: [PATCH 10/15] Votes to multiline inline keyboard --- .../tgbotapi_extensions/VotesExtensions.kt | 16 ++++ .../VotesExtensionsTest.kt | 82 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/votes/tgbotapi-extensions/src/main/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensions.kt b/votes/tgbotapi-extensions/src/main/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensions.kt index e2c15b61..ad855fb7 100644 --- a/votes/tgbotapi-extensions/src/main/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensions.kt +++ b/votes/tgbotapi-extensions/src/main/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensions.kt @@ -16,3 +16,19 @@ fun Votes.toInlineKeyboardMarkup() = InlineKeyboardMarkup( } ) ) + +fun Votes.toInlineKeyboardMarkup(size: Int) = InlineKeyboardMarkup( + if (this.options.isEmpty()) { + listOf(emptyList()) + } else { + this + .options + .map { + CallbackDataInlineKeyboardButton( + text = "${this.count(it)} $it", + callbackData = "${this.id}:$it" + ) + } + .chunked(size) + } +) diff --git a/votes/tgbotapi-extensions/src/test/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensionsTest.kt b/votes/tgbotapi-extensions/src/test/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensionsTest.kt index e63803b7..e2c0a390 100644 --- a/votes/tgbotapi-extensions/src/test/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensionsTest.kt +++ b/votes/tgbotapi-extensions/src/test/kotlin/by/jprof/telegram/bot/votes/tgbotapi_extensions/VotesExtensionsTest.kt @@ -19,6 +19,12 @@ internal class VotesExtensionsTest { Assertions.assertEquals(keyboard, votes.toInlineKeyboardMarkup()) } + @ParameterizedTest + @MethodSource + fun toInlineKeyboardMarkupChunked(votes: Votes, size: Int, keyboard: InlineKeyboardMarkup) { + Assertions.assertEquals(keyboard, votes.toInlineKeyboardMarkup(size)) + } + private fun toInlineKeyboardMarkup(): Stream = sequence { yield( Arguments.of( @@ -72,4 +78,80 @@ internal class VotesExtensionsTest { ) ) }.asStream() + + private fun toInlineKeyboardMarkupChunked(): Stream = sequence { + yield( + Arguments.of( + Votes("test"), + 8, + InlineKeyboardMarkup( + listOf(emptyList()) + ) + ) + ) + yield( + Arguments.of( + Votes("test", listOf("1")), + 8, + InlineKeyboardMarkup( + listOf( + listOf( + CallbackDataInlineKeyboardButton("0 1", "test:1") + ) + ) + ) + ) + ) + yield( + Arguments.of( + Votes("test", listOf("1", "2")), + 8, + InlineKeyboardMarkup( + listOf( + listOf( + CallbackDataInlineKeyboardButton("0 1", "test:1"), + CallbackDataInlineKeyboardButton("0 2", "test:2"), + ) + ) + ) + ) + ) + yield( + Arguments.of( + Votes("test", listOf("1", "2")), + 1, + InlineKeyboardMarkup( + listOf( + listOf( + CallbackDataInlineKeyboardButton("0 1", "test:1"), + ), + listOf( + CallbackDataInlineKeyboardButton("0 2", "test:2"), + ) + ) + ) + ) + ) + yield( + Arguments.of( + Votes( + "test", + listOf("1", "2", "3"), + mapOf("user1" to "1", "user2" to "2", "user3" to "UNEXISTING_OPTION") + ), + 2, + InlineKeyboardMarkup( + listOf( + listOf( + CallbackDataInlineKeyboardButton("1 1", "test:1"), + CallbackDataInlineKeyboardButton("1 2", "test:2"), + ), + listOf( + CallbackDataInlineKeyboardButton("0 3", "test:3"), + ) + ) + ) + ) + ) + }.asStream() } From 1b5a001b96296b4a4c1b11793ea6136add5e4b71 Mon Sep 17 00:00:00 2001 From: madhead Date: Wed, 30 Jun 2021 01:06:17 +0200 Subject: [PATCH 11/15] Allow to process InlineMessageIdDataCallbackQuery in VotingProcessor --- .../votes/voting_processor/VotingProcessor.kt | 28 +++- .../voting_processor/VotingProcessorTest.kt | 129 ++++++++++++++---- 2 files changed, 125 insertions(+), 32 deletions(-) diff --git a/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt b/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt index e0adce5c..199ead5a 100644 --- a/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt +++ b/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt @@ -7,6 +7,8 @@ import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery import dev.inmo.tgbotapi.extensions.api.edit.ReplyMarkup.editMessageReplyMarkup import dev.inmo.tgbotapi.types.CallbackQuery.CallbackQuery +import dev.inmo.tgbotapi.types.CallbackQuery.DataCallbackQuery +import dev.inmo.tgbotapi.types.CallbackQuery.InlineMessageIdDataCallbackQuery import dev.inmo.tgbotapi.types.CallbackQuery.MessageDataCallbackQuery import org.apache.logging.log4j.LogManager @@ -21,9 +23,9 @@ abstract class VotingProcessor( } suspend fun processCallbackQuery(callbackQuery: CallbackQuery) { - logger.debug("Processing callback query: {}", callbackQuery) + logger.debug("[{}] Processing callback query: {}", prefix, callbackQuery) - (callbackQuery as? MessageDataCallbackQuery)?.data?.takeIf { it.startsWith(prefix) }?.let { data -> + (callbackQuery as? DataCallbackQuery)?.data?.takeIf { it.startsWith(prefix) }?.let { data -> val (votesId, vote) = data.split(":").takeIf { it.size == 2 } ?: return val fromUserId = callbackQuery.user.id.chatId.toString() @@ -34,10 +36,24 @@ abstract class VotingProcessor( votesDAO.save(updatedVotes) bot.answerCallbackQuery(callbackQuery) - bot.editMessageReplyMarkup( - message = callbackQuery.message, - replyMarkup = updatedVotes.toInlineKeyboardMarkup() - ) + + when (callbackQuery) { + is MessageDataCallbackQuery -> { + bot.editMessageReplyMarkup( + message = callbackQuery.message, + replyMarkup = updatedVotes.toInlineKeyboardMarkup() + ) + } + is InlineMessageIdDataCallbackQuery -> { + bot.editMessageReplyMarkup( + inlineMessageId = callbackQuery.inlineMessageId, + replyMarkup = updatedVotes.toInlineKeyboardMarkup() + ) + } + else -> { + logger.error("Unknown callback query type: {}", callbackQuery::class.simpleName) + } + } } } diff --git a/votes/voting-processor/src/test/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessorTest.kt b/votes/voting-processor/src/test/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessorTest.kt index 1752c0c9..da91f429 100644 --- a/votes/voting-processor/src/test/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessorTest.kt +++ b/votes/voting-processor/src/test/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessorTest.kt @@ -3,20 +3,24 @@ package by.jprof.telegram.bot.votes.voting_processor import by.jprof.telegram.bot.votes.dao.VotesDAO import by.jprof.telegram.bot.votes.model.Votes import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup +import com.soywiz.klock.DateTime import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery import dev.inmo.tgbotapi.extensions.api.edit.ReplyMarkup.editMessageReplyMarkup +import dev.inmo.tgbotapi.types.CallbackQuery.InlineMessageIdDataCallbackQuery import dev.inmo.tgbotapi.types.CallbackQuery.MessageDataCallbackQuery import dev.inmo.tgbotapi.types.CallbackQuery.MessageGameShortNameCallbackQuery import dev.inmo.tgbotapi.types.ChatId -import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage +import dev.inmo.tgbotapi.types.CommonUser +import dev.inmo.tgbotapi.types.UserId +import dev.inmo.tgbotapi.types.chat.GroupChatImpl +import dev.inmo.tgbotapi.types.message.CommonGroupContentMessageImpl import dev.inmo.tgbotapi.types.message.content.TextContent import io.mockk.* import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -44,23 +48,29 @@ internal class VotingProcessorTest { } @Test - @Disabled - fun processCallbackQuery() = runBlocking { - val callbackQuery = mockk { - every { id } returns "" - every { user } returns mockk { - every { id } returns ChatId(42L) - } - every { chatInstance } returns "" - every { message.hint(ContentMessage::class) } returns mockk> { - every { messageId } returns 1L - every { content.hint(TextContent::class) } returns TextContent("") - every { chat } returns mockk { - every { id } returns ChatId(1L) - } - } - every { data } returns "TEST-42:+" - } + fun processMessageDataCallbackQuery() = runBlocking { + val message = CommonGroupContentMessageImpl( + chat = GroupChatImpl( + id = ChatId(1L), + title = "Test" + ), + messageId = 1L, + user = CommonUser(UserId(2L), "Test 2"), + date = DateTime.now(), + forwardInfo = null, + editDate = null, + replyTo = null, + replyMarkup = null, + content = TextContent(""), + senderBot = null, + ) + val callbackQuery = MessageDataCallbackQuery( + id = "", + user = CommonUser(UserId(1L), "Test 1"), + chatInstance = "", + message = message, + data = "TEST-42:+" + ) coEvery { votesDAO.get("TEST-42") } returns Votes("TEST-42", listOf("+", "-")) coEvery { votesDAO.save(any()) } just runs @@ -68,12 +78,12 @@ internal class VotingProcessorTest { sut.processCallbackQuery(callbackQuery) - coVerify(exactly = 1) { votesDAO.save(Votes("TEST-42", listOf("+", "-"), mapOf("42" to "+"))) } + coVerify(exactly = 1) { votesDAO.save(Votes("TEST-42", listOf("+", "-"), mapOf("1" to "+"))) } coVerify(exactly = 1) { bot.answerCallbackQuery(callbackQuery) } coVerify(exactly = 1) { bot.editMessageReplyMarkup( - message = callbackQuery.message, - replyMarkup = Votes("TEST-42", listOf("+", "-"), mapOf("42" to "+")).toInlineKeyboardMarkup() + message = message, + replyMarkup = Votes("TEST-42", listOf("+", "-"), mapOf("1" to "+")).toInlineKeyboardMarkup(), ) } @@ -81,13 +91,80 @@ internal class VotingProcessorTest { } @Test - @Disabled - fun processCallbackQueryForNewVotes() { - TODO() + fun processCallbackQueryForNewVotes() = runBlocking { + val message = CommonGroupContentMessageImpl( + chat = GroupChatImpl( + id = ChatId(1L), + title = "Test" + ), + messageId = 1L, + user = CommonUser(UserId(2L), "Test 2"), + date = DateTime.now(), + forwardInfo = null, + editDate = null, + replyTo = null, + replyMarkup = null, + content = TextContent(""), + senderBot = null, + ) + val callbackQuery = MessageDataCallbackQuery( + id = "", + user = CommonUser(UserId(1L), "Test 1"), + chatInstance = "", + message = message, + data = "TEST-42:+" + ) + + coEvery { votesDAO.get("TEST-42") } returns null + every { votesConstructor.invoke("TEST-42") } returns Votes("TEST-42", listOf("+", "-", "=")) + coEvery { votesDAO.save(any()) } just runs + coEvery { bot.answerCallbackQuery(callbackQuery) } returns true + + sut.processCallbackQuery(callbackQuery) + + coVerify(exactly = 1) { votesDAO.save(Votes("TEST-42", listOf("+", "-", "="), mapOf("1" to "+"))) } + coVerify(exactly = 1) { bot.answerCallbackQuery(callbackQuery) } + coVerify(exactly = 1) { + bot.editMessageReplyMarkup( + message = message, + replyMarkup = Votes("TEST-42", listOf("+", "-", "="), mapOf("1" to "+")).toInlineKeyboardMarkup(), + ) + } + + clearAllMocks() + } + + @Test + fun processInlineMessageIdDataCallbackQuery() = runBlocking { + val callbackQuery = InlineMessageIdDataCallbackQuery( + id = "", + user = CommonUser(UserId(1L), "Test 1"), + chatInstance = "", + inlineMessageId = "300", + data = "TEST-42:+" + ) + + coEvery { votesDAO.get("TEST-42") } returns null + every { votesConstructor.invoke("TEST-42") } returns Votes("TEST-42", listOf("+", "-")) + coEvery { votesDAO.save(any()) } just runs + coEvery { bot.answerCallbackQuery(callbackQuery) } returns true + + sut.processCallbackQuery(callbackQuery) + + coVerify(exactly = 1) { votesDAO.save(Votes("TEST-42", listOf("+", "-"), mapOf("1" to "+"))) } + coVerify(exactly = 1) { bot.answerCallbackQuery(callbackQuery) } + coVerify(exactly = 1) { + bot.editMessageReplyMarkup( + inlineMessageId = "300", + replyMarkup = Votes("TEST-42", listOf("+", "-"), mapOf("1" to "+")).toInlineKeyboardMarkup(), + ) + } + + clearAllMocks() } @Test - fun processNonMessageDataCallbackQuery() = runBlocking { + fun processUnknownCallbackQuery() = runBlocking { sut.processCallbackQuery( MessageGameShortNameCallbackQuery( id = "test", From c1f4280a18736715412a00af29d2bb3200cbde14 Mon Sep 17 00:00:00 2001 From: madhead Date: Wed, 30 Jun 2021 01:21:47 +0200 Subject: [PATCH 12/15] /done command processor --- quizoji/build.gradle.kts | 2 + .../bot/quizoji/dynamodb/dao/QuizojiDAO.kt | 4 - .../quizoji/dynamodb/dao/QuizojiDAOTest.kt | 1 - .../bot/quizoji/dynamodb/dao/QuizojiTest.kt | 6 - .../src/test/resources/quizoji.items.json | 13 - .../QuizojiDoneCommandUpdateProcessor.kt | 98 ++++++ .../telegram/bot/quizoji/model/Quizoji.kt | 1 - .../QuizojiDoneCommandUpdateProcessorTest.kt | 280 ++++++++++++++++++ 8 files changed, 380 insertions(+), 25 deletions(-) create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt create mode 100644 quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt diff --git a/quizoji/build.gradle.kts b/quizoji/build.gradle.kts index 1f9ed494..9e76f5b6 100644 --- a/quizoji/build.gradle.kts +++ b/quizoji/build.gradle.kts @@ -6,6 +6,8 @@ dependencies { api(project.projects.core) api(libs.tgbotapi.core) implementation(project.projects.dialogs) + implementation(project.projects.votes) + implementation(project.projects.votes.tgbotapiExtensions) implementation(libs.log4j.api) implementation(libs.tgbotapi.extensions.api) implementation(libs.tgbotapi.extensions.utils) diff --git a/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt b/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt index 70b72808..35da0b9d 100644 --- a/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt +++ b/quizoji/dynamodb/src/main/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAO.kt @@ -42,13 +42,9 @@ private val json = Json { serializersModule = TgBotAPI.module } fun Quizoji.toAttributes(): Map = mapOf( "id" to this.id.toAttributeValue(), "question" to json.encodeToString(this.question).toAttributeValue(), - "options" to this.options.map { it.toAttributeValue() }.toAttributeValue(), ) fun Map.toQuizoji(): Quizoji = Quizoji( id = this["id"].toString("id"), question = json.decodeFromString(this["question"].toString("value")), - options = this["options"]?.l() - ?.mapNotNull { it.s() } - ?: emptyList(), ) diff --git a/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt index d28a6483..742f0252 100644 --- a/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt +++ b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiDAOTest.kt @@ -46,6 +46,5 @@ internal class QuizojiDAOTest { text = "Choose the door", textSources = listOf(RegularTextSource("Choose the door")), ), - options = listOf("1", "2", "3"), ) } diff --git a/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt index e772739a..f2ea7888 100644 --- a/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt +++ b/quizoji/dynamodb/src/test/kotlin/by/jprof/telegram/bot/quizoji/dynamodb/dao/QuizojiTest.kt @@ -31,7 +31,6 @@ internal class QuizojiTest { text = "Choose the door", textSources = listOf(RegularTextSource("Choose the door")), ), - options = listOf("1", "2", "3"), ) private val attributes get() = mapOf( @@ -39,10 +38,5 @@ internal class QuizojiTest { "question" to AttributeValue.builder() .s("{\"type\":\"TextContent\",\"text\":\"Choose the door\",\"textSources\":[{\"type\":\"regular\",\"value\":{\"source\":\"Choose the door\"}}]}") .build(), - "options" to AttributeValue.builder().l( - AttributeValue.builder().s("1").build(), - AttributeValue.builder().s("2").build(), - AttributeValue.builder().s("3").build(), - ).build(), ) } diff --git a/quizoji/dynamodb/src/test/resources/quizoji.items.json b/quizoji/dynamodb/src/test/resources/quizoji.items.json index 3e5b9f05..6cad5527 100644 --- a/quizoji/dynamodb/src/test/resources/quizoji.items.json +++ b/quizoji/dynamodb/src/test/resources/quizoji.items.json @@ -8,19 +8,6 @@ }, "question": { "S": "{\"type\":\"TextContent\",\"text\":\"Choose the door\",\"textSources\":[{\"type\":\"regular\",\"value\":{\"source\":\"Choose the door\"}}]}" - }, - "options": { - "L": [ - { - "S": "1" - }, - { - "S": "2" - }, - { - "S": "3" - } - ] } } } diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt new file mode 100644 index 00000000..6d3dd993 --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt @@ -0,0 +1,98 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions +import by.jprof.telegram.bot.quizoji.dao.QuizojiDAO +import by.jprof.telegram.bot.quizoji.model.Quizoji +import by.jprof.telegram.bot.votes.dao.VotesDAO +import by.jprof.telegram.bot.votes.model.Votes +import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPrivateChat +import dev.inmo.tgbotapi.extensions.utils.asPrivateContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.SwitchInlineQueryInlineKeyboardButton +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.update.abstracts.Update +import org.apache.logging.log4j.LogManager + +class QuizojiDoneCommandUpdateProcessor( + private val dialogStateDAO: DialogStateDAO, + private val quizojiDAO: QuizojiDAO, + private val votesDAO: VotesDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(QuizojiDoneCommandUpdateProcessor::class.java)!! + private val idChars = ('0'..'9') + ('a'..'z') + ('A'..'Z') + private const val idLength = 20 + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPrivateContentMessage() ?: return + val chat = message.chat.asPrivateChat() ?: return + val state = dialogStateDAO.get(chat.id.chatId, message.user.id.chatId) + + if (state !is WaitingForOptions) { + return + } + + val content = message.content.asTextContent() ?: return + + if (content.text != "/done") { + return + } + + logger.debug("{} finished his Quizoji", chat.id.chatId) + + val quizoji = Quizoji( + id = generateQuizojiID(), + question = state.question, + ) + val votes = Votes( + id = "QUIZOJI-${quizoji.id}", + options = state.options + ) + + votesDAO.save(votes) + quizojiDAO.save(quizoji) + dialogStateDAO.delete(chatId = chat.id.chatId, userId = message.user.id.chatId) + + bot.sendMessage( + chat = chat, + text = "Quizoji created! Use the 'Publish' button to send it." + ) + + when (val question = state.question) { + is TextContent -> { + bot.sendMessage( + chat = chat, + entities = question.textSources, + replyMarkup = InlineKeyboardMarkup( + votes + .toInlineKeyboardMarkup(8) + .keyboard + .plus( + listOf( + listOf( + SwitchInlineQueryInlineKeyboardButton( + text = "Publish", + switchInlineQuery = "quizoji ${quizoji.id}", + ) + ) + ) + ) + ) + ) + } + } + } + + private fun generateQuizojiID(): String { + return (0 until idLength).map { idChars.random() }.joinToString(separator = "") + } +} diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt index 15d33892..bff3095b 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/model/Quizoji.kt @@ -5,5 +5,4 @@ import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent data class Quizoji( val id: String, val question: MessageContent, - val options: List, ) diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt new file mode 100644 index 00000000..8efac903 --- /dev/null +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt @@ -0,0 +1,280 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.dialogs.model.DialogState +import by.jprof.telegram.bot.dialogs.model.quizoji.WaitingForOptions +import by.jprof.telegram.bot.quizoji.dao.QuizojiDAO +import by.jprof.telegram.bot.quizoji.model.Quizoji +import by.jprof.telegram.bot.votes.dao.VotesDAO +import by.jprof.telegram.bot.votes.model.Votes +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.CommonUser +import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.SwitchInlineQueryInlineKeyboardButton +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup +import dev.inmo.tgbotapi.types.chat.PrivateChatImpl +import dev.inmo.tgbotapi.types.chat.abstracts.ChannelChat +import dev.inmo.tgbotapi.types.message.PrivateContentMessageImpl +import dev.inmo.tgbotapi.types.message.abstracts.ChannelContentMessage +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.media.AudioContent +import dev.inmo.tgbotapi.types.update.MessageUpdate +import dev.inmo.tgbotapi.types.update.PollUpdate +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +internal class QuizojiDoneCommandUpdateProcessorTest { + @MockK(relaxed = true) + private lateinit var bot: RequestsExecutor + + @MockK + private lateinit var dialogStateDAO: DialogStateDAO + + @MockK + private lateinit var quizojiDAO: QuizojiDAO + + @MockK + private lateinit var votesDAO: VotesDAO + + lateinit var sut: QuizojiDoneCommandUpdateProcessor + + @BeforeEach + fun setUp() { + sut = QuizojiDoneCommandUpdateProcessor( + dialogStateDAO = dialogStateDAO, + quizojiDAO = quizojiDAO, + votesDAO = votesDAO, + bot = bot, + ) + } + + @Test + fun processNonMessageUpdate() = runBlocking { + sut.process( + PollUpdate( + updateId = 1, + data = mockk() + ) + ) + + verify { listOf(dialogStateDAO, quizojiDAO, votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateContentMessage() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = mockk>() + ) + ) + + verify { listOf(dialogStateDAO, quizojiDAO, votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonPrivateChat() = runBlocking { + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = mockk(), + chat = mockk(), + content = mockk(), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + verify { listOf(dialogStateDAO, quizojiDAO, votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonWaitingForOptions() = runBlocking { + coEvery { dialogStateDAO.get(1, 2) }.returns(object : DialogState { + override val chatId = 1L + override val userId = 2L + }) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(ChatId(2), "Test"), + chat = PrivateChatImpl(ChatId(1)), + content = TextContent("/done"), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + verify { listOf(quizojiDAO, votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processNonTextContentMessage() = runBlocking { + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, TextContent(""), listOf(""))) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(ChatId(2), "Test"), + chat = PrivateChatImpl(ChatId(1)), + content = mockk(), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + verify { listOf(quizojiDAO, votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processWrongCommand() = runBlocking { + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, TextContent(""), listOf(""))) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(ChatId(2), "Test"), + chat = PrivateChatImpl(ChatId(1)), + content = TextContent("/start doing your morning exercise"), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + verify { listOf(quizojiDAO, votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processTextQuestion() = runBlocking { + val question = TextContent("Question", listOf(RegularTextSource("Question"))) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, question, listOf("1", "2"))) + coEvery { votesDAO.save(any()) } just runs + coEvery { quizojiDAO.save(any()) } just runs + coEvery { dialogStateDAO.delete(any(), any()) } just runs + + val chat = PrivateChatImpl(ChatId(1)) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(ChatId(2), "Test"), + chat = chat, + content = TextContent("/done"), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + val quizojiSlot = CapturingSlot() + val votesSlot = CapturingSlot() + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { quizojiDAO.save(capture(quizojiSlot)) } + Assertions.assertEquals(question, quizojiSlot.captured.question) + coVerify(exactly = 1) { votesDAO.save(capture(votesSlot)) } + Assertions.assertEquals("QUIZOJI-${quizojiSlot.captured.id}", votesSlot.captured.id) + Assertions.assertEquals(listOf("1", "2"), votesSlot.captured.options) + coVerify(exactly = 1) { dialogStateDAO.delete(1, 2) } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Quizoji created! Use the 'Publish' button to send it." + ) + } + coVerify { + bot.sendMessage( + chat = chat, + entities = question.textSources, + replyMarkup = InlineKeyboardMarkup( + listOf( + listOf( + CallbackDataInlineKeyboardButton( + text = "0 1", + callbackData = "QUIZOJI-${quizojiSlot.captured.id}:1" + ), + CallbackDataInlineKeyboardButton( + text = "0 2", + callbackData = "QUIZOJI-${quizojiSlot.captured.id}:2" + ) + ), + listOf( + SwitchInlineQueryInlineKeyboardButton( + text = "Publish", + switchInlineQuery = "quizoji ${quizojiSlot.captured.id}", + ) + ), + ) + ) + ) + } + + clearAllMocks() + } +} From b83d817ff5d252758a00587b187943b43f6f479b Mon Sep 17 00:00:00 2001 From: madhead Date: Wed, 30 Jun 2021 21:59:47 +0200 Subject: [PATCH 13/15] Quizoji votes processor --- quizoji/build.gradle.kts | 1 + .../QuizojiInlineQueryUpdateProcessor.kt | 46 +++++++ .../bot/quizoji/QuizojiVoteUpdateProcessor.kt | 24 ++++ .../QuizojiInlineQueryUpdateProcessorTest.kt | 122 +++++++++++++++++- .../QuizojiOptionUpdateProcessorTest.kt | 37 ++++++ runners/lambda/build.gradle.kts | 2 +- .../bot/runners/lambda/config/database.kt | 9 ++ .../bot/runners/lambda/config/pipeline.kt | 23 +++- 8 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt diff --git a/quizoji/build.gradle.kts b/quizoji/build.gradle.kts index 9e76f5b6..73f07b34 100644 --- a/quizoji/build.gradle.kts +++ b/quizoji/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { implementation(project.projects.dialogs) implementation(project.projects.votes) implementation(project.projects.votes.tgbotapiExtensions) + implementation(project.projects.votes.votingProcessor) implementation(libs.log4j.api) implementation(libs.tgbotapi.extensions.api) implementation(libs.tgbotapi.extensions.utils) diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt index 27c0b05b..db83828a 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessor.kt @@ -1,13 +1,21 @@ package by.jprof.telegram.bot.quizoji import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.quizoji.dao.QuizojiDAO +import by.jprof.telegram.bot.votes.dao.VotesDAO +import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery import dev.inmo.tgbotapi.extensions.utils.asInlineQueryUpdate +import dev.inmo.tgbotapi.types.InlineQueries.InlineQueryResult.InlineQueryResultArticle +import dev.inmo.tgbotapi.types.InlineQueries.InputMessageContent.InputTextMessageContent +import dev.inmo.tgbotapi.types.message.content.TextContent import dev.inmo.tgbotapi.types.update.abstracts.Update import org.apache.logging.log4j.LogManager class QuizojiInlineQueryUpdateProcessor( + private val quizojiDAO: QuizojiDAO, + private val votesDAO: VotesDAO, private val bot: RequestsExecutor, ) : UpdateProcessor { companion object { @@ -23,6 +31,44 @@ class QuizojiInlineQueryUpdateProcessor( switchPmText = "Create new quizoji", switchPmParameter = "quizoji", ) + } else if (inlineQuery.query.startsWith("quizoji")) { + val id = inlineQuery.query.substring(7).trim() + + logger.debug("Quizoji #{} requested", id) + + val quizoji = quizojiDAO.get(id) + + if (null == quizoji) { + logger.warn("Quizoji #{} not found!", id) + + return + } + + val votes = votesDAO.get("QUIZOJI-$id") + + if (null == votes) { + logger.warn("Votes for quizoji #{} not found!", id) + + return + } + + when (val question = quizoji.question) { + is TextContent -> { + bot.answerInlineQuery( + inlineQuery = inlineQuery, + results = listOf( + InlineQueryResultArticle( + id = quizoji.id, + title = "Quizoji with ${votes.options.joinToString()} options", + inputMessageContent = InputTextMessageContent( + entities = question.textSources + ), + replyMarkup = votes.toInlineKeyboardMarkup(8) + ) + ) + ) + } + } } } } diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt new file mode 100644 index 00000000..7b958c9b --- /dev/null +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt @@ -0,0 +1,24 @@ +package by.jprof.telegram.bot.quizoji + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.votes.dao.VotesDAO +import by.jprof.telegram.bot.votes.voting_processor.VotingProcessor +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.types.update.CallbackQueryUpdate +import dev.inmo.tgbotapi.types.update.abstracts.Update + +class QuizojiVoteUpdateProcessor( + votesDAO: VotesDAO, + bot: RequestsExecutor +) : VotingProcessor( + "QUIZOJI", + votesDAO, + { throw UnsupportedOperationException("Votes should be constructed elsewhere!") }, + bot, +), UpdateProcessor { + override suspend fun process(update: Update) { + when (update) { + is CallbackQueryUpdate -> processCallbackQuery(update.data) + } + } +} diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt index c09c26c7..4911012e 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiInlineQueryUpdateProcessorTest.kt @@ -1,8 +1,14 @@ package by.jprof.telegram.bot.quizoji +import by.jprof.telegram.bot.quizoji.dao.QuizojiDAO +import by.jprof.telegram.bot.quizoji.model.Quizoji +import by.jprof.telegram.bot.votes.dao.VotesDAO +import by.jprof.telegram.bot.votes.model.Votes import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.answers.answerInlineQuery import dev.inmo.tgbotapi.types.InlineQueries.query.BaseInlineQuery +import dev.inmo.tgbotapi.types.MessageEntity.textsources.RegularTextSource +import dev.inmo.tgbotapi.types.message.content.TextContent import dev.inmo.tgbotapi.types.update.InlineQueryUpdate import dev.inmo.tgbotapi.types.update.MessageUpdate import io.mockk.* @@ -15,6 +21,12 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) internal class QuizojiInlineQueryUpdateProcessorTest { + @MockK + private lateinit var quizojiDAO: QuizojiDAO + + @MockK + private lateinit var votesDAO: VotesDAO + @MockK(relaxed = true) private lateinit var bot: RequestsExecutor @@ -23,6 +35,8 @@ internal class QuizojiInlineQueryUpdateProcessorTest { @BeforeEach fun setUp() { sut = QuizojiInlineQueryUpdateProcessor( + quizojiDAO = quizojiDAO, + votesDAO = votesDAO, bot = bot, ) } @@ -36,7 +50,7 @@ internal class QuizojiInlineQueryUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(quizojiDAO, votesDAO, bot) wasNot called } clearAllMocks() } @@ -56,13 +70,13 @@ internal class QuizojiInlineQueryUpdateProcessorTest { ) ) - verify { listOf(bot) wasNot called } + verify { listOf(quizojiDAO, votesDAO, bot) wasNot called } clearAllMocks() } @Test - fun processQuizojiInilineQuery() = runBlocking { + fun processNewQuizojiInilineQuery() = runBlocking { val inlineQuery = BaseInlineQuery( id = "QuizojiStartCommandUpdateProcessorTest", from = mockk(), @@ -85,6 +99,108 @@ internal class QuizojiInlineQueryUpdateProcessorTest { switchPmParameter = "quizoji", ) } + verify { listOf(quizojiDAO, votesDAO) wasNot called } + + clearAllMocks() + } + + @Test + fun processUnknownQuizojiIDInilineQuery() = runBlocking { + val inlineQuery = BaseInlineQuery( + id = "QuizojiStartCommandUpdateProcessorTest", + from = mockk(), + query = "quizoji id", + offset = "", + chatType = null, + ) + + coEvery { quizojiDAO.get("id") }.returns(null) + + sut.process( + InlineQueryUpdate( + updateId = 1L, + data = inlineQuery + ) + ) + + coVerify(exactly = 1) { + quizojiDAO.get("id") + } + + verify { listOf(votesDAO, bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processUnexistingVotesQuizojiIDInilineQuery() = runBlocking { + val inlineQuery = BaseInlineQuery( + id = "QuizojiStartCommandUpdateProcessorTest", + from = mockk(), + query = "quizoji id", + offset = "", + chatType = null, + ) + + coEvery { quizojiDAO.get("id") }.returns(Quizoji("id", TextContent("Question"))) + coEvery { votesDAO.get("QUIZOJI-id") }.returns(null) + + sut.process( + InlineQueryUpdate( + updateId = 1L, + data = inlineQuery + ) + ) + + coVerify(exactly = 1) { + quizojiDAO.get("id") + } + coVerify(exactly = 1) { + votesDAO.get("QUIZOJI-id") + } + + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + + @Test + fun processQuizojiIDInilineQuery() = runBlocking { + val inlineQuery = BaseInlineQuery( + id = "QuizojiStartCommandUpdateProcessorTest", + from = mockk(), + query = "quizoji id", + offset = "", + chatType = null, + ) + + coEvery { quizojiDAO.get("id") }.returns( + Quizoji( + "id", + TextContent("Question", listOf(RegularTextSource("Question"))) + ) + ) + coEvery { votesDAO.get("QUIZOJI-id") }.returns(Votes("QUIZOJI-id", listOf("1", "2"), mapOf("1" to "1"))) + + sut.process( + InlineQueryUpdate( + updateId = 1L, + data = inlineQuery + ) + ) + + coVerify(exactly = 1) { + quizojiDAO.get("id") + } + coVerify(exactly = 1) { + votesDAO.get("QUIZOJI-id") + } +// coVerify(exactly = 1) { +// bot.answerInlineQuery( +// inlineQuery = inlineQuery, +// results = any() +// ) +// } clearAllMocks() } diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt index 3a88c057..7fc49a6d 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiOptionUpdateProcessorTest.kt @@ -8,6 +8,7 @@ import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.send.sendMessage import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.CommonUser +import dev.inmo.tgbotapi.types.MessageEntity.textsources.BotCommandTextSource import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2 import dev.inmo.tgbotapi.types.chat.PrivateChatImpl import dev.inmo.tgbotapi.types.chat.abstracts.ChannelChat @@ -175,6 +176,42 @@ internal class QuizojiOptionUpdateProcessorTest { clearAllMocks() } + @Test + fun processDoneCommand() = runBlocking { + val chat = PrivateChatImpl( + id = ChatId(1), + ) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, TextContent("Test"))) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(id = ChatId(2), "Test"), + chat = chat, + content = TextContent( + text = "/done", + textSources = listOf(BotCommandTextSource("done")) + ), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + verify { listOf(bot) wasNot called } + + clearAllMocks() + } + @Test fun process() = runBlocking { val chat = PrivateChatImpl( diff --git a/runners/lambda/build.gradle.kts b/runners/lambda/build.gradle.kts index 13fd1e7f..2b47fe12 100644 --- a/runners/lambda/build.gradle.kts +++ b/runners/lambda/build.gradle.kts @@ -14,5 +14,5 @@ dependencies { implementation(project.projects.youtube.dynamodb) implementation(project.projects.kotlin.dynamodb) implementation(project.projects.dialogs.dynamodb) - implementation(project.projects.quizoji) + implementation(project.projects.quizoji.dynamodb) } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt index 55330816..1ac80e68 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/database.kt @@ -2,6 +2,7 @@ package by.jprof.telegram.bot.runners.lambda.config import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO import by.jprof.telegram.bot.kotlin.dao.KotlinMentionsDAO +import by.jprof.telegram.bot.quizoji.dao.QuizojiDAO import by.jprof.telegram.bot.votes.dao.VotesDAO import by.jprof.telegram.bot.youtube.dao.YouTubeChannelsWhitelistDAO import org.koin.core.qualifier.named @@ -9,6 +10,7 @@ import org.koin.dsl.module import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient import by.jprof.telegram.bot.dialogs.dynamodb.dao.DialogStateDAO as DynamoDBDialogStateDAO import by.jprof.telegram.bot.kotlin.dynamodb.dao.KotlinMentionsDAO as DynamoDBKotlinMentionsDAO +import by.jprof.telegram.bot.quizoji.dynamodb.dao.QuizojiDAO as DynamoDBQuizojiDAO import by.jprof.telegram.bot.votes.dynamodb.dao.VotesDAO as DynamoDBVotesDAO import by.jprof.telegram.bot.youtube.dynamodb.dao.YouTubeChannelsWhitelistDAO as DynamoDBYouTubeChannelsWhitelistDAO @@ -44,4 +46,11 @@ val databaseModule = module { get(named(TABLE_DIALOG_STATES)) ) } + + single { + DynamoDBQuizojiDAO( + get(), + get(named(TABLE_QUIZOJIS)) + ) + } } diff --git a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt index 3ab74d52..06c342ba 100644 --- a/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt +++ b/runners/lambda/src/main/kotlin/by/jprof/telegram/bot/runners/lambda/config/pipeline.kt @@ -5,10 +5,7 @@ import by.jprof.telegram.bot.core.UpdateProcessor import by.jprof.telegram.bot.jep.JEPUpdateProcessor import by.jprof.telegram.bot.jep.JsoupJEPSummary import by.jprof.telegram.bot.kotlin.KotlinMentionsUpdateProcessor -import by.jprof.telegram.bot.quizoji.QuizojiInlineQueryUpdateProcessor -import by.jprof.telegram.bot.quizoji.QuizojiOptionUpdateProcessor -import by.jprof.telegram.bot.quizoji.QuizojiQuestionUpdateProcessor -import by.jprof.telegram.bot.quizoji.QuizojiStartCommandUpdateProcessor +import by.jprof.telegram.bot.quizoji.* import by.jprof.telegram.bot.youtube.YouTubeUpdateProcessor import org.koin.core.qualifier.named import org.koin.dsl.module @@ -44,6 +41,8 @@ val pipelineModule = module { single(named("QuizojiInlineQueryUpdateProcessor")) { QuizojiInlineQueryUpdateProcessor( + quizojiDAO = get(), + votesDAO = get(), bot = get(), ) } @@ -68,4 +67,20 @@ val pipelineModule = module { bot = get(), ) } + + single(named("QuizojiDoneCommandUpdateProcessor")) { + QuizojiDoneCommandUpdateProcessor( + dialogStateDAO = get(), + quizojiDAO = get(), + votesDAO = get(), + bot = get(), + ) + } + + single(named("QuizojiVoteUpdateProcessor")) { + QuizojiVoteUpdateProcessor( + votesDAO = get(), + bot = get(), + ) + } } From 0457493e4edab971667fe08b0da343a7b1a27d3d Mon Sep 17 00:00:00 2001 From: madhead Date: Thu, 1 Jul 2021 23:38:24 +0200 Subject: [PATCH 14/15] Disallow empty options list in /done --- .../QuizojiDoneCommandUpdateProcessor.kt | 9 ++++ .../QuizojiDoneCommandUpdateProcessorTest.kt | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt index 6d3dd993..3e3f408e 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessor.kt @@ -47,6 +47,15 @@ class QuizojiDoneCommandUpdateProcessor( return } + if (state.options.isEmpty()) { + bot.sendMessage( + chat = chat, + text = "Please, provide some options for your quizoji!" + ) + + return + } + logger.debug("{} finished his Quizoji", chat.id.chatId) val quizoji = Quizoji( diff --git a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt index 8efac903..d3203340 100644 --- a/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt +++ b/quizoji/src/test/kotlin/by/jprof/telegram/bot/quizoji/QuizojiDoneCommandUpdateProcessorTest.kt @@ -203,6 +203,48 @@ internal class QuizojiDoneCommandUpdateProcessorTest { clearAllMocks() } + @Test + fun processNoOptions() = runBlocking { + val question = TextContent("Question", listOf(RegularTextSource("Question"))) + + coEvery { dialogStateDAO.get(1, 2) }.returns(WaitingForOptions(1, 2, question)) + coEvery { votesDAO.save(any()) } just runs + coEvery { quizojiDAO.save(any()) } just runs + coEvery { dialogStateDAO.delete(any(), any()) } just runs + + val chat = PrivateChatImpl(ChatId(1)) + + sut.process( + MessageUpdate( + updateId = 1, + data = PrivateContentMessageImpl( + messageId = 1, + user = CommonUser(ChatId(2), "Test"), + chat = chat, + content = TextContent("/done"), + date = DateTime.now(), + editDate = null, + forwardInfo = null, + replyTo = null, + replyMarkup = null, + senderBot = null, + paymentInfo = null, + ) + ) + ) + + coVerify(exactly = 1) { dialogStateDAO.get(1, 2) } + coVerify(exactly = 1) { + bot.sendMessage( + chat = chat, + text = "Please, provide some options for your quizoji!" + ) + } + verify { listOf(quizojiDAO, votesDAO) wasNot called } + + clearAllMocks() + } + @Test fun processTextQuestion() = runBlocking { val question = TextContent("Question", listOf(RegularTextSource("Question"))) From df6c8356221a3820f40743009b2d7cda295c59ee Mon Sep 17 00:00:00 2001 From: madhead Date: Thu, 1 Jul 2021 23:59:02 +0200 Subject: [PATCH 15/15] Allow to override votes to keyboard logic in voting processor --- .../telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt | 4 ++++ .../telegram/bot/votes/voting_processor/VotingProcessor.kt | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt index 7b958c9b..c1f5de94 100644 --- a/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt +++ b/quizoji/src/main/kotlin/by/jprof/telegram/bot/quizoji/QuizojiVoteUpdateProcessor.kt @@ -2,6 +2,8 @@ package by.jprof.telegram.bot.quizoji import by.jprof.telegram.bot.core.UpdateProcessor import by.jprof.telegram.bot.votes.dao.VotesDAO +import by.jprof.telegram.bot.votes.model.Votes +import by.jprof.telegram.bot.votes.tgbotapi_extensions.toInlineKeyboardMarkup import by.jprof.telegram.bot.votes.voting_processor.VotingProcessor import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.types.update.CallbackQueryUpdate @@ -21,4 +23,6 @@ class QuizojiVoteUpdateProcessor( is CallbackQueryUpdate -> processCallbackQuery(update.data) } } + + override fun votesToInlineKeyboardMarkup(votes: Votes) = votes.toInlineKeyboardMarkup(8) } diff --git a/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt b/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt index 199ead5a..9b4446de 100644 --- a/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt +++ b/votes/voting-processor/src/main/kotlin/by/jprof/telegram/bot/votes/voting_processor/VotingProcessor.kt @@ -41,13 +41,13 @@ abstract class VotingProcessor( is MessageDataCallbackQuery -> { bot.editMessageReplyMarkup( message = callbackQuery.message, - replyMarkup = updatedVotes.toInlineKeyboardMarkup() + replyMarkup = votesToInlineKeyboardMarkup(updatedVotes) ) } is InlineMessageIdDataCallbackQuery -> { bot.editMessageReplyMarkup( inlineMessageId = callbackQuery.inlineMessageId, - replyMarkup = updatedVotes.toInlineKeyboardMarkup() + replyMarkup = votesToInlineKeyboardMarkup(updatedVotes) ) } else -> { @@ -57,5 +57,7 @@ abstract class VotingProcessor( } } + open fun votesToInlineKeyboardMarkup(votes: Votes) = votes.toInlineKeyboardMarkup() + protected fun String.toVotesID() = "$prefix-$this" }