Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .deploy/lambda/lib/JProfByBotStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ 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 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],
Expand All @@ -50,6 +62,8 @@ 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,
'TABLE_QUIZOJIS': quizojisTable.tableName,
'TOKEN_TELEGRAM_BOT': props.telegramToken,
'TOKEN_YOUTUBE_API': props.youtubeToken,
},
Expand All @@ -58,6 +72,8 @@ export class JProfByBotStack extends cdk.Stack {
votesTable.grantReadWriteData(lambdaWebhook);
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',
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ 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: quizoji/dynamodb/src/test/resources/seed.sh
- run: ./gradlew clean dbTest
- uses: actions/upload-artifact@v2
if: always()
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
9 changes: 9 additions & 0 deletions dialogs/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
}

dependencies {
api(libs.tgbotapi.core)
implementation(libs.kotlinx.serialization.core)
}
30 changes: 30 additions & 0 deletions dialogs/dynamodb/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
plugins {
kotlin("jvm")
}

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)

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")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 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

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 + TgBotAPI.module }

fun Map<String, AttributeValue>.toDialogState(): DialogState = json.decodeFromString(this["value"].toString("value"))

fun DialogState.toAttributes(): Map<String, AttributeValue> = mapOf(
"userId" to this.userId.toAttributeValue(),
"chatId" to this.chatId.toAttributeValue(),
"value" to json.encodeToString(this).toAttributeValue(),
)
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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 quizojiWaitingForQuestionToAttributes() {
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 quizojiAttributesToWaitingForQuestion() {
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()
)
}

@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()
)
}
}
19 changes: 19 additions & 0 deletions dialogs/dynamodb/src/test/resources/dialog-states.items.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
]
}
27 changes: 27 additions & 0 deletions dialogs/dynamodb/src/test/resources/dialog-states.table.json
Original file line number Diff line number Diff line change
@@ -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
}
}
8 changes: 8 additions & 0 deletions dialogs/dynamodb/src/test/resources/seed.sh
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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(dialogState: DialogState)

suspend fun delete(chatId: Long, userId: Long)
}
Loading