diff --git a/.deploy/lambda/bin/JProfByBotStack.ts b/.deploy/lambda/bin/JProfByBotStack.ts index e5d25338..1b218db4 100644 --- a/.deploy/lambda/bin/JProfByBotStack.ts +++ b/.deploy/lambda/bin/JProfByBotStack.ts @@ -11,6 +11,10 @@ if (process.env.TOKEN_YOUTUBE_API == null) { throw new Error('Undefined TOKEN_YOUTUBE_API') } +if (process.env.EMAIL_DAILY_URBAN_DICTIONARY == null) { + throw new Error('Undefined EMAIL_DAILY_URBAN_DICTIONARY') +} + const app = new cdk.App(); new JProfByBotStack( app, @@ -18,6 +22,7 @@ new JProfByBotStack( { telegramToken: process.env.TOKEN_TELEGRAM_BOT, youtubeToken: process.env.TOKEN_YOUTUBE_API, + dailyUrbanDictionaryEmail: process.env.EMAIL_DAILY_URBAN_DICTIONARY, env: { region: 'us-east-1' } diff --git a/.deploy/lambda/lib/JProfByBotStack.ts b/.deploy/lambda/lib/JProfByBotStack.ts index 7093500f..b6206179 100644 --- a/.deploy/lambda/lib/JProfByBotStack.ts +++ b/.deploy/lambda/lib/JProfByBotStack.ts @@ -5,6 +5,8 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks'; +import * as ses from 'aws-cdk-lib/aws-ses'; +import * as sesActions from 'aws-cdk-lib/aws-ses-actions'; import {JProfByBotStackProps} from "./JProfByBotStackProps"; export class JProfByBotStack extends cdk.Stack { @@ -63,6 +65,18 @@ export class JProfByBotStack extends cdk.Stack { billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, removalPolicy: cdk.RemovalPolicy.DESTROY, }); + const languageRoomsTable = new dynamodb.Table(this, 'jprof-by-bot-table-language-rooms', { + tableName: 'jprof-by-bot-table-language-rooms', + partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING}, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + const urbanWordsOfTheDayTable = new dynamodb.Table(this, 'jprof-by-bot-table-urban-words-of-the-day', { + tableName: 'jprof-by-bot-table-urban-words-of-the-day', + partitionKey: {name: 'date', type: dynamodb.AttributeType.STRING}, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); pinsTable.addGlobalSecondaryIndex({ indexName: 'chatId', @@ -117,6 +131,7 @@ export class JProfByBotStack extends cdk.Stack { compatibleRuntimes: [lambda.Runtime.JAVA_11], }); + const lambdaWebhookTimeout = cdk.Duration.seconds(29); const lambdaWebhook = new lambda.Function(this, 'jprof-by-bot-lambda-webhook', { functionName: 'jprof-by-bot-lambda-webhook', runtime: lambda.Runtime.JAVA_11, @@ -124,8 +139,10 @@ export class JProfByBotStack extends cdk.Stack { layerLibGL, layerLibfontconfig, ], - timeout: cdk.Duration.seconds(30), - memorySize: 1024, + timeout: lambdaWebhookTimeout, + maxEventAge: cdk.Duration.minutes(5), + retryAttempts: 0, + memorySize: 512, code: lambda.Code.fromAsset('../../launchers/lambda/build/libs/jprof_by_bot-launchers-lambda-all.jar'), handler: 'by.jprof.telegram.bot.launchers.lambda.JProf', environment: { @@ -138,12 +155,49 @@ export class JProfByBotStack extends cdk.Stack { 'TABLE_MONIES': moniesTable.tableName, 'TABLE_PINS': pinsTable.tableName, 'TABLE_TIMEZONES': timezonesTable.tableName, + 'TABLE_LANGUAGE_ROOMS': languageRoomsTable.tableName, + 'TABLE_URBAN_WORDS_OF_THE_DAY': urbanWordsOfTheDayTable.tableName, 'STATE_MACHINE_UNPINS': stateMachineUnpin.stateMachineArn, 'TOKEN_TELEGRAM_BOT': props.telegramToken, 'TOKEN_YOUTUBE_API': props.youtubeToken, + 'TIMEOUT': lambdaWebhookTimeout.toMilliseconds().toString(), }, }); + (lambdaWebhook.node.defaultChild as lambda.CfnFunction).snapStart = { + applyOn: 'PublishedVersions' + }; + + const lambdaDailyUrbanDictionary = new lambda.Function(this, 'jprof-by-bot-lambda-daily-urban-dictionary', { + functionName: 'jprof-by-bot-lambda-daily-urban-dictionary', + runtime: lambda.Runtime.JAVA_11, + timeout: cdk.Duration.seconds(30), + retryAttempts: 0, + memorySize: 512, + code: lambda.Code.fromAsset('../../english/urban-dictionary-daily/build/libs/jprof_by_bot-english-urban-dictionary-daily-all.jar'), + handler: 'by.jprof.telegram.bot.english.urban_dictionary_daily.Handler', + environment: { + 'LOG_THRESHOLD': 'DEBUG', + 'TABLE_URBAN_WORDS_OF_THE_DAY': urbanWordsOfTheDayTable.tableName, + 'TABLE_LANGUAGE_ROOMS': languageRoomsTable.tableName, + 'TOKEN_TELEGRAM_BOT': props.telegramToken, + 'STATE_MACHINE_UNPINS': stateMachineUnpin.stateMachineArn, + } + }); + + new ses.ReceiptRuleSet(this, 'jprof-by-bot-receipt-rule-set-daily-urbandictionary', { + receiptRuleSetName: 'jprof-by-bot-receipt-rule-set-daily-urbandictionary', + rules: [ + { + receiptRuleName: 'jprof-by-bot-receipt-rule-daily-urbandictionary', + recipients: [props.dailyUrbanDictionaryEmail], + actions: [ + new sesActions.Lambda({function: lambdaDailyUrbanDictionary}) + ] + } + ], + }); + votesTable.grantReadWriteData(lambdaWebhook); youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook); @@ -161,7 +215,14 @@ export class JProfByBotStack extends cdk.Stack { timezonesTable.grantReadWriteData(lambdaWebhook); - stateMachineUnpin.grantStartExecution(lambdaWebhook) + languageRoomsTable.grantReadWriteData(lambdaWebhook); + languageRoomsTable.grantReadData(lambdaDailyUrbanDictionary); + + urbanWordsOfTheDayTable.grantWriteData(lambdaDailyUrbanDictionary); + urbanWordsOfTheDayTable.grantReadData(lambdaWebhook); + + stateMachineUnpin.grantStartExecution(lambdaWebhook); + stateMachineUnpin.grantStartExecution(lambdaDailyUrbanDictionary); const api = new apigateway.RestApi(this, 'jprof-by-bot-api', { restApiName: 'jprof-by-bot-api', @@ -177,7 +238,7 @@ export class JProfByBotStack extends cdk.Stack { api.root .addResource(props.telegramToken.replace(':', '_')) - .addMethod('POST', new apigateway.LambdaIntegration(lambdaWebhook)); + .addMethod('POST', new apigateway.LambdaIntegration(lambdaWebhook.currentVersion)); new cdk.CfnOutput(this, 'URL', { value: api.deploymentStage.urlForPath() diff --git a/.deploy/lambda/lib/JProfByBotStackProps.ts b/.deploy/lambda/lib/JProfByBotStackProps.ts index 550eda9e..a7aab471 100644 --- a/.deploy/lambda/lib/JProfByBotStackProps.ts +++ b/.deploy/lambda/lib/JProfByBotStackProps.ts @@ -3,4 +3,5 @@ import * as cdk from 'aws-cdk-lib'; export interface JProfByBotStackProps extends cdk.StackProps { readonly telegramToken: string; readonly youtubeToken: string; + readonly dailyUrbanDictionaryEmail: string; } diff --git a/.deploy/lambda/package-lock.json b/.deploy/lambda/package-lock.json index 293e77ff..763ae76d 100644 --- a/.deploy/lambda/package-lock.json +++ b/.deploy/lambda/package-lock.json @@ -8,7 +8,7 @@ "name": "jprof-by-bot", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "2.49.0", + "aws-cdk-lib": "2.53.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" }, @@ -19,7 +19,7 @@ "@types/jest": "^27.5.2", "@types/node": "10.17.27", "@types/prettier": "2.6.0", - "aws-cdk": "2.49.0", + "aws-cdk": "2.53.0", "jest": "^27.5.1", "jest-junit": "^14.0.1", "ts-jest": "^27.1.4", @@ -40,6 +40,21 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.26.tgz", + "integrity": "sha512-j6bp4iDIZlqAlmACpBCZuCZjLYNeJccQFAPmvgjG9edLBGvVyJZvBT7m39lVtjfEyl8EyznCv7czOFaKsZH2Zg==" + }, + "node_modules/@aws-cdk/asset-kubectl-v20": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.1.tgz", + "integrity": "sha512-U1ntiX8XiMRRRH5J1IdC+1t5CE89015cwyt5U63Cpk0GnMlN5+h9WsWMlKlPXZR4rdq/m806JRlBMRpBUB2Dhw==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v5": { + "version": "2.0.32", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.32.tgz", + "integrity": "sha512-w1MaBSyR1vT+glPBNd0smp9MPWbz1z/o3+N59MYpjtA4lW845hb8LwthzvbPUzb0YglUfrnOj64i9BFe6BXeZA==" + }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -1227,9 +1242,9 @@ "dev": true }, "node_modules/aws-cdk": { - "version": "2.49.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.49.0.tgz", - "integrity": "sha512-C+/B8mO85wlKv+/pDtm1OlObIRVaj/XSpWOR31Q6TgxVxJeLnH2K4B/aik+l7yZK45arpUpK2Ka0fvSfTDJvqQ==", + "version": "2.53.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.53.0.tgz", + "integrity": "sha512-Tt/H7quNxA/Z0COwHnznnW7+6iJ1oUuJ/dZ5kjPTBtbgMvxRXKzZLDTanlTaET4Q7l6PprQPbUkWkYpka6v0qw==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -1242,9 +1257,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.49.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.49.0.tgz", - "integrity": "sha512-HMrV41VaYVLFhm5i35bXuxiiib8IXPwuspDF7W3LJ4WVwxQZWx4eGcvRAdaXuNM7dDwM52cF5BNl8lYa/xCt5Q==", + "version": "2.53.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.53.0.tgz", + "integrity": "sha512-ADpJ9luJxzmOrP/GJ37nA1YtdNmsOsjE+NZnQOpB7YL0spUxZJGNVAselaFLSDZmIsC6d9mSnW81fj4SDP1gAw==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -1257,6 +1272,9 @@ "yaml" ], "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.9", + "@aws-cdk/asset-kubectl-v20": "^2.1.1", + "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.15", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^9.1.0", @@ -4569,6 +4587,21 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@aws-cdk/asset-awscli-v1": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.26.tgz", + "integrity": "sha512-j6bp4iDIZlqAlmACpBCZuCZjLYNeJccQFAPmvgjG9edLBGvVyJZvBT7m39lVtjfEyl8EyznCv7czOFaKsZH2Zg==" + }, + "@aws-cdk/asset-kubectl-v20": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.1.tgz", + "integrity": "sha512-U1ntiX8XiMRRRH5J1IdC+1t5CE89015cwyt5U63Cpk0GnMlN5+h9WsWMlKlPXZR4rdq/m806JRlBMRpBUB2Dhw==" + }, + "@aws-cdk/asset-node-proxy-agent-v5": { + "version": "2.0.32", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.32.tgz", + "integrity": "sha512-w1MaBSyR1vT+glPBNd0smp9MPWbz1z/o3+N59MYpjtA4lW845hb8LwthzvbPUzb0YglUfrnOj64i9BFe6BXeZA==" + }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -5519,19 +5552,22 @@ "dev": true }, "aws-cdk": { - "version": "2.49.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.49.0.tgz", - "integrity": "sha512-C+/B8mO85wlKv+/pDtm1OlObIRVaj/XSpWOR31Q6TgxVxJeLnH2K4B/aik+l7yZK45arpUpK2Ka0fvSfTDJvqQ==", + "version": "2.53.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.53.0.tgz", + "integrity": "sha512-Tt/H7quNxA/Z0COwHnznnW7+6iJ1oUuJ/dZ5kjPTBtbgMvxRXKzZLDTanlTaET4Q7l6PprQPbUkWkYpka6v0qw==", "dev": true, "requires": { "fsevents": "2.3.2" } }, "aws-cdk-lib": { - "version": "2.49.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.49.0.tgz", - "integrity": "sha512-HMrV41VaYVLFhm5i35bXuxiiib8IXPwuspDF7W3LJ4WVwxQZWx4eGcvRAdaXuNM7dDwM52cF5BNl8lYa/xCt5Q==", + "version": "2.53.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.53.0.tgz", + "integrity": "sha512-ADpJ9luJxzmOrP/GJ37nA1YtdNmsOsjE+NZnQOpB7YL0spUxZJGNVAselaFLSDZmIsC6d9mSnW81fj4SDP1gAw==", "requires": { + "@aws-cdk/asset-awscli-v1": "^2.2.9", + "@aws-cdk/asset-kubectl-v20": "^2.1.1", + "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.15", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^9.1.0", diff --git a/.deploy/lambda/package.json b/.deploy/lambda/package.json index a7f0c936..9b3503dc 100644 --- a/.deploy/lambda/package.json +++ b/.deploy/lambda/package.json @@ -16,13 +16,13 @@ "@types/prettier": "2.6.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "aws-cdk": "2.49.0", + "aws-cdk": "2.53.0", "ts-node": "^10.9.1", "typescript": "~3.9.7", "jest-junit": "^14.0.1" }, "dependencies": { - "aws-cdk-lib": "2.49.0", + "aws-cdk-lib": "2.53.0", "constructs": "^10.0.0", "source-map-support": "^0.5.21" } diff --git a/.deploy/lambda/test/JProfByBotStack.test.ts b/.deploy/lambda/test/JProfByBotStack.test.ts index 2480e8c0..9711bee3 100644 --- a/.deploy/lambda/test/JProfByBotStack.test.ts +++ b/.deploy/lambda/test/JProfByBotStack.test.ts @@ -7,6 +7,7 @@ describe('JProfByBotStack', () => { const stack = new JProfByBotStack(app, 'JProfByBotStack', { telegramToken: 'TOKEN_TELEGRAM_BOT', youtubeToken: 'TOKEN_YOUTUBE_API', + dailyUrbanDictionaryEmail: 'EMAIL_DAILY_URBAN_DICTIONARY', env: {region: 'us-east-1'} }); const template = Template.fromStack(stack); diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index eba96305..337fbd58 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -57,6 +57,8 @@ jobs: - run: monies/dynamodb/src/test/resources/seed.sh - run: pins/dynamodb/src/test/resources/seed.sh - run: times/timezones/dynamodb/src/test/resources/seed.sh + - run: english/language-rooms/dynamodb/src/test/resources/seed.sh + - run: english/urban-word-of-the-day/dynamodb/src/test/resources/seed.sh - uses: gradle/gradle-build-action@v2 with: arguments: clean dbTest @@ -69,6 +71,27 @@ jobs: **/build/test-results **/build/reports + integration-test: + name: Integration test + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: adopt + - uses: gradle/gradle-build-action@v2 + with: + arguments: clean integrationTest + cache-read-only: ${{ github.ref != 'refs/heads/master' }} + - uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: | + **/build/test-results + **/build/reports + cdk-test: name: CDK test runs-on: ubuntu-20.04 @@ -105,6 +128,7 @@ jobs: needs: - test - db-test + - integration-test - cdk-test if: always() steps: @@ -116,10 +140,12 @@ jobs: report_paths: |- **/test-results/test/TEST-*.xml **/test-results/dbTest/TEST-*.xml + **/test-results/integrationTest/TEST-*.xml **/junit.xml check_name: |- Test reports DB test reports + Integration test reports CDK test reports include_passed: true github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b675d8e2..b2e27cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build .idea *.private.env.json + +.env diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 00000000..7b3ddf93 Binary files /dev/null and b/.idea/icon.png differ diff --git a/.idea/icon_dark.png b/.idea/icon_dark.png new file mode 100644 index 00000000..861b86bd Binary files /dev/null and b/.idea/icon_dark.png differ diff --git a/LICENSE.adoc b/LICENSE.adoc index eb42b6de..87262f0d 100644 --- a/LICENSE.adoc +++ b/LICENSE.adoc @@ -1,6 +1,6 @@ MIT License -Copyright ⓒ 2021 mailto:jug@jprof.by[Java Professionals BY community]. +Copyright ⓒ 2023 mailto:jug@jprof.by[Java Professionals BY community]. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.adoc b/README.adoc index 3967e459..2411b957 100644 --- a/README.adoc +++ b/README.adoc @@ -11,7 +11,8 @@ Official Telegram bot of Java Professionals BY community. * Allows users to create polls with reply buttons (just like https://t.me/like[`@like`] bot) * Converts some currencies to EUR and USD * Posts scheduled messages from this repo's `posts` branch -* Expand LeetCode links +* Expands LeetCode links +* Regulates our English Rooms & teaches us new English words So, it just brings some fun and interactivity in our chat. diff --git a/build.gradle.kts b/build.gradle.kts index b79c951a..cd417b68 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,7 @@ subprojects { } withType { useJUnitPlatform { - excludeTags("db") + excludeTags("db", "it") } testLogging { showStandardStreams = true diff --git a/core/src/main/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipeline.kt b/core/src/main/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipeline.kt index 25747ce1..e4e886f8 100644 --- a/core/src/main/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipeline.kt +++ b/core/src/main/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipeline.kt @@ -6,20 +6,29 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withTimeoutOrNull import org.apache.logging.log4j.LogManager class UpdateProcessingPipeline( - private val processors: List + private val processors: List, + private val timeout: Long, ) { companion object { private val logger = LogManager.getLogger(UpdateProcessingPipeline::class.java)!! } fun process(update: Update) = runBlocking { - supervisorScope { - processors - .map { launch(exceptionHandler(it)) { it.process(update) } } - .joinAll() + withTimeoutOrNull(timeout) { + supervisorScope { + processors + .map { + launch(exceptionHandler(it)) { + logger.debug("Processing update with ${it::class.simpleName}") + it.process(update) + } + } + .joinAll() + } } } diff --git a/core/src/test/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipelineTest.kt b/core/src/test/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipelineTest.kt index abb0bf90..d79b989d 100644 --- a/core/src/test/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipelineTest.kt +++ b/core/src/test/kotlin/by/jprof/telegram/bot/core/UpdateProcessingPipelineTest.kt @@ -2,47 +2,80 @@ package by.jprof.telegram.bot.core import dev.inmo.tgbotapi.types.update.abstracts.UnknownUpdate import dev.inmo.tgbotapi.types.update.abstracts.Update +import java.time.Duration +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.random.Random import kotlinx.coroutines.delay import kotlinx.serialization.json.JsonNull -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTimeout +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test -import java.time.Duration -import java.util.concurrent.atomic.AtomicBoolean internal class UpdateProcessingPipelineTest { @Test fun process() { val completionFlags = (1..5).map { AtomicBoolean(false) } val sut = UpdateProcessingPipeline( - completionFlags.mapIndexed { index, atomicBoolean -> + processors = completionFlags.mapIndexed { index, atomicBoolean -> Delay((index + 1) * 1000L, atomicBoolean) - } + }, + timeout = 10_000, ) - Assertions.assertTimeout(Duration.ofMillis(6000)) { + assertTimeout(Duration.ofMillis(6000)) { sut.process(UnknownUpdate(1L, "", JsonNull)) } - Assertions.assertTrue(completionFlags.all { it.get() }) + assertTrue(completionFlags.all { it.get() }) } @Test fun processWithBroken() { val completionFlags = (1..5).map { AtomicBoolean(false) } val sut = UpdateProcessingPipeline( - completionFlags.mapIndexed { index, atomicBoolean -> + processors = completionFlags.mapIndexed { index, atomicBoolean -> when (index % 2) { 0 -> Delay((index + 1) * 1000L, atomicBoolean) else -> Fail() } - } + }, + timeout = 10_000, ) - Assertions.assertTimeout(Duration.ofMillis(6000)) { + assertTimeout(Duration.ofMillis(6000)) { sut.process(UnknownUpdate(1L, "", JsonNull)) } completionFlags.forEachIndexed { index, atomicBoolean -> if (index % 2 == 0) { - Assertions.assertTrue(atomicBoolean.get()) + assertTrue(atomicBoolean.get()) + } + } + } + + @Test + fun processWithTimeout() { + val completionFlags = (1..6).map { AtomicBoolean(false) } + val sut = UpdateProcessingPipeline( + processors = completionFlags.mapIndexed { index, atomicBoolean -> + when (index % 3) { + 0 -> Delay(Long.MAX_VALUE, atomicBoolean) + 1 -> Delay(Random.nextLong(0, 100), atomicBoolean) + else -> Fail() + } + }, + timeout = 1000, + ) + + assertTimeout(Duration.ofMillis(2000)) { + sut.process(UnknownUpdate(1L, "", JsonNull)) + } + completionFlags.forEachIndexed { index, atomicBoolean -> + if (index % 3 == 1) { + assertTrue(atomicBoolean.get()) + } + when (index % 3) { + 1 -> assertTrue(atomicBoolean.get()) + else -> assertFalse(atomicBoolean.get()) } } } diff --git a/english/README.adoc b/english/README.adoc new file mode 100644 index 00000000..32c06f99 --- /dev/null +++ b/english/README.adoc @@ -0,0 +1,8 @@ += English + +This is an umbrella feature for several smaller sub-features: + +* The bot sends https://www.urbandictionary.com[Urban Word of the Day] into English rooms. +* The bot explains emphasised text parts in English rooms. +* The bot tolerates no stupid questions. +* The bot tolerates no other languages, but English in English rooms. Remember: every motherfucker must speak English from his heart in an English room! diff --git a/english/build.gradle.kts b/english/build.gradle.kts new file mode 100644 index 00000000..7c01b7e9 --- /dev/null +++ b/english/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.core) + implementation(projects.english.languageRooms) + implementation(projects.english.urbanWordOfTheDay) + implementation(projects.english.urbanWordOfTheDayFormatter) + implementation(projects.english.urbanDictionary) + implementation(projects.english.dictionaryapiDev) + implementation(libs.log4j.api) + + 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/english/dictionaryapi-dev/README.adoc b/english/dictionaryapi-dev/README.adoc new file mode 100644 index 00000000..5cb79a67 --- /dev/null +++ b/english/dictionaryapi-dev/README.adoc @@ -0,0 +1,3 @@ += English / dictionaryapi.dev + +https://dictionaryapi.dev[dictionaryapi.dev] client. diff --git a/english/dictionaryapi-dev/build.gradle.kts b/english/dictionaryapi-dev/build.gradle.kts new file mode 100644 index 00000000..7fba757b --- /dev/null +++ b/english/dictionaryapi-dev/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") +} + +dependencies { + implementation(platform(libs.ktor.bom)) + + implementation(libs.ktor.client.apache) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.log4j.api) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.mockk) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.log4j.core) +} + +tasks { + val integrationTest by registering(Test::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Runs the integration tests." + shouldRunAfter("test") + outputs.upToDateWhen { false } + useJUnitPlatform { + includeTags("it") + } + } +} diff --git a/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/DictionaryAPIDotDevClient.kt b/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/DictionaryAPIDotDevClient.kt new file mode 100644 index 00000000..e758be2b --- /dev/null +++ b/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/DictionaryAPIDotDevClient.kt @@ -0,0 +1,5 @@ +package by.jprof.telegram.bot.english.dictionaryapi_dev + +interface DictionaryAPIDotDevClient { + suspend fun define(term: String): Collection +} diff --git a/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/KtorDictionaryAPIDotDevClient.kt b/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/KtorDictionaryAPIDotDevClient.kt new file mode 100644 index 00000000..6bd26ec7 --- /dev/null +++ b/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/KtorDictionaryAPIDotDevClient.kt @@ -0,0 +1,35 @@ +package by.jprof.telegram.bot.english.dictionaryapi_dev + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.apache.Apache +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.request.url +import io.ktor.http.encodeURLPathPart +import io.ktor.serialization.kotlinx.json.json +import java.io.Closeable +import kotlinx.serialization.json.Json + +class KtorDictionaryAPIDotDevClient( + private val baseUrl: String = "https://api.dictionaryapi.dev/api/v2" +) : DictionaryAPIDotDevClient, Closeable { + private val client = HttpClient(Apache) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + } + ) + } + } + + override suspend fun define(term: String): Collection = + client.get { + url("$baseUrl/entries/en/${term.encodeURLPathPart()}") + }.body() + + override fun close() { + client.close() + } +} diff --git a/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/Word.kt b/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/Word.kt new file mode 100644 index 00000000..a5a631f1 --- /dev/null +++ b/english/dictionaryapi-dev/src/main/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/Word.kt @@ -0,0 +1,42 @@ +package by.jprof.telegram.bot.english.dictionaryapi_dev + +import kotlinx.serialization.Serializable + +@Serializable +data class Word( + val word: String, + val phonetics: Collection? = null, + val meanings: Collection? = null, + val license: License? = null, + val sourceUrls: Collection? = null, +) + +@Serializable +data class Phonetic( + val text: String? = null, + val audio: String? = null, + val sourceUrl: String? = null, + val license: License? = null, +) + +@Serializable +data class Meaning( + val partOfSpeech: String, + val definitions: Collection, + val synonyms: Collection? = null, + val antonyms: Collection? = null, +) + +@Serializable +data class Definition( + val definition: String, + val example: String? = null, + val synonyms: Collection? = null, + val antonyms: Collection? = null, +) + +@Serializable +data class License( + val name: String, + val url: String, +) diff --git a/english/dictionaryapi-dev/src/test/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/KtorKtorDictionaryAPIDotDevClientIntegrationTest.kt b/english/dictionaryapi-dev/src/test/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/KtorKtorDictionaryAPIDotDevClientIntegrationTest.kt new file mode 100644 index 00000000..9b26a301 --- /dev/null +++ b/english/dictionaryapi-dev/src/test/kotlin/by/jprof/telegram/bot/english/dictionaryapi_dev/KtorKtorDictionaryAPIDotDevClientIntegrationTest.kt @@ -0,0 +1,26 @@ +package by.jprof.telegram.bot.english.dictionaryapi_dev + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@Tag("it") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class KtorKtorDictionaryAPIDotDevClientIntegrationTest { + private lateinit var sut: KtorDictionaryAPIDotDevClient + + @BeforeAll + internal fun setup() { + sut = KtorDictionaryAPIDotDevClient() + } + + @Test + fun define() = runBlocking { + val definitions = sut.define("motherfucker") + + assertTrue(definitions.isNotEmpty()) + } +} diff --git a/english/language-rooms/README.adoc b/english/language-rooms/README.adoc new file mode 100644 index 00000000..8974ffb1 --- /dev/null +++ b/english/language-rooms/README.adoc @@ -0,0 +1,3 @@ += English / Language Rooms + +Language Room model. diff --git a/english/language-rooms/build.gradle.kts b/english/language-rooms/build.gradle.kts new file mode 100644 index 00000000..444baaa3 --- /dev/null +++ b/english/language-rooms/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + kotlin("jvm") +} diff --git a/english/language-rooms/dynamodb/README.adoc b/english/language-rooms/dynamodb/README.adoc new file mode 100644 index 00000000..ebbae34c --- /dev/null +++ b/english/language-rooms/dynamodb/README.adoc @@ -0,0 +1,3 @@ += English / Language Rooms / DynamoDB + +DynamoDB DAO for Language Rooms. diff --git a/english/language-rooms/dynamodb/build.gradle.kts b/english/language-rooms/dynamodb/build.gradle.kts new file mode 100644 index 00000000..9d0457db --- /dev/null +++ b/english/language-rooms/dynamodb/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.english.languageRooms) + api(libs.dynamodb) + implementation(project.projects.utils.dynamodb) + 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/english/language-rooms/dynamodb/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomDAO.kt b/english/language-rooms/dynamodb/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomDAO.kt new file mode 100644 index 00000000..622debb1 --- /dev/null +++ b/english/language-rooms/dynamodb/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomDAO.kt @@ -0,0 +1,86 @@ +package by.jprof.telegram.bot.english.language_rooms.dynamodb.dao + +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.language_rooms.model.LanguageRoom +import by.jprof.telegram.bot.english.language_rooms.model.Violence +import by.jprof.telegram.bot.utils.dynamodb.toAttributeValue +import by.jprof.telegram.bot.utils.dynamodb.toBoolean +import by.jprof.telegram.bot.utils.dynamodb.toLong +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +class LanguageRoomDAO( + private val dynamoDb: DynamoDbAsyncClient, + private val table: String +) : LanguageRoomDAO { + override suspend fun save(languageRoom: LanguageRoom) { + withContext(Dispatchers.IO) { + dynamoDb.putItem { + it.tableName(table) + it.item(languageRoom.toAttributes()) + }.await() + } + } + + override suspend fun get(chatId: Long, threadId: Long?): LanguageRoom? { + return withContext(Dispatchers.IO) { + dynamoDb.getItem { + it.tableName(table) + it.key( + mapOf( + "id" to id(chatId, threadId).toAttributeValue() + ) + ) + }.await()?.item()?.takeUnless { it.isEmpty() }?.toLanguageRoom() + } + } + + override suspend fun getAll(): List { + return withContext(Dispatchers.IO) { + dynamoDb.scan { + it.tableName(table) + }.await()?.items()?.map { it.toLanguageRoom() } ?: emptyList() + } + } + + override suspend fun delete(chatId: Long, threadId: Long?) { + return withContext(Dispatchers.IO) { + dynamoDb.deleteItem { + it.tableName(table) + it.key( + mapOf( + "id" to id(chatId, threadId).toAttributeValue() + ) + ) + }.await() + } + } +} + +fun id(chatId: Long, threadId: Long?): String = "$chatId${if (threadId != null) ":$threadId" else ""}" + +val LanguageRoom.id: String + get() = id(chatId, threadId) + +fun LanguageRoom.toAttributes(): Map = buildMap { + put("id", this@toAttributes.id.toAttributeValue()) + put("chatId", this@toAttributes.chatId.toAttributeValue()) + this@toAttributes.threadId?.let { + put("threadId", it.toAttributeValue()) + } + put("language", this@toAttributes.language.name.toAttributeValue()) + put("violence", this@toAttributes.violence.name.toAttributeValue()) + put("urbanWordOfTheDay", this@toAttributes.urbanWordOfTheDay.toAttributeValue()) +} + +fun Map.toLanguageRoom(): LanguageRoom = LanguageRoom( + chatId = this["chatId"].toLong("chatId"), + threadId = this["threadId"]?.n()?.toLong(), + language = Language.valueOf(this["language"]?.s() ?: throw IllegalStateException("Missing language property")), + violence = Violence.valueOf(this["violence"]?.s() ?: throw IllegalStateException("Missing violence property")), + urbanWordOfTheDay = this["urbanWordOfTheDay"].toBoolean("urbanWordOfTheDay"), +) diff --git a/english/language-rooms/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomDAOTest.kt b/english/language-rooms/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomDAOTest.kt new file mode 100644 index 00000000..cd7456b3 --- /dev/null +++ b/english/language-rooms/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomDAOTest.kt @@ -0,0 +1,86 @@ +package by.jprof.telegram.bot.english.language_rooms.dynamodb.dao + +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.language_rooms.model.LanguageRoom +import by.jprof.telegram.bot.english.language_rooms.model.Violence +import by.jprof.telegram.bot.utils.aws_junit5.Endpoint +import kotlinx.coroutines.runBlocking +import me.madhead.aws_junit5.common.AWSClient +import me.madhead.aws_junit5.dynamo.v2.DynamoDB +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +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 LanguageRoomDAOTest { + @AWSClient(endpoint = Endpoint::class) + private lateinit var dynamoDB: DynamoDbAsyncClient + private lateinit var sut: LanguageRoomDAO + + @BeforeAll + internal fun setup() { + sut = LanguageRoomDAO(dynamoDB, "languageRooms") + } + + @Test + fun save() = runBlocking { + sut.save(languageRoom.copy(chatId = 11)) + } + + @Test + fun saveNoThreadId() = runBlocking { + sut.save(languageRoom.copy(chatId = 12, threadId = null)) + } + + @Test + fun get() = runBlocking { + assertEquals(languageRoom, sut.get(1, 101)) + } + + @Test + fun getNoThreadId() = runBlocking { + assertEquals(languageRoom.copy(chatId = 2, threadId = null), sut.get(2)) + } + + @Test + fun getUnexisting() = runBlocking { + assertNull(sut.get(-1, -2)) + assertNull(sut.get(-2)) + } + + @Test + fun getAll() = runBlocking { + val all = sut.getAll() + + assertTrue(all.contains(languageRoom)) + assertTrue(all.contains(languageRoom.copy(chatId = 2, threadId = null))) + } + + @Test + fun delete() = runBlocking { + sut.save(languageRoom.copy(chatId = 21)) + assertEquals(languageRoom.copy(chatId = 21), sut.get(21, 101)) + sut.delete(21, 101) + assertNull(sut.get(21, 101)) + + sut.save(languageRoom.copy(chatId = 22, threadId = null)) + assertEquals(languageRoom.copy(chatId = 22, threadId = null), sut.get(22)) + sut.delete(22) + assertNull(sut.get(22)) + } + + private val languageRoom + get() = LanguageRoom( + chatId = 1, + threadId = 101, + language = Language.ENGLISH, + violence = Violence.POLITE, + urbanWordOfTheDay = true, + ) +} diff --git a/english/language-rooms/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomTest.kt b/english/language-rooms/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomTest.kt new file mode 100644 index 00000000..301877ed --- /dev/null +++ b/english/language-rooms/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/language_rooms/dynamodb/dao/LanguageRoomTest.kt @@ -0,0 +1,60 @@ +package by.jprof.telegram.bot.english.language_rooms.dynamodb.dao + +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.language_rooms.model.LanguageRoom +import by.jprof.telegram.bot.english.language_rooms.model.Violence +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +internal class LanguageRoomTest { + @Test + fun toAttributes() { + assertEquals( + attributes, + languageRoom.toAttributes() + ) + } + + @Test + fun toAttributesNoThreadId() { + assertEquals( + attributes.toMutableMap().apply { this.remove("threadId"); this["id"] = AttributeValue.builder().s("1").build() }, + languageRoom.copy(threadId = null).toAttributes() + ) + } + + @Test + fun toLanguageRoom() { + assertEquals( + languageRoom, + attributes.toLanguageRoom() + ) + } + + @Test + fun toLanguageRoomNoThreadId() { + assertEquals( + languageRoom.copy(threadId = null), + attributes.toMutableMap().apply { this.remove("threadId") }.toLanguageRoom() + ) + } + + private val languageRoom + get() = LanguageRoom( + chatId = 1, + threadId = 101, + language = Language.ENGLISH, + violence = Violence.POLITE, + urbanWordOfTheDay = true, + ) + private val attributes + get() = mapOf( + "id" to AttributeValue.builder().s("1:101").build(), + "chatId" to AttributeValue.builder().n("1").build(), + "threadId" to AttributeValue.builder().n("101").build(), + "language" to AttributeValue.builder().s("ENGLISH").build(), + "violence" to AttributeValue.builder().s("POLITE").build(), + "urbanWordOfTheDay" to AttributeValue.builder().bool(true).build(), + ) +} diff --git a/english/language-rooms/dynamodb/src/test/resources/languageRooms.items.json b/english/language-rooms/dynamodb/src/test/resources/languageRooms.items.json new file mode 100644 index 00000000..723536b2 --- /dev/null +++ b/english/language-rooms/dynamodb/src/test/resources/languageRooms.items.json @@ -0,0 +1,49 @@ +{ + "languageRooms": [ + { + "PutRequest": { + "Item": { + "id": { + "S": "1:101" + }, + "chatId": { + "N": "1" + }, + "threadId": { + "N": "101" + }, + "language": { + "S": "ENGLISH" + }, + "violence": { + "S": "POLITE" + }, + "urbanWordOfTheDay": { + "BOOL": true + } + } + } + }, + { + "PutRequest": { + "Item": { + "id": { + "S": "2" + }, + "chatId": { + "N": "2" + }, + "language": { + "S": "ENGLISH" + }, + "violence": { + "S": "POLITE" + }, + "urbanWordOfTheDay": { + "BOOL": true + } + } + } + } + ] +} diff --git a/english/language-rooms/dynamodb/src/test/resources/languageRooms.table.json b/english/language-rooms/dynamodb/src/test/resources/languageRooms.table.json new file mode 100644 index 00000000..fcf74cc4 --- /dev/null +++ b/english/language-rooms/dynamodb/src/test/resources/languageRooms.table.json @@ -0,0 +1,19 @@ +{ + "TableName": "languageRooms", + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 1, + "WriteCapacityUnits": 1 + } +} diff --git a/english/language-rooms/dynamodb/src/test/resources/seed.sh b/english/language-rooms/dynamodb/src/test/resources/seed.sh new file mode 100755 index 00000000..6c0a0d25 --- /dev/null +++ b/english/language-rooms/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 languageRooms || true +aws --endpoint-url "${DYNAMODB_URL}" dynamodb create-table --cli-input-json file://english/language-rooms/dynamodb/src/test/resources/languageRooms.table.json +aws --endpoint-url "${DYNAMODB_URL}" dynamodb batch-write-item --request-items file://english/language-rooms/dynamodb/src/test/resources/languageRooms.items.json diff --git a/english/language-rooms/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/dao/LanguageRoomDAO.kt b/english/language-rooms/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/dao/LanguageRoomDAO.kt new file mode 100644 index 00000000..a812ac31 --- /dev/null +++ b/english/language-rooms/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/dao/LanguageRoomDAO.kt @@ -0,0 +1,13 @@ +package by.jprof.telegram.bot.english.language_rooms.dao + +import by.jprof.telegram.bot.english.language_rooms.model.LanguageRoom + +interface LanguageRoomDAO { + suspend fun save(languageRoom: LanguageRoom) + + suspend fun get(chatId: Long, threadId: Long? = null): LanguageRoom? + + suspend fun getAll(): List + + suspend fun delete(chatId: Long, threadId: Long? = null) +} diff --git a/english/language-rooms/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/model/LanguageRoom.kt b/english/language-rooms/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/model/LanguageRoom.kt new file mode 100644 index 00000000..4f2ed4f4 --- /dev/null +++ b/english/language-rooms/src/main/kotlin/by/jprof/telegram/bot/english/language_rooms/model/LanguageRoom.kt @@ -0,0 +1,18 @@ +package by.jprof.telegram.bot.english.language_rooms.model + +enum class Language { + ENGLISH, +} + +enum class Violence { + POLITE, + MOTHERFUCKER, +} + +data class LanguageRoom( + val chatId: Long, + val threadId: Long?, + val language: Language, + val violence: Violence, + val urbanWordOfTheDay: Boolean, +) diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/EnglishCommandUpdateProcessor.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/EnglishCommandUpdateProcessor.kt new file mode 100644 index 00000000..b26997a5 --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/EnglishCommandUpdateProcessor.kt @@ -0,0 +1,86 @@ +package by.jprof.telegram.bot.english + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.language_rooms.model.LanguageRoom +import by.jprof.telegram.bot.english.language_rooms.model.Violence +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember +import dev.inmo.tgbotapi.extensions.api.send.media.sendAnimation +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asBaseMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asBotCommandTextSource +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.extensions.utils.extensions.raw.from +import dev.inmo.tgbotapi.requests.abstracts.MultipartFile +import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember +import dev.inmo.tgbotapi.types.message.MarkdownV2ParseMode +import dev.inmo.tgbotapi.types.update.abstracts.Update +import io.ktor.utils.io.streams.asInput +import org.apache.logging.log4j.LogManager + +class EnglishCommandUpdateProcessor( + private val languageRoomDAO: LanguageRoomDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(EnglishCommandUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val update = update.asBaseMessageUpdate() ?: return + val message = update.data.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + val (_, argument) = (content.textSources + null) + .zipWithNext() + .firstOrNull { it.first?.asBotCommandTextSource()?.command == "english" } ?: return + val roomId = update.data.chat.id + val languageRoom = languageRoomDAO.get(roomId.chatId, roomId.threadId) + val englishRoom = LanguageRoom(roomId.chatId, roomId.threadId, Language.ENGLISH, Violence.POLITE, true) + + if (argument == null) { + bot.reply( + to = message, + text = if ((languageRoom == null) || (languageRoom.language != Language.ENGLISH)) { + "This is not an English Room\\!" + } else { + "This is ${if (languageRoom.violence == Violence.MOTHERFUCKER) "a motherfucking \uD83E\uDD2C" else "an"} English Room\\!" + }, + parseMode = MarkdownV2ParseMode, + ) + } else { + val member = bot.getChatMember(message.chat.id, message.from ?: return) + + if (member is AdministratorChatMember) { + if (argument.source.trim() == "on") { + val languageRoomConfig = (languageRoom ?: englishRoom).copy(language = Language.ENGLISH, violence = Violence.POLITE) + + languageRoomDAO.save(languageRoomConfig) + bot.reply(to = message, text = "This room is set to be an English Room\\!", parseMode = MarkdownV2ParseMode) + } else if ((argument.source.trim() == "motherfucker") || (argument.source.trim() == "motherfuckers")) { + val languageRoomConfig = (languageRoom + ?: englishRoom).copy(language = Language.ENGLISH, violence = Violence.MOTHERFUCKER) + + languageRoomDAO.save(languageRoomConfig) + bot.reply(to = message, text = "This room is set to be a motherfucking \uD83E\uDD2C English Room\\!", parseMode = MarkdownV2ParseMode) + } else if ((argument.source.trim() == "off") && (languageRoom?.language == Language.ENGLISH)) { + languageRoomDAO.delete(roomId.chatId, roomId.threadId) + bot.reply(to = message, text = "Ok, this is not an English Room anymore\\!", parseMode = MarkdownV2ParseMode) + } + } else { + bot.sendAnimation( + chat = message.chat, + animation = MultipartFile( + filename = "no power.gif", + inputSource = { + this::class.java.getResourceAsStream("/no power.gif").asInput() + }, + ), + replyToMessageId = message.messageId, + ) + } + } + } +} diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/ExplainerUpdateProcessor.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/ExplainerUpdateProcessor.kt new file mode 100644 index 00000000..e130dfdf --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/ExplainerUpdateProcessor.kt @@ -0,0 +1,230 @@ +package by.jprof.telegram.bot.english + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.english.dictionaryapi_dev.DictionaryAPIDotDevClient +import by.jprof.telegram.bot.english.dictionaryapi_dev.Meaning +import by.jprof.telegram.bot.english.dictionaryapi_dev.Phonetic +import by.jprof.telegram.bot.english.dictionaryapi_dev.Word +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.urban_dictionary.Definition +import by.jprof.telegram.bot.english.urban_dictionary.UrbanDictionaryClient +import by.jprof.telegram.bot.english.utils.iVeExplainedSomeWordsForYou +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asBaseMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.message.MarkdownV2 +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.textsources.BoldTextSource +import dev.inmo.tgbotapi.types.message.textsources.CodeTextSource +import dev.inmo.tgbotapi.types.message.textsources.ItalicTextSource +import dev.inmo.tgbotapi.types.message.textsources.UnderlineTextSource +import dev.inmo.tgbotapi.types.message.textsources.bold +import dev.inmo.tgbotapi.types.message.textsources.link +import dev.inmo.tgbotapi.types.message.textsources.regular +import dev.inmo.tgbotapi.types.message.textsources.underline +import dev.inmo.tgbotapi.types.update.abstracts.Update +import java.time.Duration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.time.withTimeoutOrNull +import org.apache.logging.log4j.LogManager + +class ExplainerUpdateProcessor( + private val languageRoomDAO: LanguageRoomDAO, + private val urbanDictionaryClient: UrbanDictionaryClient, + private val dictionaryapiDevClient: DictionaryAPIDotDevClient, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(ExplainerUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val update = update.asBaseMessageUpdate() ?: return + val roomId = update.data.chat.id + val message = update.data.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + + if ( + languageRoomDAO.get(roomId.chatId, roomId.threadId)?.takeIf { it.language == Language.ENGLISH } == null + ) { + return + } + + val emphasizedWords = extractEmphasizedWords(content) + + logger.debug("Emphasized words: $emphasizedWords") + + val explanations = fetchExplanations(emphasizedWords) + + logger.debug("Explanations: $explanations") + + explanations.keys.sorted().forEach { word -> + bot.reply( + to = message, + text = buildString { + appendLine(regular(iVeExplainedSomeWordsForYou()).markdownV2) + appendLine() + + dictionaryDotDevExplanations(explanations.dictionaryDotDev[word]) + urbanDictionaryExplanations(explanations.urbanDictionary[word]) + }, + parseMode = MarkdownV2, + disableWebPagePreview = true, + ) + } + } + + private fun extractEmphasizedWords(content: TextContent) = content.textSources + .filter { + it is BoldTextSource + || it is CodeTextSource + || it is ItalicTextSource + || it is UnderlineTextSource + + } + .map { it.source } + + private data class Explanations( + val urbanDictionary: Map>, + val dictionaryDotDev: Map>, + ) { + val keys by lazy { + urbanDictionary.keys + dictionaryDotDev.keys + } + } + + private suspend fun fetchExplanations(terms: Collection) = supervisorScope { + val urbanDictionaryDefinitions = terms.map { word -> + asyncWithTimeout(Duration.ofSeconds(5)) { + word to urbanDictionaryClient.define(word) + } + } + val dictionaryDevDefinitions = terms.map { word -> + asyncWithTimeout(Duration.ofSeconds(5)) { + word to dictionaryapiDevClient.define(word) + } + } + + Explanations( + urbanDictionary = urbanDictionaryDefinitions.await().toMap(), + dictionaryDotDev = dictionaryDevDefinitions.await().toMap(), + ) + } + + private suspend fun CoroutineScope.asyncWithTimeout( + timeout: Duration, + block: suspend CoroutineScope.() -> T, + ) = async { + withTimeoutOrNull(timeout) { block() } + } + + private suspend fun List>.await() = this.mapNotNull { + try { + it.await() + } catch (_: Exception) { + null + } + } + + private fun StringBuilder.dictionaryDotDevExplanations(dictionaryDotDevExplanations: Collection?) { + dictionaryDotDevExplanations?.let { definitions -> + definitions.take(3).forEachIndexed { index, definition -> + val link = definition.sourceUrls?.firstOrNull() + + if (link != null) { + append(bold(link(definition.word, link)).markdownV2) + } else { + append(bold(definition.word).markdownV2) + } + append(regular(" @ ").markdownV2) + append(link("Free Dictionary API", "https://dictionaryapi.dev").markdownV2) + appendLine() + appendLine() + + definition.phonetics?.takeUnless(Collection::isEmpty)?.let { phonetics -> + appendLine( + phonetics.mapNotNull { phonetic -> + val text = phonetic.text?.takeUnless(String::isNullOrBlank) + val audio = phonetic.audio?.takeUnless(String::isNullOrBlank) + + when { + text != null && audio != null -> link("\uD83D\uDDE3️ $text", audio).markdownV2 + text != null -> regular(text).markdownV2 + audio != null -> link("\uD83D\uDDE3️", audio).markdownV2 + else -> null + } + }.joinToString(regular(" • ").markdownV2) + ) + } + + definition.meanings?.takeUnless(Collection::isEmpty)?.take(3)?.toList()?.let { meanings -> + appendLine() + + meanings.forEachIndexed { meaningIndex, meaning -> + appendLine(underline(meaning.partOfSpeech).markdownV2) + + meaning.definitions.toList().let { definitions -> + definitions.forEachIndexed { definitionIndex, definition -> + append(regular("\uD83D\uDC49 ").markdownV2) + append(regular(definition.definition).markdownV2) + + definition.example?.let { example -> + appendLine() + append(regular("✍️ ").markdownV2) + append(regular(example).markdownV2) + } + + if (definitionIndex != definitions.lastIndex) { + appendLine() + appendLine() + } + } + } + + if (meaningIndex != meanings.lastIndex) { + appendLine() + appendLine() + } + } + } + } + } + } + + private fun StringBuilder.urbanDictionaryExplanations(urbanDictionaryExplanations: Collection?) { + urbanDictionaryExplanations?.let { + if (this.lines().size > 3) { + appendLine() + appendLine() + } + + val topDefinitions = it.sortedBy(Definition::thumbsUp).take(3) + + topDefinitions.forEachIndexed { index, definition -> + append(bold(link(definition.word, definition.permalink)).markdownV2) + append(regular(" @ ").markdownV2) + append(link("Urban Dictionary", "https://urbandictionary.com").markdownV2) + appendLine() + appendLine() + + append(regular("\uD83D\uDC49 ").markdownV2) + append(regular(definition.definition).markdownV2) + appendLine() + + append(regular("✍️ ").markdownV2) + append(regular(definition.example).markdownV2) + + if (index != topDefinitions.lastIndex) { + appendLine() + appendLine() + } + } + } + } +} diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/MotherfuckingUpdateProcessor.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/MotherfuckingUpdateProcessor.kt new file mode 100644 index 00000000..fd6394e6 --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/MotherfuckingUpdateProcessor.kt @@ -0,0 +1,71 @@ +package by.jprof.telegram.bot.english + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.language_rooms.model.Violence +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.delete +import dev.inmo.tgbotapi.extensions.api.send.media.sendAnimation +import dev.inmo.tgbotapi.extensions.api.send.media.sendPhoto +import dev.inmo.tgbotapi.extensions.utils.asBaseMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.requests.abstracts.MultipartFile +import dev.inmo.tgbotapi.types.update.abstracts.Update +import io.ktor.utils.io.streams.asInput +import kotlin.random.Random +import org.apache.logging.log4j.LogManager + +class MotherfuckingUpdateProcessor( + private val languageRoomDAO: LanguageRoomDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(MotherfuckingUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val update = update.asBaseMessageUpdate() ?: return + val roomId = update.data.chat.id + val message = update.data.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + + if ( + languageRoomDAO.get(roomId.chatId, roomId.threadId)?.takeIf { it.language == Language.ENGLISH && it.violence == Violence.MOTHERFUCKER } == null + ) { + return + } + + val latinLetters = content.text.count { it in 'a'..'z' || it in 'A'..'Z' } + + logger.debug("Latin letters detected: {}, total letters: {}", latinLetters, content.text.length) + + if (latinLetters.toDouble() / content.text.length < 2.toDouble() / 3) { + when (val i = Random.nextInt(4)) { + 0, 1 -> bot.sendPhoto( + chat = message.chat, + fileId = MultipartFile( + filename = "english$i.png", + inputSource = { + this::class.java.getResourceAsStream("/english$i.png").asInput() + }, + ), + replyToMessageId = message.messageId, + ) + + 2, 3 -> bot.sendAnimation( + chat = message.chat, + animation = MultipartFile( + filename = "english$i.gif", + inputSource = { + this::class.java.getResourceAsStream("/english$i.gif").asInput() + }, + ), + replyToMessageId = message.messageId, + ) + } + bot.delete(message) + } + } +} \ No newline at end of file diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/UrbanWordOfTheDayUpdateProcessor.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/UrbanWordOfTheDayUpdateProcessor.kt new file mode 100644 index 00000000..a91bd61e --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/UrbanWordOfTheDayUpdateProcessor.kt @@ -0,0 +1,69 @@ +package by.jprof.telegram.bot.english + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.urban_dictionary_definition_formatter.format +import by.jprof.telegram.bot.english.urban_word_of_the_day.dao.UrbanWordOfTheDayDAO +import by.jprof.telegram.bot.english.utils.downTo +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asBaseMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asBotCommandTextSource +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.message.MarkdownV2 +import dev.inmo.tgbotapi.types.update.abstracts.Update +import java.time.LocalDate +import org.apache.logging.log4j.LogManager + +class UrbanWordOfTheDayUpdateProcessor( + private val languageRoomDAO: LanguageRoomDAO, + private val urbanWordOfTheDayDAO: UrbanWordOfTheDayDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(UrbanWordOfTheDayUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val update = update.asBaseMessageUpdate() ?: return + val roomId = update.data.chat.id + val message = update.data.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + + if ( + content.textSources.none { + it.asBotCommandTextSource()?.command?.lowercase() in listOf("urban", "ud", "urbanword", "urbanwordoftheday") + } + ) { + return + } + + if ( + languageRoomDAO.get(roomId.chatId, roomId.threadId)?.takeIf { it.language == Language.ENGLISH } == null + ) { + return + } + + val urbanWordOfTheDay = (LocalDate.now() downTo LocalDate.now().minusDays(5)).asSequence().firstNotNullOfOrNull { + urbanWordOfTheDayDAO.get(it) + } + + if (urbanWordOfTheDay != null) { + bot.reply( + to = message, + text = urbanWordOfTheDay.format(), + parseMode = MarkdownV2, + disableWebPagePreview = true, + ) + } else { + bot.reply( + to = message, + text = "No recent words of the day \uD83D\uDE22", + parseMode = MarkdownV2, + disableWebPagePreview = true, + ) + } + } +} diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/WhatWordUpdateProcessor.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/WhatWordUpdateProcessor.kt new file mode 100644 index 00000000..da13aabc --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/WhatWordUpdateProcessor.kt @@ -0,0 +1,50 @@ +package by.jprof.telegram.bot.english + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.language_rooms.model.Violence +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.media.sendAnimation +import dev.inmo.tgbotapi.extensions.utils.asBaseMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.requests.abstracts.MultipartFile +import dev.inmo.tgbotapi.types.update.abstracts.Update +import io.ktor.utils.io.streams.asInput +import org.apache.logging.log4j.LogManager + +class WhatWordUpdateProcessor( + private val languageRoomDAO: LanguageRoomDAO, + private val bot: RequestsExecutor, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(WhatWordUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val update = update.asBaseMessageUpdate() ?: return + val roomId = update.data.chat.id + val message = update.data.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + + if ( + languageRoomDAO.get(roomId.chatId, roomId.threadId)?.takeIf { it.language == Language.ENGLISH && it.violence == Violence.MOTHERFUCKER } == null + ) { + return + } + + if (content.text.contains(Regex("w((ha)|(a)|(o)|(u))t\\?", RegexOption.IGNORE_CASE))) { + bot.sendAnimation( + chat = message.chat, + animation = MultipartFile( + filename = "say what again.gif", + inputSource = { + this::class.java.getResourceAsStream("/say what again.gif").asInput() + }, + ), + replyToMessageId = message.messageId, + ) + } + } +} diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/utils/LocalDateX.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/utils/LocalDateX.kt new file mode 100644 index 00000000..6cd79a96 --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/utils/LocalDateX.kt @@ -0,0 +1,12 @@ +package by.jprof.telegram.bot.english.utils + +import java.time.LocalDate + +infix fun LocalDate.downTo(other: LocalDate): Iterator = iterator { + var current = this@downTo + + while (current >= other) { + yield(current) + current = current.minusDays(1) + } +} diff --git a/english/src/main/kotlin/by/jprof/telegram/bot/english/utils/messages.kt b/english/src/main/kotlin/by/jprof/telegram/bot/english/utils/messages.kt new file mode 100644 index 00000000..d3f23d3e --- /dev/null +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/utils/messages.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.english.utils + +private val iVeExplainedSomeWordsForYouMessages = listOf( + "Yo, I've explained some words for you \uD83D\uDC47", + "Here are some explanations \uD83D\uDC47", + "You've emphasized some words, so I decided to explain them \uD83D\uDC47", +) + +internal fun iVeExplainedSomeWordsForYou(): String { + return iVeExplainedSomeWordsForYouMessages.random() +} diff --git a/english/src/main/resources/english0.png b/english/src/main/resources/english0.png new file mode 100644 index 00000000..3325ff65 Binary files /dev/null and b/english/src/main/resources/english0.png differ diff --git a/english/src/main/resources/english1.png b/english/src/main/resources/english1.png new file mode 100644 index 00000000..ff29193a Binary files /dev/null and b/english/src/main/resources/english1.png differ diff --git a/english/src/main/resources/english2.gif b/english/src/main/resources/english2.gif new file mode 100644 index 00000000..b432c8f6 Binary files /dev/null and b/english/src/main/resources/english2.gif differ diff --git a/english/src/main/resources/english3.gif b/english/src/main/resources/english3.gif new file mode 100644 index 00000000..20219f1b Binary files /dev/null and b/english/src/main/resources/english3.gif differ diff --git a/english/src/main/resources/no power.gif b/english/src/main/resources/no power.gif new file mode 100644 index 00000000..02e1a308 Binary files /dev/null and b/english/src/main/resources/no power.gif differ diff --git a/english/src/main/resources/say what again.gif b/english/src/main/resources/say what again.gif new file mode 100644 index 00000000..eab8a463 Binary files /dev/null and b/english/src/main/resources/say what again.gif differ diff --git a/english/urban-dictionary-daily/README.adoc b/english/urban-dictionary-daily/README.adoc new file mode 100644 index 00000000..e9fbc3d6 --- /dev/null +++ b/english/urban-dictionary-daily/README.adoc @@ -0,0 +1,3 @@ += English / Urban Dictionary Daily + +Handler for https://urbandictionary.com[Urban Dictionary's] "Word of the Day" mails. diff --git a/english/urban-dictionary-daily/build.gradle.kts b/english/urban-dictionary-daily/build.gradle.kts new file mode 100644 index 00000000..a5e15cfc --- /dev/null +++ b/english/urban-dictionary-daily/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + id("com.github.johnrengelman.shadow") +} + +dependencies { + implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.tgbotapi) + implementation(projects.english.urbanDictionary) + implementation(projects.english.urbanWordOfTheDay.dynamodb) + implementation(projects.english.languageRooms.dynamodb) + implementation(projects.english.urbanWordOfTheDayFormatter) + implementation(projects.pins.sfn) + + implementation(libs.bundles.aws.lambda) + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.jdk8) + implementation(libs.bundles.log4j) +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/Handler.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/Handler.kt new file mode 100644 index 00000000..08c5270c --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/Handler.kt @@ -0,0 +1,102 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily + +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.language_rooms.model.Language +import by.jprof.telegram.bot.english.urban_dictionary.UrbanDictionaryClient +import by.jprof.telegram.bot.english.urban_dictionary_daily.config.databaseModule +import by.jprof.telegram.bot.english.urban_dictionary_daily.config.envModule +import by.jprof.telegram.bot.english.urban_dictionary_daily.config.jsonModule +import by.jprof.telegram.bot.english.urban_dictionary_daily.config.sfnModule +import by.jprof.telegram.bot.english.urban_dictionary_daily.config.telegramModule +import by.jprof.telegram.bot.english.urban_dictionary_daily.config.urbanDictionaryModule +import by.jprof.telegram.bot.english.urban_dictionary_daily.model.Event +import by.jprof.telegram.bot.english.urban_dictionary_definition_formatter.format +import by.jprof.telegram.bot.english.urban_word_of_the_day.dao.UrbanWordOfTheDayDAO +import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay +import by.jprof.telegram.bot.pins.dto.Unpin +import by.jprof.telegram.bot.pins.scheduler.UnpinScheduler +import com.amazonaws.services.lambda.runtime.Context +import com.amazonaws.services.lambda.runtime.RequestStreamHandler +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.ChatIdWithThreadId +import dev.inmo.tgbotapi.types.message.MarkdownV2 +import java.io.InputStream +import java.io.OutputStream +import java.time.LocalDate +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import org.apache.logging.log4j.LogManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.context.startKoin + +@ExperimentalSerializationApi +@Suppress("unused") +class Handler : RequestStreamHandler, KoinComponent { + companion object { + private val logger = LogManager.getLogger(Handler::class.java) + } + + init { + startKoin { + modules( + envModule, + jsonModule, + urbanDictionaryModule, + databaseModule, + telegramModule, + sfnModule, + ) + } + } + + private val json: Json by inject() + private val urbanDictionaryClient: UrbanDictionaryClient by inject() + private val urbanWordOfTheDayDAO: UrbanWordOfTheDayDAO by inject() + private val languageRoomDAO: LanguageRoomDAO by inject() + private val bot: RequestsExecutor by inject() + private val unpinScheduler: UnpinScheduler by inject() + + override fun handleRequest(input: InputStream, output: OutputStream, context: Context) = runBlocking { + val event = json.decodeFromStream(input) + + logger.debug("Parsed event: {}", event) + + val term = event.records.firstOrNull()?.ses?.mail?.subject ?: return@runBlocking + val definition = urbanDictionaryClient.define(term).maxByOrNull { it.thumbsUp } ?: return@runBlocking + + logger.debug("Definition: {}", definition) + + val urbanWordOfTheDay = UrbanWordOfTheDay( + date = LocalDate.now(), + word = definition.word, + definition = definition.definition, + example = definition.example, + permalink = definition.permalink, + ) + + urbanWordOfTheDayDAO.save(urbanWordOfTheDay) + languageRoomDAO.getAll().filter { it.language == Language.ENGLISH && it.urbanWordOfTheDay }.forEach { languageRoom -> + val message = bot.sendMessage( + chatId = languageRoom.threadId?.let { ChatIdWithThreadId(languageRoom.chatId, it) } ?: ChatId(languageRoom.chatId), + text = urbanWordOfTheDay.format().also { logger.debug("Formatted message: {}", it) }, + parseMode = MarkdownV2, + disableWebPagePreview = true, + ) + + bot.pinChatMessage(message, disableNotification = true) + unpinScheduler.scheduleUnpin( + Unpin().apply { + chatId = languageRoom.chatId + messageId = message.messageId + ttl = (24 * 1.5 * 60 * 60).toLong() + } + ) + } + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/database.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/database.kt new file mode 100644 index 00000000..820fd956 --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/database.kt @@ -0,0 +1,29 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.config + +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.urban_word_of_the_day.dao.UrbanWordOfTheDayDAO +import org.koin.core.qualifier.named +import org.koin.dsl.module +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import by.jprof.telegram.bot.english.language_rooms.dynamodb.dao.LanguageRoomDAO as DynamoDBLanguageRoomDAO +import by.jprof.telegram.bot.english.urban_word_of_the_day.dynamodb.dao.UrbanWordOfTheDayDAO as DynamoDBUrbanWordOfTheDayDAO + +val databaseModule = module { + single { + DynamoDbAsyncClient.create() + } + + single { + DynamoDBUrbanWordOfTheDayDAO( + get(), + get(named(TABLE_URBAN_WORDS_OF_THE_DAY)) + ) + } + + single { + DynamoDBLanguageRoomDAO( + get(), + get(named(TABLE_LANGUAGE_ROOMS)) + ) + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/env.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/env.kt new file mode 100644 index 00000000..d9e8f28c --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/env.kt @@ -0,0 +1,22 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.config + +import org.koin.core.qualifier.named +import org.koin.dsl.module + +const val TOKEN_TELEGRAM_BOT = "TOKEN_TELEGRAM_BOT" +const val TABLE_URBAN_WORDS_OF_THE_DAY = "TABLE_URBAN_WORDS_OF_THE_DAY" +const val TABLE_LANGUAGE_ROOMS = "TABLE_LANGUAGE_ROOMS" +const val STATE_MACHINE_UNPINS = "STATE_MACHINE_UNPINS" + +val envModule = module { + listOf( + TOKEN_TELEGRAM_BOT, + TABLE_URBAN_WORDS_OF_THE_DAY, + TABLE_LANGUAGE_ROOMS, + STATE_MACHINE_UNPINS, + ).forEach { variable -> + single(named(variable)) { + System.getenv(variable)!! + } + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/json.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/json.kt new file mode 100644 index 00000000..f7b1c230 --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/json.kt @@ -0,0 +1,12 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.config + +import kotlinx.serialization.json.Json +import org.koin.dsl.module + +val jsonModule = module { + single { + Json { + ignoreUnknownKeys = true + } + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/sfn.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/sfn.kt new file mode 100644 index 00000000..68f9d036 --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/sfn.kt @@ -0,0 +1,20 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.config + +import by.jprof.telegram.bot.pins.scheduler.UnpinScheduler +import org.koin.core.qualifier.named +import org.koin.dsl.module +import software.amazon.awssdk.services.sfn.SfnAsyncClient +import by.jprof.telegram.bot.pins.sfn.scheduler.UnpinScheduler as SfnUnpinScheduler + +val sfnModule = module { + single { + SfnAsyncClient.create() + } + + single { + SfnUnpinScheduler( + sfn = get(), + stateMachine = get(named(STATE_MACHINE_UNPINS)) + ) + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/telegram.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/telegram.kt new file mode 100644 index 00000000..82668680 --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/telegram.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.config + +import dev.inmo.tgbotapi.extensions.api.telegramBot +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val telegramModule = module { + single { + telegramBot(get(named(TOKEN_TELEGRAM_BOT))) + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/urbanDictionary.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/urbanDictionary.kt new file mode 100644 index 00000000..b3cff2f5 --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/config/urbanDictionary.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.config + +import by.jprof.telegram.bot.english.urban_dictionary.KtorUrbanDictionaryClient +import by.jprof.telegram.bot.english.urban_dictionary.UrbanDictionaryClient +import org.koin.dsl.module + +val urbanDictionaryModule = module { + single { + KtorUrbanDictionaryClient() + } +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Event.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Event.kt new file mode 100644 index 00000000..10d3125d --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Event.kt @@ -0,0 +1,10 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Event( + @SerialName("Records") + val records: List +) diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Header.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Header.kt new file mode 100644 index 00000000..6383df8d --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Header.kt @@ -0,0 +1,9 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Header( + val name: String, + val value: String, +) diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Mail.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Mail.kt new file mode 100644 index 00000000..cfa0864f --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Mail.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Mail( + val headers: List
+) { + val subject: String? + get() = headers.firstOrNull { it.name == "Subject" }?.value +} diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Record.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Record.kt new file mode 100644 index 00000000..0ba060df --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/Record.kt @@ -0,0 +1,8 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Record( + val ses: SES +) diff --git a/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/SES.kt b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/SES.kt new file mode 100644 index 00000000..dd7a887a --- /dev/null +++ b/english/urban-dictionary-daily/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_daily/model/SES.kt @@ -0,0 +1,8 @@ +package by.jprof.telegram.bot.english.urban_dictionary_daily.model + +import kotlinx.serialization.Serializable + +@Serializable +data class SES( + val mail: Mail +) diff --git a/english/urban-dictionary-daily/src/main/resources/log4j2.xml b/english/urban-dictionary-daily/src/main/resources/log4j2.xml new file mode 100644 index 00000000..1c4c0559 --- /dev/null +++ b/english/urban-dictionary-daily/src/main/resources/log4j2.xml @@ -0,0 +1,20 @@ + + + + + + false + true + + + + + + + + + + + + + diff --git a/english/urban-dictionary/README.adoc b/english/urban-dictionary/README.adoc new file mode 100644 index 00000000..b89da3ff --- /dev/null +++ b/english/urban-dictionary/README.adoc @@ -0,0 +1,3 @@ += English / Urban Dictionary + +https://urbandictionary.com[Urban Dictionary] client. diff --git a/english/urban-dictionary/build.gradle.kts b/english/urban-dictionary/build.gradle.kts new file mode 100644 index 00000000..7fba757b --- /dev/null +++ b/english/urban-dictionary/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") +} + +dependencies { + implementation(platform(libs.ktor.bom)) + + implementation(libs.ktor.client.apache) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.log4j.api) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) + testImplementation(libs.mockk) + testRuntimeOnly(libs.junit.jupiter.engine) + testRuntimeOnly(libs.log4j.core) +} + +tasks { + val integrationTest by registering(Test::class) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Runs the integration tests." + shouldRunAfter("test") + outputs.upToDateWhen { false } + useJUnitPlatform { + includeTags("it") + } + } +} diff --git a/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/Definition.kt b/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/Definition.kt new file mode 100644 index 00000000..8b4543c9 --- /dev/null +++ b/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/Definition.kt @@ -0,0 +1,17 @@ +package by.jprof.telegram.bot.english.urban_dictionary + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Definition( + val definition: String, + val permalink: String, + val author: String, + val word: String, + val example: String, + @SerialName("thumbs_up") + val thumbsUp: Int, + @SerialName("thumbs_down") + val thumbsDown: Int, +) diff --git a/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/KtorUrbanDictionaryClient.kt b/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/KtorUrbanDictionaryClient.kt new file mode 100644 index 00000000..104f808d --- /dev/null +++ b/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/KtorUrbanDictionaryClient.kt @@ -0,0 +1,47 @@ +package by.jprof.telegram.bot.english.urban_dictionary + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.apache.Apache +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.url +import io.ktor.serialization.kotlinx.json.json +import java.io.Closeable +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +class KtorUrbanDictionaryClient( + private val baseUrl: String = "https://api.urbandictionary.com/v0" +) : UrbanDictionaryClient, Closeable { + private val client = HttpClient(Apache) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + } + ) + } + } + + override suspend fun define(term: String): Collection = + client.get { + url("$baseUrl/define") + parameter("term", term) + }.body().list + + override suspend fun random(): Collection = + client.get { + url("$baseUrl/random") + }.body().list + + override fun close() { + client.close() + } + + @Serializable + private data class UrbanDictionaryResponse( + val list: List + ) +} diff --git a/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/UrbanDictionaryClient.kt b/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/UrbanDictionaryClient.kt new file mode 100644 index 00000000..9a34c68e --- /dev/null +++ b/english/urban-dictionary/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary/UrbanDictionaryClient.kt @@ -0,0 +1,7 @@ +package by.jprof.telegram.bot.english.urban_dictionary + +interface UrbanDictionaryClient { + suspend fun define(term: String): Collection + + suspend fun random(): Collection +} diff --git a/english/urban-dictionary/src/test/kotlin/by/jprof/telegram/bot/english/urban_dictionary/KtorUrbanDictionaryClientIntegrationTest.kt b/english/urban-dictionary/src/test/kotlin/by/jprof/telegram/bot/english/urban_dictionary/KtorUrbanDictionaryClientIntegrationTest.kt new file mode 100644 index 00000000..456a4899 --- /dev/null +++ b/english/urban-dictionary/src/test/kotlin/by/jprof/telegram/bot/english/urban_dictionary/KtorUrbanDictionaryClientIntegrationTest.kt @@ -0,0 +1,33 @@ +package by.jprof.telegram.bot.english.urban_dictionary + +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@Tag("it") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class KtorUrbanDictionaryClientIntegrationTest { + private lateinit var sut: KtorUrbanDictionaryClient + + @BeforeAll + internal fun setup() { + sut = KtorUrbanDictionaryClient() + } + + @Test + fun define() = runBlocking { + val definitions = sut.define("motherfucker") + + assertTrue(definitions.isNotEmpty()) + } + + @Test + fun random() = runBlocking { + val definitions = sut.random() + + assertTrue(definitions.isNotEmpty()) + } +} diff --git a/english/urban-word-of-the-day-formatter/README.adoc b/english/urban-word-of-the-day-formatter/README.adoc new file mode 100644 index 00000000..9b97cdb5 --- /dev/null +++ b/english/urban-word-of-the-day-formatter/README.adoc @@ -0,0 +1,3 @@ += English / Urban Dictionary WotD Formatter + +Markdown formatter for Urban Dictionary's WotD. diff --git a/english/urban-word-of-the-day-formatter/build.gradle.kts b/english/urban-word-of-the-day-formatter/build.gradle.kts new file mode 100644 index 00000000..a0237e49 --- /dev/null +++ b/english/urban-word-of-the-day-formatter/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(projects.english.urbanWordOfTheDay) + implementation(libs.tgbotapi) +} diff --git a/english/urban-word-of-the-day-formatter/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_definition_formatter/UrbanWordOfTheDayX.kt b/english/urban-word-of-the-day-formatter/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_definition_formatter/UrbanWordOfTheDayX.kt new file mode 100644 index 00000000..ef891331 --- /dev/null +++ b/english/urban-word-of-the-day-formatter/src/main/kotlin/by/jprof/telegram/bot/english/urban_dictionary_definition_formatter/UrbanWordOfTheDayX.kt @@ -0,0 +1,14 @@ +package by.jprof.telegram.bot.english.urban_dictionary_definition_formatter + +import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay +import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common + +fun UrbanWordOfTheDay.format(): String = buildString { + appendLine("*[${word.escapeMarkdownV2Common()}]($permalink)*") + appendLine() + appendLine(definition.escapeMarkdownV2Common()) + example?.let { + appendLine() + appendLine("_${it.escapeMarkdownV2Common()}_") + } +} diff --git a/english/urban-word-of-the-day/README.adoc b/english/urban-word-of-the-day/README.adoc new file mode 100644 index 00000000..71c3b9b5 --- /dev/null +++ b/english/urban-word-of-the-day/README.adoc @@ -0,0 +1,3 @@ += English / Urban Word of the Day + +Urban Word of the Day model. diff --git a/english/urban-word-of-the-day/build.gradle.kts b/english/urban-word-of-the-day/build.gradle.kts new file mode 100644 index 00000000..444baaa3 --- /dev/null +++ b/english/urban-word-of-the-day/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + kotlin("jvm") +} diff --git a/english/urban-word-of-the-day/dynamodb/README.adoc b/english/urban-word-of-the-day/dynamodb/README.adoc new file mode 100644 index 00000000..2d39cd94 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/README.adoc @@ -0,0 +1,3 @@ += English / Urban Word of the Day / DynamoDB + +DynamoDB DAO for the Urban Word of the Day. diff --git a/english/urban-word-of-the-day/dynamodb/build.gradle.kts b/english/urban-word-of-the-day/dynamodb/build.gradle.kts new file mode 100644 index 00000000..3caed3d2 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.english.urbanWordOfTheDay) + api(libs.dynamodb) + implementation(project.projects.utils.dynamodb) + 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/english/urban-word-of-the-day/dynamodb/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayDAO.kt b/english/urban-word-of-the-day/dynamodb/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayDAO.kt new file mode 100644 index 00000000..dca77d56 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayDAO.kt @@ -0,0 +1,57 @@ +package by.jprof.telegram.bot.english.urban_word_of_the_day.dynamodb.dao + +import by.jprof.telegram.bot.english.urban_word_of_the_day.dao.UrbanWordOfTheDayDAO +import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay +import by.jprof.telegram.bot.utils.dynamodb.toAttributeValue +import by.jprof.telegram.bot.utils.dynamodb.toString +import java.time.LocalDate +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +class UrbanWordOfTheDayDAO( + private val dynamoDb: DynamoDbAsyncClient, + private val table: String +) : UrbanWordOfTheDayDAO { + override suspend fun save(urbanWordOfTheDay: UrbanWordOfTheDay) { + withContext(Dispatchers.IO) { + dynamoDb.putItem { + it.tableName(table) + it.item(urbanWordOfTheDay.toAttributes()) + }.await() + } + } + + override suspend fun get(date: LocalDate): UrbanWordOfTheDay? { + return withContext(Dispatchers.IO) { + dynamoDb.getItem { + it.tableName(table) + it.key( + mapOf( + "date" to date.toString().toAttributeValue() + ) + ) + }.await()?.item()?.takeUnless { it.isEmpty() }?.toUrbanWordOfTheDay() + } + } +} + +fun UrbanWordOfTheDay.toAttributes(): Map = buildMap { + put("date", this@toAttributes.date.toString().toAttributeValue()) + put("word", this@toAttributes.word.toAttributeValue()) + put("definition", this@toAttributes.definition.toAttributeValue()) + this@toAttributes.example?.let { + put("example", it.toAttributeValue()) + } + put("permalink", this@toAttributes.permalink.toAttributeValue()) +} + +fun Map.toUrbanWordOfTheDay(): UrbanWordOfTheDay = UrbanWordOfTheDay( + date = LocalDate.parse(this["date"].toString("date")), + word = this["word"].toString("word"), + definition = this["definition"].toString("definition"), + example = this["example"]?.s(), + permalink = this["permalink"].toString("permalink"), +) diff --git a/english/urban-word-of-the-day/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayDAOTest.kt b/english/urban-word-of-the-day/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayDAOTest.kt new file mode 100644 index 00000000..5924c8e8 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayDAOTest.kt @@ -0,0 +1,54 @@ +package by.jprof.telegram.bot.english.urban_word_of_the_day.dynamodb.dao + +import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay +import by.jprof.telegram.bot.utils.aws_junit5.Endpoint +import java.time.LocalDate +import kotlinx.coroutines.runBlocking +import me.madhead.aws_junit5.common.AWSClient +import me.madhead.aws_junit5.dynamo.v2.DynamoDB +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +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 UrbanWordOfTheDayDAOTest { + @AWSClient(endpoint = Endpoint::class) + private lateinit var dynamoDB: DynamoDbAsyncClient + private lateinit var sut: UrbanWordOfTheDayDAO + + @BeforeAll + internal fun setup() { + sut = UrbanWordOfTheDayDAO(dynamoDB, "urbanWordsOfTheDay") + } + + @Test + fun save() = runBlocking { + sut.save(urbanWordOfTheDay.copy(date = LocalDate.MAX)) + } + + @Test + fun get() = runBlocking { + assertEquals(urbanWordOfTheDay, sut.get(now)) + } + + @Test + fun getUnexisting() = runBlocking { + assertNull(sut.get(LocalDate.MIN)) + } + + private val now = LocalDate.parse("2022-12-05") + private val urbanWordOfTheDay + get() = UrbanWordOfTheDay( + date = now, + word = "word", + definition = "definition", + example = "example", + permalink = "https://word.urbanup.com", + ) +} diff --git a/english/urban-word-of-the-day/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayTest.kt b/english/urban-word-of-the-day/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayTest.kt new file mode 100644 index 00000000..38daffc5 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/src/test/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dynamodb/dao/UrbanWordOfTheDayTest.kt @@ -0,0 +1,43 @@ +package by.jprof.telegram.bot.english.urban_word_of_the_day.dynamodb.dao + +import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay +import java.time.LocalDate +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import software.amazon.awssdk.services.dynamodb.model.AttributeValue + +internal class UrbanWordOfTheDayTest { + @Test + fun toAttributes() { + assertEquals( + attributes, + urbanWordOfTheDay.toAttributes() + ) + } + + @Test + fun toUrbanWordOfTheDay() { + assertEquals( + urbanWordOfTheDay, + attributes.toUrbanWordOfTheDay() + ) + } + + private val now = LocalDate.now() + private val urbanWordOfTheDay + get() = UrbanWordOfTheDay( + date = now, + word = "word", + definition = "definition", + example = "example", + permalink = "https://word.urbanup.com", + ) + private val attributes + get() = mapOf( + "date" to AttributeValue.builder().s(now.toString()).build(), + "word" to AttributeValue.builder().s("word").build(), + "definition" to AttributeValue.builder().s("definition").build(), + "example" to AttributeValue.builder().s("example").build(), + "permalink" to AttributeValue.builder().s("https://word.urbanup.com").build(), + ) +} diff --git a/english/urban-word-of-the-day/dynamodb/src/test/resources/seed.sh b/english/urban-word-of-the-day/dynamodb/src/test/resources/seed.sh new file mode 100755 index 00000000..8a90501d --- /dev/null +++ b/english/urban-word-of-the-day/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 urbanWordsOfTheDay || true +aws --endpoint-url "${DYNAMODB_URL}" dynamodb create-table --cli-input-json file://english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.table.json +aws --endpoint-url "${DYNAMODB_URL}" dynamodb batch-write-item --request-items file://english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.items.json diff --git a/english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.items.json b/english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.items.json new file mode 100644 index 00000000..b945aab8 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.items.json @@ -0,0 +1,25 @@ +{ + "urbanWordsOfTheDay": [ + { + "PutRequest": { + "Item": { + "date": { + "S": "2022-12-05" + }, + "word": { + "S": "word" + }, + "definition": { + "S": "definition" + }, + "example": { + "S": "example" + }, + "permalink": { + "S": "https://word.urbanup.com" + } + } + } + } + ] +} diff --git a/english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.table.json b/english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.table.json new file mode 100644 index 00000000..69d04676 --- /dev/null +++ b/english/urban-word-of-the-day/dynamodb/src/test/resources/urbanWordsOfTheDay.table.json @@ -0,0 +1,19 @@ +{ + "TableName": "urbanWordsOfTheDay", + "AttributeDefinitions": [ + { + "AttributeName": "date", + "AttributeType": "S" + } + ], + "KeySchema": [ + { + "AttributeName": "date", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 1, + "WriteCapacityUnits": 1 + } +} diff --git a/english/urban-word-of-the-day/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dao/UrbanWordOfTheDayDAO.kt b/english/urban-word-of-the-day/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dao/UrbanWordOfTheDayDAO.kt new file mode 100644 index 00000000..39276829 --- /dev/null +++ b/english/urban-word-of-the-day/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/dao/UrbanWordOfTheDayDAO.kt @@ -0,0 +1,10 @@ +package by.jprof.telegram.bot.english.urban_word_of_the_day.dao + +import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay +import java.time.LocalDate + +interface UrbanWordOfTheDayDAO { + suspend fun save(urbanWordOfTheDay: UrbanWordOfTheDay) + + suspend fun get(date: LocalDate): UrbanWordOfTheDay? +} diff --git a/english/urban-word-of-the-day/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/model/UrbanWordOfTheDay.kt b/english/urban-word-of-the-day/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/model/UrbanWordOfTheDay.kt new file mode 100644 index 00000000..8c25026a --- /dev/null +++ b/english/urban-word-of-the-day/src/main/kotlin/by/jprof/telegram/bot/english/urban_word_of_the_day/model/UrbanWordOfTheDay.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.english.urban_word_of_the_day.model + +import java.time.LocalDate + +data class UrbanWordOfTheDay( + val date: LocalDate, + val word: String, + val definition: String, + val example: String?, + val permalink: String, +) diff --git a/launchers/lambda/build.gradle.kts b/launchers/lambda/build.gradle.kts index 59297da2..a1bbab28 100644 --- a/launchers/lambda/build.gradle.kts +++ b/launchers/lambda/build.gradle.kts @@ -23,4 +23,9 @@ dependencies { implementation(project.projects.leetcode) implementation(project.projects.times.timezones.dynamodb) implementation(project.projects.times) + implementation(project.projects.english) + implementation(project.projects.english.languageRooms.dynamodb) + implementation(project.projects.english.urbanWordOfTheDay.dynamodb) + implementation(project.projects.english.urbanDictionary) + implementation(project.projects.english.dictionaryapiDev) } diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/JProf.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/JProf.kt index c8be590a..336e606c 100644 --- a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/JProf.kt +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/JProf.kt @@ -3,11 +3,13 @@ package by.jprof.telegram.bot.launchers.lambda import by.jprof.telegram.bot.core.UpdateProcessingPipeline import by.jprof.telegram.bot.launchers.lambda.config.currenciesModule import by.jprof.telegram.bot.launchers.lambda.config.databaseModule +import by.jprof.telegram.bot.launchers.lambda.config.dictionaryApiDevModule import by.jprof.telegram.bot.launchers.lambda.config.envModule import by.jprof.telegram.bot.launchers.lambda.config.jsonModule import by.jprof.telegram.bot.launchers.lambda.config.pipelineModule import by.jprof.telegram.bot.launchers.lambda.config.sfnModule import by.jprof.telegram.bot.launchers.lambda.config.telegramModule +import by.jprof.telegram.bot.launchers.lambda.config.urbanDictionaryModule import by.jprof.telegram.bot.launchers.lambda.config.youtubeModule import com.amazonaws.services.lambda.runtime.Context import com.amazonaws.services.lambda.runtime.RequestHandler @@ -19,7 +21,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import org.apache.logging.log4j.LogManager import org.koin.core.component.KoinComponent -import org.koin.core.component.inject +import org.koin.core.component.get import org.koin.core.context.startKoin @PreviewFeature @@ -51,12 +53,14 @@ class JProf : RequestHandler, K pipelineModule, sfnModule, currenciesModule, + urbanDictionaryModule, + dictionaryApiDevModule, ) } } - private val json: Json by inject() - private val pipeline: UpdateProcessingPipeline by inject() + private val json: Json = get() + private val pipeline: UpdateProcessingPipeline = get() override fun handleRequest(input: APIGatewayV2HTTPEvent, context: Context): APIGatewayV2HTTPResponse { logger.debug("Incoming request: {}", input) diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/database.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/database.kt index d1c3e833..c43e11d8 100644 --- a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/database.kt +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/database.kt @@ -1,6 +1,8 @@ package by.jprof.telegram.bot.launchers.lambda.config import by.jprof.telegram.bot.dialogs.dao.DialogStateDAO +import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO +import by.jprof.telegram.bot.english.urban_word_of_the_day.dao.UrbanWordOfTheDayDAO import by.jprof.telegram.bot.kotlin.dao.KotlinMentionsDAO import by.jprof.telegram.bot.monies.dao.MoniesDAO import by.jprof.telegram.bot.pins.dao.PinDAO @@ -13,6 +15,8 @@ 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.english.language_rooms.dynamodb.dao.LanguageRoomDAO as DynamoDBLanguageRoomDAO +import by.jprof.telegram.bot.english.urban_word_of_the_day.dynamodb.dao.UrbanWordOfTheDayDAO as DynamoDBUrbanWordOfTheDayDAO import by.jprof.telegram.bot.kotlin.dynamodb.dao.KotlinMentionsDAO as DynamoDBKotlinMentionsDAO import by.jprof.telegram.bot.monies.dynamodb.dao.MoniesDAO as DynamoDBMoniesDAO import by.jprof.telegram.bot.pins.dynamodb.dao.PinDAO as DynamoDBPinDAO @@ -82,4 +86,18 @@ val databaseModule = module { get(named(TABLE_TIMEZONES)) ) } + + single { + DynamoDBLanguageRoomDAO( + get(), + get(named(TABLE_LANGUAGE_ROOMS)) + ) + } + + single { + DynamoDBUrbanWordOfTheDayDAO( + get(), + get(named(TABLE_URBAN_WORDS_OF_THE_DAY)) + ) + } } diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/dictionaryApiDev.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/dictionaryApiDev.kt new file mode 100644 index 00000000..7272c233 --- /dev/null +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/dictionaryApiDev.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.launchers.lambda.config + +import by.jprof.telegram.bot.english.dictionaryapi_dev.DictionaryAPIDotDevClient +import by.jprof.telegram.bot.english.dictionaryapi_dev.KtorDictionaryAPIDotDevClient +import org.koin.dsl.module + +val dictionaryApiDevModule = module { + single { + KtorDictionaryAPIDotDevClient() + } +} diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/env.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/env.kt index 180ab098..1af82d2f 100644 --- a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/env.kt +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/env.kt @@ -13,7 +13,10 @@ const val TABLE_QUIZOJIS = "TABLE_QUIZOJIS" const val TABLE_MONIES = "TABLE_MONIES" const val TABLE_PINS = "TABLE_PINS" const val TABLE_TIMEZONES = "TABLE_TIMEZONES" +const val TABLE_LANGUAGE_ROOMS = "TABLE_LANGUAGE_ROOMS" +const val TABLE_URBAN_WORDS_OF_THE_DAY = "TABLE_URBAN_WORDS_OF_THE_DAY" const val STATE_MACHINE_UNPINS = "STATE_MACHINE_UNPINS" +const val TIMEOUT = "TIMEOUT" val envModule = module { listOf( @@ -27,10 +30,16 @@ val envModule = module { TABLE_MONIES, TABLE_PINS, TABLE_TIMEZONES, + TABLE_LANGUAGE_ROOMS, + TABLE_URBAN_WORDS_OF_THE_DAY, STATE_MACHINE_UNPINS, ).forEach { variable -> single(named(variable)) { System.getenv(variable)!! } } + + single(named(TIMEOUT)) { + System.getenv(TIMEOUT)!!.toLong() + } } diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/pipeline.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/pipeline.kt index e9942dd9..d9180f1a 100644 --- a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/pipeline.kt +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/pipeline.kt @@ -3,6 +3,11 @@ package by.jprof.telegram.bot.launchers.lambda.config import by.jprof.telegram.bot.core.UpdateProcessingPipeline import by.jprof.telegram.bot.core.UpdateProcessor import by.jprof.telegram.bot.currencies.CurrenciesUpdateProcessor +import by.jprof.telegram.bot.english.EnglishCommandUpdateProcessor +import by.jprof.telegram.bot.english.ExplainerUpdateProcessor +import by.jprof.telegram.bot.english.MotherfuckingUpdateProcessor +import by.jprof.telegram.bot.english.UrbanWordOfTheDayUpdateProcessor +import by.jprof.telegram.bot.english.WhatWordUpdateProcessor import by.jprof.telegram.bot.eval.EvalUpdateProcessor import by.jprof.telegram.bot.jep.JEPUpdateProcessor import by.jprof.telegram.bot.jep.JsoupJEPSummary @@ -26,7 +31,10 @@ import org.koin.dsl.module @PreviewFeature val pipelineModule = module { single { - UpdateProcessingPipeline(getAll()) + UpdateProcessingPipeline( + processors = getAll(), + timeout = get(named(TIMEOUT)) - 1000 + ) } single(named("JEPUpdateProcessor")) { @@ -145,4 +153,42 @@ val pipelineModule = module { bot = get(), ) } + + single(named("EnglishCommandUpdateProcessor")) { + EnglishCommandUpdateProcessor( + languageRoomDAO = get(), + bot = get(), + ) + } + + single(named("UrbanWordOfTheDayUpdateProcessor")) { + UrbanWordOfTheDayUpdateProcessor( + languageRoomDAO = get(), + urbanWordOfTheDayDAO = get(), + bot = get(), + ) + } + + single(named("ExplainerUpdateProcessor")) { + ExplainerUpdateProcessor( + languageRoomDAO = get(), + urbanDictionaryClient = get(), + dictionaryapiDevClient = get(), + bot = get(), + ) + } + + single(named("WhatWordUpdateProcessor")) { + WhatWordUpdateProcessor( + languageRoomDAO = get(), + bot = get(), + ) + } + + single(named("MotherfuckingUpdateProcessor")) { + MotherfuckingUpdateProcessor( + languageRoomDAO = get(), + bot = get(), + ) + } } diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/urbanDictionary.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/urbanDictionary.kt new file mode 100644 index 00000000..51b67a3b --- /dev/null +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/urbanDictionary.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.launchers.lambda.config + +import by.jprof.telegram.bot.english.urban_dictionary.KtorUrbanDictionaryClient +import by.jprof.telegram.bot.english.urban_dictionary.UrbanDictionaryClient +import org.koin.dsl.module + +val urbanDictionaryModule = module { + single { + KtorUrbanDictionaryClient() + } +} diff --git a/pins/build.gradle.kts b/pins/build.gradle.kts index 321a237d..f1d8c651 100644 --- a/pins/build.gradle.kts +++ b/pins/build.gradle.kts @@ -7,6 +7,7 @@ dependencies { api(libs.tgbotapi) api(project.projects.monies) implementation(project.projects.pins.dto) + implementation(project.projects.pins.scheduler) implementation(libs.log4j.api) testImplementation(libs.junit.jupiter.api) diff --git a/pins/dto/src/main/java/by/jprof/telegram/bot/pins/dto/Unpin.java b/pins/dto/src/main/java/by/jprof/telegram/bot/pins/dto/Unpin.java index 4ececec0..6775ca29 100644 --- a/pins/dto/src/main/java/by/jprof/telegram/bot/pins/dto/Unpin.java +++ b/pins/dto/src/main/java/by/jprof/telegram/bot/pins/dto/Unpin.java @@ -3,63 +3,53 @@ import java.util.Objects; public class Unpin { - private Long messageId; - private Long chatId; - private Long userId; - private Long ttl; - - public Long getMessageId() { - return messageId; - } - - public void setMessageId(Long messageId) { - this.messageId = messageId; - } - - public Long getChatId() { - return chatId; - } - - public void setChatId(Long chatId) { - this.chatId = chatId; - } - - public Long getUserId() { - return userId; - } - - public void setUserId(Long userId) { - this.userId = userId; - } - - public Long getTtl() { - return ttl; - } - - public void setTtl(Long ttl) { - this.ttl = ttl; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Unpin unpin = (Unpin) o; - return Objects.equals(messageId, unpin.messageId) && Objects.equals(chatId, unpin.chatId) && Objects.equals(userId, unpin.userId) && Objects.equals(ttl, unpin.ttl); - } - - @Override - public int hashCode() { - return Objects.hash(messageId, chatId, userId, ttl); - } - - @Override - public String toString() { - return "Unpin{" + - "messageId=" + messageId + - ", chatId=" + chatId + - ", userId=" + userId + - ", ttl=" + ttl + - '}'; - } + private Long messageId; + private Long chatId; + private Long ttl; + + public Long getMessageId() { + return messageId; + } + + public void setMessageId(Long messageId) { + this.messageId = messageId; + } + + public Long getChatId() { + return chatId; + } + + public void setChatId(Long chatId) { + this.chatId = chatId; + } + + public Long getTtl() { + return ttl; + } + + public void setTtl(Long ttl) { + this.ttl = ttl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Unpin unpin = (Unpin) o; + return Objects.equals(messageId, unpin.messageId) && Objects.equals(chatId, unpin.chatId) && Objects.equals(ttl, unpin.ttl); + } + + @Override + public int hashCode() { + return Objects.hash(messageId, chatId, ttl); + } + + @Override + public String toString() { + return "Unpin{" + + "messageId=" + messageId + + ", chatId=" + chatId + + ", ttl=" + ttl + + '}'; + } } diff --git a/pins/scheduler/README.adoc b/pins/scheduler/README.adoc new file mode 100644 index 00000000..6b3e0189 --- /dev/null +++ b/pins/scheduler/README.adoc @@ -0,0 +1,3 @@ += Pins / Unpin Scheduler + +Message unpin scheduler interface. diff --git a/pins/scheduler/build.gradle.kts b/pins/scheduler/build.gradle.kts new file mode 100644 index 00000000..19f8d9b8 --- /dev/null +++ b/pins/scheduler/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.pins.dto) +} diff --git a/pins/src/main/kotlin/by/jprof/telegram/bot/pins/scheduler/UnpinScheduler.kt b/pins/scheduler/src/main/kotlin/by/jprof/telegram/bot/pins/scheduler/UnpinScheduler.kt similarity index 100% rename from pins/src/main/kotlin/by/jprof/telegram/bot/pins/scheduler/UnpinScheduler.kt rename to pins/scheduler/src/main/kotlin/by/jprof/telegram/bot/pins/scheduler/UnpinScheduler.kt diff --git a/pins/sfn/build.gradle.kts b/pins/sfn/build.gradle.kts index 0f20e05a..79f857ed 100644 --- a/pins/sfn/build.gradle.kts +++ b/pins/sfn/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(project.projects.pins) api(project.projects.pins.dto) + api(project.projects.pins.scheduler) api(libs.sfn) implementation(libs.kotlinx.coroutines.jdk8) diff --git a/pins/sfn/src/main/kotlin/by/jprof/telegram/bot/pins/sfn/scheduler/UnpinScheduler.kt b/pins/sfn/src/main/kotlin/by/jprof/telegram/bot/pins/sfn/scheduler/UnpinScheduler.kt index 934bc54f..9f2518d0 100644 --- a/pins/sfn/src/main/kotlin/by/jprof/telegram/bot/pins/sfn/scheduler/UnpinScheduler.kt +++ b/pins/sfn/src/main/kotlin/by/jprof/telegram/bot/pins/sfn/scheduler/UnpinScheduler.kt @@ -16,7 +16,6 @@ class UnpinScheduler( it.input("""{ | "messageId": ${unpin.messageId}, | "chatId": ${unpin.chatId}, - | "userId": ${unpin.userId}, | "ttl": ${unpin.ttl} |}""".trimMargin()) }.await() diff --git a/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinCommandUpdateProcessor.kt b/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinCommandUpdateProcessor.kt index 17168f4d..24c17603 100644 --- a/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinCommandUpdateProcessor.kt +++ b/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinCommandUpdateProcessor.kt @@ -88,7 +88,6 @@ class PinCommandUpdateProcessor( unpinScheduler.scheduleUnpin(Unpin().apply { messageId = pin.message.messageId chatId = pin.chat.id.chatId - userId = pin.user.id.chatId ttl = duration.duration.seconds }) } catch (e: Exception) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 989050ef..f11ae81b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include(":quizoji:dynamodb") include(":eval") include(":pins") include(":pins:dto") +include(":pins:scheduler") include(":pins:unpin") include(":pins:dynamodb") include(":pins:sfn") @@ -33,4 +34,13 @@ include(":leetcode") include(":times:timezones") include(":times:timezones:dynamodb") include(":times") +include(":english") +include(":english:language-rooms") +include(":english:language-rooms:dynamodb") +include(":english:urban-dictionary") +include(":english:dictionaryapi-dev") +include(":english:urban-word-of-the-day") +include(":english:urban-word-of-the-day:dynamodb") +include(":english:urban-word-of-the-day-formatter") +include(":english:urban-dictionary-daily") include(":launchers:lambda") diff --git a/utils/dynamodb/src/main/kotlin/by/jprof/telegram/bot/utils/dynamodb/extensions.kt b/utils/dynamodb/src/main/kotlin/by/jprof/telegram/bot/utils/dynamodb/extensions.kt index ade3ab97..751e4bbb 100644 --- a/utils/dynamodb/src/main/kotlin/by/jprof/telegram/bot/utils/dynamodb/extensions.kt +++ b/utils/dynamodb/src/main/kotlin/by/jprof/telegram/bot/utils/dynamodb/extensions.kt @@ -1,7 +1,7 @@ package by.jprof.telegram.bot.utils.dynamodb -import software.amazon.awssdk.services.dynamodb.model.AttributeValue import java.time.Instant +import software.amazon.awssdk.services.dynamodb.model.AttributeValue fun String.toAttributeValue(): AttributeValue = AttributeValue.builder().s(this).build() @@ -9,6 +9,8 @@ fun Int.toAttributeValue(): AttributeValue = AttributeValue.builder().n(this.toS fun Long.toAttributeValue(): AttributeValue = AttributeValue.builder().n(this.toString()).build() +fun Boolean.toAttributeValue(): AttributeValue = AttributeValue.builder().bool(this).build() + fun Instant.toAttributeValue(): AttributeValue = this.toEpochMilli().toAttributeValue() fun List.toAttributeValue(): AttributeValue = AttributeValue.builder().l(this).build() @@ -20,5 +22,7 @@ fun AttributeValue?.toString(name: String): String = this?.s() ?: throw IllegalS fun AttributeValue?.toLong(name: String): Long = this?.n()?.toLong() ?: throw IllegalStateException("Missing $name property") +fun AttributeValue?.toBoolean(name: String): Boolean = this?.bool() ?: throw IllegalStateException("Missing $name property") + fun AttributeValue?.toInstant(name: String): Instant = Instant.ofEpochMilli(this?.n()?.toLong() ?: throw IllegalStateException("Missing $name property"))