diff --git a/.deploy/lambda/lib/JProfByBotStack.ts b/.deploy/lambda/lib/JProfByBotStack.ts index b6206179..5fe0aada 100644 --- a/.deploy/lambda/lib/JProfByBotStack.ts +++ b/.deploy/lambda/lib/JProfByBotStack.ts @@ -3,6 +3,8 @@ import {Construct} from 'constructs'; import * as apigateway from 'aws-cdk-lib/aws-apigateway'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as lambda from 'aws-cdk-lib/aws-lambda'; +import {Architecture} from 'aws-cdk-lib/aws-lambda'; +import * as secrets from 'aws-cdk-lib/aws-secretsmanager'; 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'; @@ -13,6 +15,14 @@ export class JProfByBotStack extends cdk.Stack { constructor(scope: Construct, id: string, props: JProfByBotStackProps) { super(scope, id, props); + const secretPaymentProviderTokens = new secrets.Secret(this, 'jprof-by-bot-secret-payment-provider-tokens', { + secretName: 'jprof-by-bot-secret-payment-provider-tokens', + secretObjectValue: { + 1: cdk.SecretValue.unsafePlainText('test'), + 2: cdk.SecretValue.unsafePlainText('production'), + } + }); + const votesTable = new dynamodb.Table(this, 'jprof-by-bot-table-votes', { tableName: 'jprof-by-bot-table-votes', partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING}, @@ -103,7 +113,8 @@ export class JProfByBotStack extends cdk.Stack { code: lambda.Code.fromAsset('../../pins/unpin/build/libs/jprof_by_bot-pins-unpin-all.jar'), handler: 'by.jprof.telegram.bot.pins.unpin.Handler', environment: { - 'LOG_THRESHOLD': 'DEBUG', + 'JAVA_TOOL_OPTIONS': '-Dsoftware.amazon.awssdk.http.async.service.impl=software.amazon.awssdk.http.nio.netty.NettySdkAsyncHttpService', + 'LOG_THRESHOLD': 'INFO', 'TABLE_PINS': pinsTable.tableName, 'TOKEN_TELEGRAM_BOT': props.telegramToken, }, @@ -123,13 +134,20 @@ export class JProfByBotStack extends cdk.Stack { }); const layerLibGL = new lambda.LayerVersion(this, 'jprof-by-bot-lambda-layer-libGL', { + layerVersionName: 'libGL', code: lambda.Code.fromAsset('layers/libGL.zip'), - compatibleRuntimes: [lambda.Runtime.JAVA_11], + compatibleArchitectures: [Architecture.X86_64], }); const layerLibfontconfig = new lambda.LayerVersion(this, 'jprof-by-bot-lambda-layer-libfontconfig', { + layerVersionName: 'libfontconfig', code: lambda.Code.fromAsset('layers/libfontconfig.zip'), - compatibleRuntimes: [lambda.Runtime.JAVA_11], + compatibleArchitectures: [Architecture.X86_64], }); + const layerParametersAndSecretsLambdaExtension = lambda.LayerVersion.fromLayerVersionArn( + this, + 'jprof-by-bot-lambda-layer-parametersAndSecretsLambdaExtension', + 'arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:2' + ) const lambdaWebhookTimeout = cdk.Duration.seconds(29); const lambdaWebhook = new lambda.Function(this, 'jprof-by-bot-lambda-webhook', { @@ -138,15 +156,17 @@ export class JProfByBotStack extends cdk.Stack { layers: [ layerLibGL, layerLibfontconfig, + layerParametersAndSecretsLambdaExtension, ], timeout: lambdaWebhookTimeout, maxEventAge: cdk.Duration.minutes(5), retryAttempts: 0, - memorySize: 512, + memorySize: 768, code: lambda.Code.fromAsset('../../launchers/lambda/build/libs/jprof_by_bot-launchers-lambda-all.jar'), handler: 'by.jprof.telegram.bot.launchers.lambda.JProf', environment: { - 'LOG_THRESHOLD': 'DEBUG', + 'JAVA_TOOL_OPTIONS': '-Dsoftware.amazon.awssdk.http.async.service.impl=software.amazon.awssdk.http.nio.netty.NettySdkAsyncHttpService', + 'LOG_THRESHOLD': 'INFO', 'TABLE_VOTES': votesTable.tableName, 'TABLE_YOUTUBE_CHANNELS_WHITELIST': youtubeChannelsWhitelistTable.tableName, 'TABLE_KOTLIN_MENTIONS': kotlinMentionsTable.tableName, @@ -177,7 +197,8 @@ export class JProfByBotStack extends cdk.Stack { 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', + 'JAVA_TOOL_OPTIONS': '-Dsoftware.amazon.awssdk.http.async.service.impl=software.amazon.awssdk.http.nio.netty.NettySdkAsyncHttpService', + 'LOG_THRESHOLD': 'INFO', 'TABLE_URBAN_WORDS_OF_THE_DAY': urbanWordsOfTheDayTable.tableName, 'TABLE_LANGUAGE_ROOMS': languageRoomsTable.tableName, 'TOKEN_TELEGRAM_BOT': props.telegramToken, @@ -198,6 +219,8 @@ export class JProfByBotStack extends cdk.Stack { ], }); + secretPaymentProviderTokens.grantRead(lambdaWebhook); + votesTable.grantReadWriteData(lambdaWebhook); youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook); diff --git a/README.adoc b/README.adoc index 2411b957..fd229ac8 100644 --- a/README.adoc +++ b/README.adoc @@ -12,7 +12,9 @@ Official Telegram bot of Java Professionals BY community. * Converts some currencies to EUR and USD * Posts scheduled messages from this repo's `posts` branch * Expands LeetCode links +* Tells users' local times * Regulates our English Rooms & teaches us new English words +* Sells pins and custom statuses 🤑 So, it just brings some fun and interactivity in our chat. diff --git a/build.gradle.kts b/build.gradle.kts index cd417b68..6577441c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,9 @@ subprojects { withType { // Workaround for https://stackoverflow.com/q/42174572/750510 archiveBaseName.set(rootProject.name + "-" + this.project.path.removePrefix(":").replace(":", "-")) + manifest { + attributes["Multi-Release"] = true + } } withType { useJUnitPlatform { @@ -40,6 +43,9 @@ subprojects { } withType { transform(Log4j2PluginsCacheFileTransformer::class.java) + manifest { + attributes["Multi-Release"] = 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 e4e886f8..b34fadb1 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 @@ -23,7 +23,7 @@ class UpdateProcessingPipeline( processors .map { launch(exceptionHandler(it)) { - logger.debug("Processing update with ${it::class.simpleName}") + logger.trace("Processing update with ${it::class.simpleName}") it.process(update) } } diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..247e9e7a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +source .env && +./gradlew clean shadowJar && +pushd .deploy/lambda && +npm install && +cdk deploy --outputs-file=cdk.out/outputs.json --require-approval=never && +popd 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 index b26997a5..515c1799 100644 --- a/english/src/main/kotlin/by/jprof/telegram/bot/english/EnglishCommandUpdateProcessor.kt +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/EnglishCommandUpdateProcessor.kt @@ -18,9 +18,12 @@ 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 dev.inmo.tgbotapi.utils.PreviewFeature +import dev.inmo.tgbotapi.utils.RiskFeature import io.ktor.utils.io.streams.asInput import org.apache.logging.log4j.LogManager +@OptIn(PreviewFeature::class, RiskFeature::class) class EnglishCommandUpdateProcessor( private val languageRoomDAO: LanguageRoomDAO, private val bot: RequestsExecutor, @@ -30,7 +33,7 @@ class EnglishCommandUpdateProcessor( } override suspend fun process(update: Update) { - val update = update.asBaseMessageUpdate() ?: return + @Suppress("NAME_SHADOWING") val update = update.asBaseMessageUpdate() ?: return val message = update.data.asContentMessage() ?: return val content = message.content.asTextContent() ?: return val (_, argument) = (content.textSources + null) 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 index 7b0b0405..ad0dd1c5 100644 --- a/english/src/main/kotlin/by/jprof/telegram/bot/english/ExplainerUpdateProcessor.kt +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/ExplainerUpdateProcessor.kt @@ -28,6 +28,7 @@ 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 dev.inmo.tgbotapi.utils.PreviewFeature import java.time.Duration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -36,6 +37,7 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.time.withTimeoutOrNull import org.apache.logging.log4j.LogManager +@OptIn(PreviewFeature::class) class ExplainerUpdateProcessor( private val languageRoomDAO: LanguageRoomDAO, private val urbanDictionaryClient: UrbanDictionaryClient, @@ -47,7 +49,7 @@ class ExplainerUpdateProcessor( } override suspend fun process(update: Update) { - val update = update.asBaseMessageUpdate() ?: return + @Suppress("NAME_SHADOWING") val update = update.asBaseMessageUpdate() ?: return val roomId = update.data.chat.id val message = update.data.asContentMessage() ?: return val content = message.content.asTextContent() ?: return @@ -60,7 +62,7 @@ class ExplainerUpdateProcessor( val emphasizedWords = extractEmphasizedWords(content) - logger.debug("Emphasized words: $emphasizedWords") + logger.info("Emphasized words: $emphasizedWords") val explanations = fetchExplanations(emphasizedWords) @@ -151,7 +153,7 @@ class ExplainerUpdateProcessor( private fun StringBuilder.dictionaryDotDevExplanations(dictionaryDotDevExplanations: Collection?) { dictionaryDotDevExplanations?.let { definitions -> - definitions.take(3).forEachIndexed { index, definition -> + definitions.take(3).forEachIndexed { _, definition -> val link = definition.sourceUrls?.firstOrNull() if (link != null) { 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 index fd6394e6..493bba9f 100644 --- a/english/src/main/kotlin/by/jprof/telegram/bot/english/MotherfuckingUpdateProcessor.kt +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/MotherfuckingUpdateProcessor.kt @@ -13,10 +13,12 @@ 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 dev.inmo.tgbotapi.utils.PreviewFeature import io.ktor.utils.io.streams.asInput import kotlin.random.Random import org.apache.logging.log4j.LogManager +@OptIn(PreviewFeature::class) class MotherfuckingUpdateProcessor( private val languageRoomDAO: LanguageRoomDAO, private val bot: RequestsExecutor, @@ -26,7 +28,7 @@ class MotherfuckingUpdateProcessor( } override suspend fun process(update: Update) { - val update = update.asBaseMessageUpdate() ?: return + @Suppress("NAME_SHADOWING") val update = update.asBaseMessageUpdate() ?: return val roomId = update.data.chat.id val message = update.data.asContentMessage() ?: return val content = message.content.asTextContent() ?: return 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 index a91bd61e..74144a6f 100644 --- a/english/src/main/kotlin/by/jprof/telegram/bot/english/UrbanWordOfTheDayUpdateProcessor.kt +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/UrbanWordOfTheDayUpdateProcessor.kt @@ -14,9 +14,11 @@ 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 dev.inmo.tgbotapi.utils.PreviewFeature import java.time.LocalDate import org.apache.logging.log4j.LogManager +@OptIn(PreviewFeature::class) class UrbanWordOfTheDayUpdateProcessor( private val languageRoomDAO: LanguageRoomDAO, private val urbanWordOfTheDayDAO: UrbanWordOfTheDayDAO, @@ -27,7 +29,7 @@ class UrbanWordOfTheDayUpdateProcessor( } override suspend fun process(update: Update) { - val update = update.asBaseMessageUpdate() ?: return + @Suppress("NAME_SHADOWING") val update = update.asBaseMessageUpdate() ?: return val roomId = update.data.chat.id val message = update.data.asContentMessage() ?: return val content = message.content.asTextContent() ?: return 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 index da13aabc..e9e541e4 100644 --- a/english/src/main/kotlin/by/jprof/telegram/bot/english/WhatWordUpdateProcessor.kt +++ b/english/src/main/kotlin/by/jprof/telegram/bot/english/WhatWordUpdateProcessor.kt @@ -11,9 +11,11 @@ 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 dev.inmo.tgbotapi.utils.PreviewFeature import io.ktor.utils.io.streams.asInput import org.apache.logging.log4j.LogManager +@OptIn(PreviewFeature::class) class WhatWordUpdateProcessor( private val languageRoomDAO: LanguageRoomDAO, private val bot: RequestsExecutor, @@ -23,7 +25,7 @@ class WhatWordUpdateProcessor( } override suspend fun process(update: Update) { - val update = update.asBaseMessageUpdate() ?: return + @Suppress("NAME_SHADOWING") val update = update.asBaseMessageUpdate() ?: return val roomId = update.data.chat.id val message = update.data.asContentMessage() ?: return val content = message.content.asTextContent() ?: return diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce66c3e9..af9f6e6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,7 @@ aws-lambda-java-events = { group = "com.amazonaws", name = "aws-lambda-java-even aws-lambda-java-core = { group = "com.amazonaws", name = "aws-lambda-java-core", version.ref = "aws-lambda-java-core" } aws-lambda-java-log4j2 = { group = "com.amazonaws", name = "aws-lambda-java-log4j2", version.ref = "aws-lambda-java-log4j2" } dynamodb = { group = "software.amazon.awssdk", name = "dynamodb", version.ref = "awssdk" } +secretsmanager = { group = "software.amazon.awssdk", name = "secretsmanager", version.ref = "awssdk" } sfn = { group = "software.amazon.awssdk", name = "sfn", version.ref = "awssdk" } koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } diff --git a/kotlin/src/main/kotlin/by/jprof/telegram/bot/kotlin/KotlinMentionsUpdateProcessor.kt b/kotlin/src/main/kotlin/by/jprof/telegram/bot/kotlin/KotlinMentionsUpdateProcessor.kt index 36242faf..77d77e57 100644 --- a/kotlin/src/main/kotlin/by/jprof/telegram/bot/kotlin/KotlinMentionsUpdateProcessor.kt +++ b/kotlin/src/main/kotlin/by/jprof/telegram/bot/kotlin/KotlinMentionsUpdateProcessor.kt @@ -50,7 +50,7 @@ class KotlinMentionsUpdateProcessor( else -> return } - logger.info("Kotlin mentioned!") + logger.debug("Kotlin mentioned!") val now = Instant.now() val user = (message as? FromUserMessage)?.user ?: return @@ -128,6 +128,6 @@ class KotlinMentionsUpdateProcessor( replyToMessageId = message.messageId, ) - logger.info("Kotlin mention reported!") + logger.debug("Kotlin mention reported!") } } diff --git a/launchers/lambda/build.gradle.kts b/launchers/lambda/build.gradle.kts index a1bbab28..78d0f9fe 100644 --- a/launchers/lambda/build.gradle.kts +++ b/launchers/lambda/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { implementation(libs.bundles.aws.lambda) + implementation(libs.secretsmanager) implementation(libs.koin.core) implementation(libs.tgbotapi) implementation(libs.bundles.log4j) @@ -28,4 +29,6 @@ dependencies { implementation(project.projects.english.urbanWordOfTheDay.dynamodb) implementation(project.projects.english.urbanDictionary) implementation(project.projects.english.dictionaryapiDev) + implementation(project.projects.shop.provider) + implementation(project.projects.shop) } 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 336e606c..fbfc8cf4 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 @@ -7,6 +7,7 @@ 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.secretsModule 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 @@ -45,6 +46,7 @@ class JProf : RequestHandler, K init { startKoin { modules( + secretsModule, envModule, databaseModule, jsonModule, @@ -64,10 +66,11 @@ class JProf : RequestHandler, K override fun handleRequest(input: APIGatewayV2HTTPEvent, context: Context): APIGatewayV2HTTPResponse { logger.debug("Incoming request: {}", input) + logger.info(input.body) val update = json.decodeFromString(UpdateDeserializationStrategy, input.body ?: return OK) - logger.debug("Parsed update: {}", update) + logger.info("{}", update) pipeline.process(update) 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 1af82d2f..77e687ba 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 @@ -1,7 +1,13 @@ package by.jprof.telegram.bot.launchers.lambda.config +import by.jprof.telegram.bot.shop.provider.ChatProviderTokens +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json import org.koin.core.qualifier.named import org.koin.dsl.module +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient + +private const val SECRET_PAYMENT_PROVIDER_TOKENS = "jprof-by-bot-secret-payment-provider-tokens" const val TOKEN_TELEGRAM_BOT = "TOKEN_TELEGRAM_BOT" const val TOKEN_YOUTUBE_API = "TOKEN_YOUTUBE_API" @@ -42,4 +48,14 @@ val envModule = module { single(named(TIMEOUT)) { System.getenv(TIMEOUT)!!.toLong() } + + single { + val json: Json = get() + val secrets: SecretsManagerClient = get() + val secret = secrets.getSecretValue { + it.secretId(SECRET_PAYMENT_PROVIDER_TOKENS) + } + + json.decodeFromString(secret.secretString()) + } } 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 d9180f1a..b6ce17a2 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 @@ -21,6 +21,15 @@ import by.jprof.telegram.bot.quizoji.QuizojiOptionUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiQuestionUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiStartCommandUpdateProcessor import by.jprof.telegram.bot.quizoji.QuizojiVoteUpdateProcessor +import by.jprof.telegram.bot.shop.ForwardedPaymentStartCommandUpdateProcessor +import by.jprof.telegram.bot.shop.PinsPreCheckoutQueryUpdateProcessor +import by.jprof.telegram.bot.shop.PinsSuccessfulPaymentUpdateProcessor +import by.jprof.telegram.bot.shop.RichCommandUpdateProcessor +import by.jprof.telegram.bot.shop.RichPreCheckoutQueryUpdateProcessor +import by.jprof.telegram.bot.shop.RichSuccessfulPaymentUpdateProcessor +import by.jprof.telegram.bot.shop.SupportCommandUpdateProcessor +import by.jprof.telegram.bot.shop.SupportPreCheckoutQueryUpdateProcessor +import by.jprof.telegram.bot.shop.SupportSuccessfulPaymentUpdateProcessor import by.jprof.telegram.bot.times.TimeCommandUpdateProcessor import by.jprof.telegram.bot.times.TimeZoneCommandUpdateProcessor import by.jprof.telegram.bot.youtube.YouTubeUpdateProcessor @@ -116,6 +125,8 @@ val pipelineModule = module { pinDAO = get(), unpinScheduler = get(), bot = get(), + providerTokens = get(), + json = get(), ) } @@ -191,4 +202,70 @@ val pipelineModule = module { bot = get(), ) } + + single(named("RichCommandUpdateProcessor")) { + RichCommandUpdateProcessor( + bot = get(), + providerTokens = get(), + json = get(), + ) + } + + single(named("RichPreCheckoutQueryUpdateProcessor")) { + RichPreCheckoutQueryUpdateProcessor( + bot = get(), + json = get(), + ) + } + + single(named("RichSuccessfulPaymentUpdateProcessor")) { + RichSuccessfulPaymentUpdateProcessor( + bot = get(), + json = get(), + ) + } + + single(named("SupportCommandUpdateProcessor")) { + SupportCommandUpdateProcessor( + bot = get(), + providerTokens = get(), + json = get(), + ) + } + + single(named("SupportPreCheckoutQueryUpdateProcessor")) { + SupportPreCheckoutQueryUpdateProcessor( + bot = get(), + json = get(), + ) + } + + single(named("SupportSuccessfulPaymentUpdateProcessor")) { + SupportSuccessfulPaymentUpdateProcessor( + bot = get(), + json = get(), + ) + } + + single(named("PinsPreCheckoutQueryUpdateProcessor")) { + PinsPreCheckoutQueryUpdateProcessor( + bot = get(), + json = get(), + moniesDAO = get(), + ) + } + + single(named("PinsSuccessfulPaymentUpdateProcessor")) { + PinsSuccessfulPaymentUpdateProcessor( + bot = get(), + json = get(), + moniesDAO = get(), + ) + } + + single(named("ForwardedPaymentStartCommandUpdateProcessor")) { + ForwardedPaymentStartCommandUpdateProcessor( + bot = get(), + ) + } } diff --git a/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/secrets.kt b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/secrets.kt new file mode 100644 index 00000000..ef60d2dc --- /dev/null +++ b/launchers/lambda/src/main/kotlin/by/jprof/telegram/bot/launchers/lambda/config/secrets.kt @@ -0,0 +1,12 @@ +package by.jprof.telegram.bot.launchers.lambda.config + +import kotlinx.serialization.ExperimentalSerializationApi +import org.koin.dsl.module +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient + +@ExperimentalSerializationApi +val secretsModule = module { + single { + SecretsManagerClient.create() + } +} diff --git a/pins/build.gradle.kts b/pins/build.gradle.kts index f1d8c651..a833199b 100644 --- a/pins/build.gradle.kts +++ b/pins/build.gradle.kts @@ -6,6 +6,8 @@ dependencies { api(project.projects.core) api(libs.tgbotapi) api(project.projects.monies) + implementation(project.projects.shop.provider) + implementation(project.projects.shop.payload) implementation(project.projects.pins.dto) implementation(project.projects.pins.scheduler) implementation(libs.log4j.api) 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 24c17603..2f7f9945 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 @@ -8,6 +8,7 @@ import by.jprof.telegram.bot.pins.dao.PinDAO import by.jprof.telegram.bot.pins.dto.Unpin import by.jprof.telegram.bot.pins.model.Pin import by.jprof.telegram.bot.pins.model.PinDuration +import by.jprof.telegram.bot.pins.model.PinRequest import by.jprof.telegram.bot.pins.scheduler.UnpinScheduler import by.jprof.telegram.bot.pins.utils.PinRequestFinder import by.jprof.telegram.bot.pins.utils.beggar @@ -16,13 +17,21 @@ import by.jprof.telegram.bot.pins.utils.negativeDuration import by.jprof.telegram.bot.pins.utils.tooManyPinnedMessages import by.jprof.telegram.bot.pins.utils.tooPositiveDuration import by.jprof.telegram.bot.pins.utils.unrecognizedDuration +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.PinsPayload +import by.jprof.telegram.bot.shop.provider.ChatProviderTokens import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage +import dev.inmo.tgbotapi.extensions.api.send.payments.sendInvoice import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.types.message.MarkdownV2 +import dev.inmo.tgbotapi.types.payments.LabeledPrice import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.tgbotapi.utils.PreviewFeature import java.time.Duration +import kotlin.random.Random +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.apache.logging.log4j.LogManager @PreviewFeature @@ -31,6 +40,8 @@ class PinCommandUpdateProcessor( private val pinDAO: PinDAO, private val unpinScheduler: UnpinScheduler, private val bot: RequestsExecutor, + private val providerTokens: ChatProviderTokens, + private val json: Json, private val pinRequestFinder: PinRequestFinder = PinRequestFinder.DEFAULT ) : UpdateProcessor { companion object { @@ -39,7 +50,7 @@ class PinCommandUpdateProcessor( override suspend fun process(update: Update) { pinRequestFinder(update)?.let { pin -> - logger.info("Pin requested: {}", pin) + logger.debug("Pin requested: {}", pin) val monies = moniesDAO.get(pin.user.id.chatId, pin.chat.id.chatId) ?: Monies(pin.user.id.chatId, pin.chat.id.chatId) val pins = monies.pins ?: 0 @@ -47,6 +58,7 @@ class PinCommandUpdateProcessor( if (pin.message == null) { bot.reply(to = pin.request, text = help(pins), parseMode = MarkdownV2) + pinsShop(pin) return } @@ -71,6 +83,7 @@ class PinCommandUpdateProcessor( if (pins < pin.price) { bot.reply(to = pin.request, text = beggar(pins, pin.price), parseMode = MarkdownV2) + pinsShop(pin) return } @@ -90,9 +103,36 @@ class PinCommandUpdateProcessor( chatId = pin.chat.id.chatId ttl = duration.duration.seconds }) + if (Random.nextInt(4) == 0) { + pinsShop(pin) + } } catch (e: Exception) { logger.error("Failed to pin a message", e) } } } + + private suspend fun pinsShop(pin: PinRequest) { + val chatProviderToken = providerTokens[pin.request.chat.id.chatId] + + if (chatProviderToken != null) { + bot.sendInvoice( + chatId = pin.request.chat.id, + title = "168 пинов", + description = "Неделя закрепа", + payload = json.encodeToString(PinsPayload( + pins = 168, + chat = pin.request.chat.id.chatId, + )), + providerToken = chatProviderToken, + currency = "USD", + prices = listOf( + LabeledPrice("Пины × 168", 200) + ), + startParameter = "forwarded_payment", + replyToMessageId = pin.request.messageId, + allowSendingWithoutReply = true, + ) + } + } } diff --git a/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinReplyUpdateProcessor.kt b/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinReplyUpdateProcessor.kt index 78d15560..e181883e 100644 --- a/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinReplyUpdateProcessor.kt +++ b/pins/src/main/kotlin/by/jprof/telegram/bot/pins/PinReplyUpdateProcessor.kt @@ -28,7 +28,7 @@ class PinReplyUpdateProcessor( return } - logger.info("{} replied to {}", replier, pin) + logger.debug("{} replied to {}", replier, pin) val monies = moniesDAO.get(pin.userId, replyTo.chat.id.chatId) ?: Monies(pin.userId, replyTo.chat.id.chatId) diff --git a/settings.gradle.kts b/settings.gradle.kts index f11ae81b..bc16e876 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,4 +43,7 @@ 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(":shop:provider") +include(":shop:payload") +include(":shop") include(":launchers:lambda") diff --git a/shop/README.adoc b/shop/README.adoc new file mode 100644 index 00000000..057b5174 --- /dev/null +++ b/shop/README.adoc @@ -0,0 +1,3 @@ += Shop + +This feature allows users to buy link:../pins[pins] and custom titles. diff --git a/shop/build.gradle.kts b/shop/build.gradle.kts new file mode 100644 index 00000000..3373f256 --- /dev/null +++ b/shop/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + kotlin("jvm") +} + +dependencies { + api(project.projects.core) + api(libs.tgbotapi) + implementation(project.projects.shop.provider) + implementation(project.projects.shop.payload) + implementation(project.projects.monies) + 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/shop/payload/README.adoc b/shop/payload/README.adoc new file mode 100644 index 00000000..cf17aea9 --- /dev/null +++ b/shop/payload/README.adoc @@ -0,0 +1,3 @@ += Shop / Payload + +Payloads to use in `payload` field of https://core.telegram.org/bots/api#sendinvoice[TG Bot API invoices]. diff --git a/shop/payload/build.gradle.kts b/shop/payload/build.gradle.kts new file mode 100644 index 00000000..d6aa50b9 --- /dev/null +++ b/shop/payload/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") +} + +dependencies { + implementation(libs.kotlinx.serialization.core) +} diff --git a/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/Payload.kt b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/Payload.kt new file mode 100644 index 00000000..b969d4e5 --- /dev/null +++ b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/Payload.kt @@ -0,0 +1,6 @@ +package by.jprof.telegram.bot.shop.payload + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface Payload diff --git a/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/PinsPayload.kt b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/PinsPayload.kt new file mode 100644 index 00000000..830fcb2d --- /dev/null +++ b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/PinsPayload.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.shop.payload + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("pins") +data class PinsPayload( + val pins: Long, + val chat: Long, +) : Payload diff --git a/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/RichPayload.kt b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/RichPayload.kt new file mode 100644 index 00000000..d59e2e30 --- /dev/null +++ b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/RichPayload.kt @@ -0,0 +1,11 @@ +package by.jprof.telegram.bot.shop.payload + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("rich") +data class RichPayload( + val status: String, + val chat: Long, +) : Payload diff --git a/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/SupportPayload.kt b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/SupportPayload.kt new file mode 100644 index 00000000..349bb27b --- /dev/null +++ b/shop/payload/src/main/kotlin/by/jprof/telegram/bot/shop/payload/SupportPayload.kt @@ -0,0 +1,10 @@ +package by.jprof.telegram.bot.shop.payload + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +@SerialName("support") +data class SupportPayload( + val chat: Long, +) : Payload diff --git a/shop/provider/README.adoc b/shop/provider/README.adoc new file mode 100644 index 00000000..cd142820 --- /dev/null +++ b/shop/provider/README.adoc @@ -0,0 +1,5 @@ += Shop / Provider + +Container for https://core.telegram.org/bots/payments#getting-a-token[provider tokens]. +Different chats/groups could have different tokens. +It is useful for test purposes: a test group could use test provider (e.g. https://stripe.com/docs/testing[Stripe Test]). diff --git a/shop/provider/build.gradle.kts b/shop/provider/build.gradle.kts new file mode 100644 index 00000000..444baaa3 --- /dev/null +++ b/shop/provider/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + kotlin("jvm") +} diff --git a/shop/provider/src/main/kotlin/by/jprof/telegram/bot/shop/provider/ChatProviderTokens.kt b/shop/provider/src/main/kotlin/by/jprof/telegram/bot/shop/provider/ChatProviderTokens.kt new file mode 100644 index 00000000..809cbc55 --- /dev/null +++ b/shop/provider/src/main/kotlin/by/jprof/telegram/bot/shop/provider/ChatProviderTokens.kt @@ -0,0 +1,3 @@ +package by.jprof.telegram.bot.shop.provider + +typealias ChatProviderTokens = Map diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/ForwardedPaymentStartCommandUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/ForwardedPaymentStartCommandUpdateProcessor.kt new file mode 100644 index 00000000..d12f6238 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/ForwardedPaymentStartCommandUpdateProcessor.kt @@ -0,0 +1,34 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.utils.forwardedPaymentsAreNotSupported +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.sendMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPrivateChat +import dev.inmo.tgbotapi.extensions.utils.asPrivateContentMessage +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.message.MarkdownV2ParseMode +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature + +@PreviewFeature +class ForwardedPaymentStartCommandUpdateProcessor( + private val bot: RequestsExecutor, +) : UpdateProcessor { + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPrivateContentMessage() ?: return + val chat = message.chat.asPrivateChat() ?: return + val content = message.content.asTextContent() ?: return + + if (content.text != "/start forwarded_payment") { + return + } + + bot.sendMessage( + chat = chat, + text = forwardedPaymentsAreNotSupported(), + parseMode = MarkdownV2ParseMode, + ) + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/PinsPreCheckoutQueryUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/PinsPreCheckoutQueryUpdateProcessor.kt new file mode 100644 index 00000000..e7551cce --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/PinsPreCheckoutQueryUpdateProcessor.kt @@ -0,0 +1,52 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.monies.dao.MoniesDAO +import by.jprof.telegram.bot.monies.model.Monies +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.PinsPayload +import by.jprof.telegram.bot.shop.utils.tooManyPins +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.answers.payments.answerPreCheckoutQueryError +import dev.inmo.tgbotapi.extensions.api.answers.payments.answerPreCheckoutQueryOk +import dev.inmo.tgbotapi.extensions.utils.asPreCheckoutQueryUpdate +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.apache.logging.log4j.LogManager + +@OptIn(PreviewFeature::class) +class PinsPreCheckoutQueryUpdateProcessor( + private val bot: RequestsExecutor, + private val json: Json, + private val moniesDAO: MoniesDAO, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(PinsPreCheckoutQueryUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val preCheckoutQuery = update.asPreCheckoutQueryUpdate()?.data ?: return + val payload = try { + json.decodeFromString(preCheckoutQuery.invoicePayload) as PinsPayload + } catch (_: Exception) { + return + } + + logger.info("{}", payload) + + val monies = moniesDAO.get(preCheckoutQuery.user.id.chatId, payload.chat) ?: Monies(preCheckoutQuery.user.id.chatId, payload.chat) + val pins = monies.pins ?: 0 + + if (pins > 9999) { + logger.info("{} already has enough ({}) pins!", preCheckoutQuery.user, pins) + + bot.answerPreCheckoutQueryError(preCheckoutQuery, tooManyPins()) + } else { + logger.info("Selling pins to {}", preCheckoutQuery.user) + + bot.answerPreCheckoutQueryOk(preCheckoutQuery) + } + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/PinsSuccessfulPaymentUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/PinsSuccessfulPaymentUpdateProcessor.kt new file mode 100644 index 00000000..b68b6425 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/PinsSuccessfulPaymentUpdateProcessor.kt @@ -0,0 +1,51 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.monies.dao.MoniesDAO +import by.jprof.telegram.bot.monies.model.Money +import by.jprof.telegram.bot.monies.model.Monies +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.PinsPayload +import dev.inmo.tgbotapi.abstracts.FromUser +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPossiblyPaymentMessage +import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.apache.logging.log4j.LogManager + +@OptIn(PreviewFeature::class) +class PinsSuccessfulPaymentUpdateProcessor( + private val bot: RequestsExecutor, + private val json: Json, + private val moniesDAO: MoniesDAO, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(PinsSuccessfulPaymentUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPossiblyPaymentMessage() ?: return + val user = (message as? FromUser)?.user ?: return + val payment = (message.paymentInfo as? SuccessfulPaymentEvent)?.payment ?: return + val payload = try { + json.decodeFromString(payment.invoicePayload) as PinsPayload + } catch (_: Exception) { + return + } + + val monies = moniesDAO.get(user.id.chatId, payload.chat) ?: Monies(user.id.chatId, payload.chat) + val pins = monies.pins ?: 0 + + moniesDAO.save( + monies.copy( + monies = monies.monies + (Money.PINS to pins) + ) + ) + bot.reply(message, "Thank you for the purchase!") + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichCommandUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichCommandUpdateProcessor.kt new file mode 100644 index 00000000..f6a31c88 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichCommandUpdateProcessor.kt @@ -0,0 +1,67 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.RichPayload +import by.jprof.telegram.bot.shop.provider.ChatProviderTokens +import by.jprof.telegram.bot.shop.utils.notAShop +import by.jprof.telegram.bot.shop.utils.richInvoiceDescription +import by.jprof.telegram.bot.shop.utils.richInvoiceTitle +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.payments.sendInvoice +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asBotCommandTextSource +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.message.MarkdownV2ParseMode +import dev.inmo.tgbotapi.types.payments.LabeledPrice +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private val item = LabeledPrice("Флекс × 9000", 500) + +@OptIn(PreviewFeature::class) +class RichCommandUpdateProcessor( + private val bot: RequestsExecutor, + private val providerTokens: ChatProviderTokens, + private val json: Json, +) : UpdateProcessor { + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + val command = content.textSources + .mapNotNull { it.asBotCommandTextSource() } + .firstOrNull { it.command == "rich" || it.command == "vip" } ?: return + val status = when (command.command) { + "rich" -> "I AM RICH" + else -> "V.I.P." + } + + val chatProviderToken = providerTokens[message.chat.id.chatId] + + if (chatProviderToken == null) { + bot.reply( + to = message, + text = notAShop(), + parseMode = MarkdownV2ParseMode, + disableNotification = true, + ) + } else { + bot.sendInvoice( + chatId = message.chat.id, + title = richInvoiceTitle(status), + description = richInvoiceDescription(), + payload = json.encodeToString(RichPayload(status = status, chat = message.chat.id.chatId)), + providerToken = chatProviderToken, + currency = currency, + prices = listOf(item), + startParameter = "forwarded_payment", + replyToMessageId = message.messageId, + allowSendingWithoutReply = true, + ) + } + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichPreCheckoutQueryUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichPreCheckoutQueryUpdateProcessor.kt new file mode 100644 index 00000000..9ade0827 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichPreCheckoutQueryUpdateProcessor.kt @@ -0,0 +1,36 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.RichPayload +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.answers.payments.answerPreCheckoutQueryOk +import dev.inmo.tgbotapi.extensions.utils.asPreCheckoutQueryUpdate +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.apache.logging.log4j.LogManager + +@OptIn(PreviewFeature::class) +class RichPreCheckoutQueryUpdateProcessor( + private val bot: RequestsExecutor, + private val json: Json, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(RichPreCheckoutQueryUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val preCheckoutQuery = update.asPreCheckoutQueryUpdate()?.data ?: return + val payload = try { + json.decodeFromString(preCheckoutQuery.invoicePayload) as RichPayload + } catch (_: Exception) { + return + } + + logger.info("{}", payload) + + bot.answerPreCheckoutQueryOk(preCheckoutQuery) + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichSuccessfulPaymentUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichSuccessfulPaymentUpdateProcessor.kt new file mode 100644 index 00000000..4c44cdcc --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/RichSuccessfulPaymentUpdateProcessor.kt @@ -0,0 +1,47 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.RichPayload +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.chat.members.setChatAdministratorCustomTitle +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asChatEventMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.types.chat.PrivateChat +import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent +import dev.inmo.tgbotapi.types.toChatId +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.apache.logging.log4j.LogManager + +@OptIn(PreviewFeature::class) +class RichSuccessfulPaymentUpdateProcessor( + private val bot: RequestsExecutor, + private val json: Json, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(RichSuccessfulPaymentUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data ?: return + val chat = message.chat as? PrivateChat ?: return + val payment = (message.asChatEventMessage()?.chatEvent as? SuccessfulPaymentEvent)?.payment ?: return + + val payload = try { + json.decodeFromString(payment.invoicePayload) as RichPayload + } catch (_: Exception) { + return + } + + bot.setChatAdministratorCustomTitle( + chatId = payload.chat.toChatId(), + userId = chat.id, + customTitle = payload.status + ) + bot.reply(message, "Thank you for the purchase!") + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportCommandUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportCommandUpdateProcessor.kt new file mode 100644 index 00000000..153b2c8b --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportCommandUpdateProcessor.kt @@ -0,0 +1,64 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.SupportPayload +import by.jprof.telegram.bot.shop.provider.ChatProviderTokens +import by.jprof.telegram.bot.shop.utils.notAShop +import by.jprof.telegram.bot.shop.utils.supportInvoiceDescription +import by.jprof.telegram.bot.shop.utils.supportInvoiceTitle +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.payments.sendInvoice +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asBotCommandTextSource +import dev.inmo.tgbotapi.extensions.utils.asContentMessage +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asTextContent +import dev.inmo.tgbotapi.types.message.MarkdownV2ParseMode +import dev.inmo.tgbotapi.types.payments.LabeledPrice +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private val item = LabeledPrice("❤️ × ∞", 100) + +@OptIn(PreviewFeature::class) +class SupportCommandUpdateProcessor( + private val bot: RequestsExecutor, + private val providerTokens: ChatProviderTokens, + private val json: Json, +) : UpdateProcessor { + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asContentMessage() ?: return + val content = message.content.asTextContent() ?: return + + if (content.textSources.mapNotNull { it.asBotCommandTextSource() }.none { it.command == "support" }) { + return + } + + val chatProviderToken = providerTokens[message.chat.id.chatId] + + if (chatProviderToken == null) { + bot.reply( + to = message, + text = notAShop(), + parseMode = MarkdownV2ParseMode, + disableNotification = true, + ) + } else { + bot.sendInvoice( + chatId = message.chat.id, + title = supportInvoiceTitle(), + description = supportInvoiceDescription(), + payload = json.encodeToString(SupportPayload(chat = message.chat.id.chatId)), + providerToken = chatProviderToken, + currency = currency, + prices = listOf(item), + startParameter = "forwarded_payment", + replyToMessageId = message.messageId, + allowSendingWithoutReply = true, + ) + } + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportPreCheckoutQueryUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportPreCheckoutQueryUpdateProcessor.kt new file mode 100644 index 00000000..e1f8e1db --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportPreCheckoutQueryUpdateProcessor.kt @@ -0,0 +1,36 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.SupportPayload +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.answers.payments.answerPreCheckoutQueryOk +import dev.inmo.tgbotapi.extensions.utils.asPreCheckoutQueryUpdate +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.apache.logging.log4j.LogManager + +@OptIn(PreviewFeature::class) +class SupportPreCheckoutQueryUpdateProcessor( + private val bot: RequestsExecutor, + private val json: Json, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(SupportPreCheckoutQueryUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val preCheckoutQuery = update.asPreCheckoutQueryUpdate()?.data ?: return + val payload = try { + json.decodeFromString(preCheckoutQuery.invoicePayload) as SupportPayload + } catch (_: Exception) { + return + } + + logger.info("{}", payload) + + bot.answerPreCheckoutQueryOk(preCheckoutQuery) + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportSuccessfulPaymentUpdateProcessor.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportSuccessfulPaymentUpdateProcessor.kt new file mode 100644 index 00000000..40d54800 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/SupportSuccessfulPaymentUpdateProcessor.kt @@ -0,0 +1,39 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.core.UpdateProcessor +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.SupportPayload +import dev.inmo.tgbotapi.abstracts.FromUser +import dev.inmo.tgbotapi.bot.RequestsExecutor +import dev.inmo.tgbotapi.extensions.api.send.reply +import dev.inmo.tgbotapi.extensions.utils.asMessageUpdate +import dev.inmo.tgbotapi.extensions.utils.asPossiblyPaymentMessage +import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentEvent +import dev.inmo.tgbotapi.types.update.abstracts.Update +import dev.inmo.tgbotapi.utils.PreviewFeature +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.apache.logging.log4j.LogManager + +@OptIn(PreviewFeature::class) +class SupportSuccessfulPaymentUpdateProcessor( + private val bot: RequestsExecutor, + private val json: Json, +) : UpdateProcessor { + companion object { + private val logger = LogManager.getLogger(SupportSuccessfulPaymentUpdateProcessor::class.java)!! + } + + override suspend fun process(update: Update) { + val message = update.asMessageUpdate()?.data?.asPossiblyPaymentMessage() ?: return + val user = (message as? FromUser)?.user ?: return + val payment = (message.paymentInfo as? SuccessfulPaymentEvent)?.payment ?: return + val payload = try { + json.decodeFromString(payment.invoicePayload) as SupportPayload + } catch (_: Exception) { + return + } + + bot.reply(message, "Thank you for the donation!") + } +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/app.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/app.kt new file mode 100644 index 00000000..b4731bd8 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/app.kt @@ -0,0 +1,19 @@ +package by.jprof.telegram.bot.shop + +import by.jprof.telegram.bot.shop.payload.Payload +import by.jprof.telegram.bot.shop.payload.RichPayload +import by.jprof.telegram.bot.shop.payload.SupportPayload +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +fun main() { + val json = Json { + encodeDefaults = false + ignoreUnknownKeys = true + } + + val s = json.encodeToString(RichPayload(status = "status", chat = 123)) + + println(json.decodeFromString(s)) +} diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/currency.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/currency.kt new file mode 100644 index 00000000..3e2b7879 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/currency.kt @@ -0,0 +1,3 @@ +package by.jprof.telegram.bot.shop + +const val currency = "USD" diff --git a/shop/src/main/kotlin/by/jprof/telegram/bot/shop/utils/messages.kt b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/utils/messages.kt new file mode 100644 index 00000000..b945c917 --- /dev/null +++ b/shop/src/main/kotlin/by/jprof/telegram/bot/shop/utils/messages.kt @@ -0,0 +1,57 @@ +package by.jprof.telegram.bot.shop.utils + +import java.text.MessageFormat + +private val notAShopMessages = listOf( + "В этом чате ничего не продаётся\\!" +) + +internal fun notAShop(): String { + return notAShopMessages.random() +} + +private val richInvoiceTitleMessages = listOf( + "Статус \"{0}\"" +) + +fun richInvoiceTitle(status: String): String = + MessageFormat(richInvoiceTitleMessages.random()) + .format(arrayOf(status)) + +private val richInvoiceDescriptionMessages = listOf( + "Отображается в чате возле ваших сообщений" +) + +fun richInvoiceDescription(): String = + richInvoiceDescriptionMessages.random() + +private val supportInvoiceTitleMessages = listOf( + "Поддержка JProf", +) + +fun supportInvoiceTitle(): String = + supportInvoiceTitleMessages.random() + +private val supportInvoiceDescriptionMessages = listOf( + "Безвозмездная поддержка JProf" +) + +fun supportInvoiceDescription(): String = + supportInvoiceDescriptionMessages.random() + +private val forwardedPaymentsAreNotSupportedMessages = listOf( + "Ты пытаешься оплатить пересланный инвойс\\. Покупка и оплата возможны только под оригинальным сообщением\\." +) + +internal fun forwardedPaymentsAreNotSupported(): String { + return forwardedPaymentsAreNotSupportedMessages.random() +} + +private val tooManyPinsMessages = listOf( + "У тебя и так хватает пинов\\!", + "У тебя и так много пинов\\!", +) + +internal fun tooManyPins(): String { + return tooManyPinsMessages.random() +} diff --git a/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeCommandUpdateProcessor.kt b/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeCommandUpdateProcessor.kt index ccfebf2b..f1ac6d59 100644 --- a/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeCommandUpdateProcessor.kt +++ b/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeCommandUpdateProcessor.kt @@ -60,7 +60,7 @@ class TimeCommandUpdateProcessor( val messageTime = Instant.ofEpochMilli(message.date.unixMillisLong).toLocalDateTime(timeZone) ?: return "" val now = Instant.now().toLocalDateTime(timeZone) ?: return "" - logger.info( + logger.debug( "Replying to {}. Author: {}. TimeZone: {}. Message time: {}, current time: {}", message, author, timeZone, messageTime, now ) @@ -91,7 +91,7 @@ class TimeCommandUpdateProcessor( val allMentions = mentionsWithTimeZones + textMentionsWithTimeZones val now = Instant.now() - logger.info( + logger.debug( "Mentions: {}. Text mentions: {}. Combined mentions with TimeZones: {}", mentions, textMentions, allMentions ) diff --git a/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeZoneCommandUpdateProcessor.kt b/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeZoneCommandUpdateProcessor.kt index 9763210d..cc29dea6 100644 --- a/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeZoneCommandUpdateProcessor.kt +++ b/times/src/main/kotlin/by/jprof/telegram/bot/times/TimeZoneCommandUpdateProcessor.kt @@ -28,7 +28,7 @@ class TimeZoneCommandUpdateProcessor( override suspend fun process(update: Update) { timeZoneParser(update)?.let { timeZone -> - logger.info("TimeZone requested: {}", timeZone) + logger.debug("TimeZone requested: {}", timeZone) when (timeZone.value) { TimeZoneValue.Unrecognized -> replyToUnrecognizedTimeZoneRequest(timeZone) diff --git a/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt b/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt index ff56dc82..bac72c4c 100644 --- a/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt +++ b/youtube/src/main/kotlin/by/jprof/telegram/bot/youtube/YouTubeUpdateProcessor.kt @@ -43,7 +43,7 @@ class YouTubeUpdateProcessor( companion object { private val logger = LogManager.getLogger(YouTubeUpdateProcessor::class.java)!! private val linkRegex = - "^.*((youtu.be/)|(v/)|(/u/\\w/)|(embed/)|(watch\\?))\\??v?=?([^#&?]*).*".toRegex() + "https?://(?:m.)?(?:www\\.)?youtu(?:\\.be/|(?:be-nocookie|be)\\.com/(?:watch|\\w+\\?(?:feature=\\w+.\\w+&)?v=|v/|e/|embed/|user/(?:[\\w#]+/)+))(?[^&#?\\n]+)".toRegex() private const val ACCEPTED_DISPLAY_LEN = 500 } @@ -55,11 +55,9 @@ class YouTubeUpdateProcessor( } private suspend fun processMessage(message: Message) { - logger.debug("Processing message: {}", message) - val youTubeVideos = extractYoutubeVideos(message) ?: return - logger.debug("YouTube videos: {}", youTubeVideos) + logger.info("Detected {} YouTube videos: {}", youTubeVideos.size, youTubeVideos) supervisorScope { youTubeVideos @@ -76,20 +74,17 @@ class YouTubeUpdateProcessor( .mapNotNull { (it as? URLTextSource)?.source ?: (it as? TextLinkTextSource)?.url } - .mapNotNull { - linkRegex.matchEntire(it)?.destructured + .mapNotNull { url -> + linkRegex.matchEntire(url)?.groups?.get("id")?.value?.takeUnless { it.isBlank() } } - .map { (_, _, _, _, _, _, id) -> id } } } private suspend fun replyToYouTubeVideo(video: String, message: Message) { - logger.debug("YouTube video ID: {}", video) - val response = withContext(Dispatchers.IO) { youTube.videos().list(listOf("snippet", "statistics")).setId(listOf(video)).execute() } - val videoDetails = response.items.first() + val videoDetails = response.items.firstOrNull() ?: return val snippet = videoDetails.snippet val channelId = snippet.channelId