Skip to content

Commit 9a40b1d

Browse files
committed
Pins
1 parent 118d495 commit 9a40b1d

File tree

12 files changed

+580
-5
lines changed

12 files changed

+580
-5
lines changed

.deploy/lambda/lib/JProfByBotStack.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as cdk from '@aws-cdk/core';
2-
import {Duration, RemovalPolicy} from '@aws-cdk/core';
32
import {JProfByBotStackProps} from './JProfByBotStackProps';
43
import * as dynamodb from '@aws-cdk/aws-dynamodb';
54
import * as lambda from '@aws-cdk/aws-lambda';
65
import * as apigateway from '@aws-cdk/aws-apigateway';
6+
import * as sfn from '@aws-cdk/aws-stepfunctions';
7+
import * as tasks from '@aws-cdk/aws-stepfunctions-tasks';
78

89
export class JProfByBotStack extends cdk.Stack {
910
constructor(scope: cdk.Construct, id: string, props: JProfByBotStackProps) {
@@ -13,35 +14,84 @@ export class JProfByBotStack extends cdk.Stack {
1314
tableName: 'jprof-by-bot-table-votes',
1415
partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING},
1516
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
16-
removalPolicy: RemovalPolicy.DESTROY,
17+
removalPolicy: cdk.RemovalPolicy.DESTROY,
1718
});
1819
const youtubeChannelsWhitelistTable = new dynamodb.Table(this, 'jprof-by-bot-table-youtube-channels-whitelist', {
1920
tableName: 'jprof-by-bot-table-youtube-channels-whitelist',
2021
partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING},
2122
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
23+
removalPolicy: cdk.RemovalPolicy.DESTROY,
2224
});
2325
const kotlinMentionsTable = new dynamodb.Table(this, 'jprof-by-bot-table-kotlin-mentions', {
2426
tableName: 'jprof-by-bot-table-kotlin-mentions',
2527
partitionKey: {name: 'chat', type: dynamodb.AttributeType.NUMBER},
2628
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
29+
removalPolicy: cdk.RemovalPolicy.DESTROY,
2730
});
2831
const dialogStatesTable = new dynamodb.Table(this, 'jprof-by-bot-table-dialog-states', {
2932
tableName: 'jprof-by-bot-table-dialog-states',
3033
partitionKey: {name: 'userId', type: dynamodb.AttributeType.NUMBER},
3134
sortKey: {name: 'chatId', type: dynamodb.AttributeType.NUMBER},
3235
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
36+
removalPolicy: cdk.RemovalPolicy.DESTROY,
3337
});
3438
const quizojisTable = new dynamodb.Table(this, 'jprof-by-bot-table-quizojis', {
3539
tableName: 'jprof-by-bot-table-quizojis',
3640
partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING},
3741
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
42+
removalPolicy: cdk.RemovalPolicy.DESTROY,
3843
});
3944
const moniesTable = new dynamodb.Table(this, 'jprof-by-bot-table-monies', {
4045
tableName: 'jprof-by-bot-table-monies',
4146
partitionKey: {name: 'user', type: dynamodb.AttributeType.NUMBER},
4247
sortKey: {name: 'chat', type: dynamodb.AttributeType.NUMBER},
4348
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
44-
removalPolicy: RemovalPolicy.DESTROY,
49+
removalPolicy: cdk.RemovalPolicy.DESTROY,
50+
});
51+
const pinsTable = new dynamodb.Table(this, 'jprof-by-bot-table-pins', {
52+
tableName: 'jprof-by-bot-table-pins',
53+
partitionKey: {name: 'messageId', type: dynamodb.AttributeType.NUMBER},
54+
sortKey: {name: 'chatId', type: dynamodb.AttributeType.NUMBER},
55+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
56+
removalPolicy: cdk.RemovalPolicy.DESTROY,
57+
});
58+
59+
pinsTable.addGlobalSecondaryIndex({
60+
indexName: 'chatId',
61+
partitionKey: {name: 'chatId', type: dynamodb.AttributeType.NUMBER},
62+
projectionType: dynamodb.ProjectionType.ALL,
63+
});
64+
pinsTable.addGlobalSecondaryIndex({
65+
indexName: 'userId',
66+
partitionKey: {name: 'userId', type: dynamodb.AttributeType.NUMBER},
67+
projectionType: dynamodb.ProjectionType.ALL,
68+
});
69+
70+
const lambdaUnpin = new lambda.Function(this, 'jprof-by-bot-lambda-unpin', {
71+
functionName: 'jprof-by-bot-lambda-unpin',
72+
runtime: lambda.Runtime.JAVA_11,
73+
timeout: cdk.Duration.seconds(30),
74+
memorySize: 512,
75+
code: lambda.Code.fromAsset('../../pins/unpin/build/libs/jprof_by_bot-pins-unpin-all.jar'),
76+
handler: 'by.jprof.telegram.bot.pins.unpin.Handler',
77+
environment: {
78+
'LOG_THRESHOLD': 'DEBUG',
79+
'TABLE_PINS': pinsTable.tableName,
80+
'TOKEN_TELEGRAM_BOT': props.telegramToken,
81+
},
82+
});
83+
84+
const stateMachineUnpin = new sfn.StateMachine(this, 'jprof-by-bot-state-machine-unpin', {
85+
stateMachineName: 'jprof-by-bot-state-machine-unpin',
86+
stateMachineType: sfn.StateMachineType.STANDARD,
87+
definition: new sfn.Wait(this, 'jprof-by-bot-state-machine-unpin-wait', {
88+
time: sfn.WaitTime.secondsPath('$.ttl'),
89+
}).next(new tasks.LambdaInvoke(this, 'jprof-by-bot-state-machine-unpin-unpin', {
90+
lambdaFunction: lambdaUnpin,
91+
invocationType: tasks.LambdaInvocationType.EVENT,
92+
retryOnServiceExceptions: false,
93+
})),
94+
tracingEnabled: false,
4595
});
4696

4797
const layerLibGL = new lambda.LayerVersion(this, 'jprof-by-bot-lambda-layer-libGL', {
@@ -60,7 +110,7 @@ export class JProfByBotStack extends cdk.Stack {
60110
layerLibGL,
61111
layerLibfontconfig,
62112
],
63-
timeout: Duration.seconds(30),
113+
timeout: cdk.Duration.seconds(30),
64114
memorySize: 1024,
65115
code: lambda.Code.fromAsset('../../runners/lambda/build/libs/jprof_by_bot-runners-lambda-all.jar'),
66116
handler: 'by.jprof.telegram.bot.runners.lambda.JProf',
@@ -71,17 +121,31 @@ export class JProfByBotStack extends cdk.Stack {
71121
'TABLE_KOTLIN_MENTIONS': kotlinMentionsTable.tableName,
72122
'TABLE_DIALOG_STATES': dialogStatesTable.tableName,
73123
'TABLE_QUIZOJIS': quizojisTable.tableName,
124+
'TABLE_MONIES': moniesTable.tableName,
125+
'TABLE_PINS': pinsTable.tableName,
126+
'STATE_MACHINE_UNPINS': stateMachineUnpin.stateMachineArn,
74127
'TOKEN_TELEGRAM_BOT': props.telegramToken,
75128
'TOKEN_YOUTUBE_API': props.youtubeToken,
76129
},
77130
});
78131

79132
votesTable.grantReadWriteData(lambdaWebhook);
133+
80134
youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook);
135+
81136
kotlinMentionsTable.grantReadWriteData(lambdaWebhook);
137+
82138
dialogStatesTable.grantReadWriteData(lambdaWebhook);
139+
83140
quizojisTable.grantReadWriteData(lambdaWebhook);
84141

142+
moniesTable.grantReadWriteData(lambdaWebhook);
143+
144+
pinsTable.grantReadWriteData(lambdaWebhook);
145+
pinsTable.grantReadWriteData(lambdaUnpin);
146+
147+
stateMachineUnpin.grantStartExecution(lambdaWebhook)
148+
85149
const api = new apigateway.RestApi(this, 'jprof-by-bot-api', {
86150
restApiName: 'jprof-by-bot-api',
87151
cloudWatchRole: false,

monies/src/main/kotlin/by/jprof/telegram/bot/monies/model/Monies.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ data class Monies(
55
val user: Long,
66
val chat: Long,
77
val monies: Map<Money, Int> = emptyMap(),
8-
)
8+
) {
9+
val pins: Int?
10+
get() = monies[Money.PINS]
11+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package by.jprof.telegram.bot.pins
2+
3+
import by.jprof.telegram.bot.core.UpdateProcessor
4+
import by.jprof.telegram.bot.monies.dao.MoniesDAO
5+
import by.jprof.telegram.bot.monies.model.Money
6+
import by.jprof.telegram.bot.monies.model.Monies
7+
import by.jprof.telegram.bot.pins.dao.PinDAO
8+
import by.jprof.telegram.bot.pins.dto.Unpin
9+
import by.jprof.telegram.bot.pins.model.Pin
10+
import by.jprof.telegram.bot.pins.model.PinDuration
11+
import by.jprof.telegram.bot.pins.scheduler.UnpinScheduler
12+
import by.jprof.telegram.bot.pins.utils.PinRequestFinder
13+
import by.jprof.telegram.bot.pins.utils.beggar
14+
import by.jprof.telegram.bot.pins.utils.help
15+
import by.jprof.telegram.bot.pins.utils.negativeDuration
16+
import by.jprof.telegram.bot.pins.utils.tooManyPinnedMessages
17+
import by.jprof.telegram.bot.pins.utils.tooPositiveDuration
18+
import by.jprof.telegram.bot.pins.utils.unrecognizedDuration
19+
import dev.inmo.tgbotapi.bot.RequestsExecutor
20+
import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage
21+
import dev.inmo.tgbotapi.extensions.api.send.reply
22+
import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2
23+
import dev.inmo.tgbotapi.types.update.abstracts.Update
24+
import dev.inmo.tgbotapi.utils.PreviewFeature
25+
import org.apache.logging.log4j.LogManager
26+
import java.time.Duration
27+
28+
@PreviewFeature
29+
class PinCommandUpdateProcessor(
30+
private val moniesDAO: MoniesDAO,
31+
private val pinDAO: PinDAO,
32+
private val unpinScheduler: UnpinScheduler,
33+
private val bot: RequestsExecutor,
34+
private val pinRequestFinder: PinRequestFinder = PinRequestFinder.DEFAULT
35+
) : UpdateProcessor {
36+
companion object {
37+
private val logger = LogManager.getLogger(PinCommandUpdateProcessor::class.java)!!
38+
}
39+
40+
override suspend fun process(update: Update) {
41+
pinRequestFinder(update)?.let { pin ->
42+
logger.info("Pin requested: {}", pin)
43+
44+
val monies = moniesDAO.get(pin.user.id.chatId, pin.chat.id.chatId) ?: Monies(pin.user.id.chatId, pin.chat.id.chatId)
45+
val pins = monies.pins ?: 0
46+
val duration = pin.duration
47+
48+
if (pin.message == null) {
49+
bot.reply(to = pin.request, text = help(pins), parseMode = MarkdownV2)
50+
51+
return
52+
}
53+
54+
if (duration !is PinDuration.Recognized) {
55+
bot.reply(to = pin.request, text = unrecognizedDuration(), parseMode = MarkdownV2)
56+
57+
return
58+
}
59+
60+
if (duration.duration.isNegative || duration.duration.isZero) {
61+
bot.reply(to = pin.request, text = negativeDuration(), parseMode = MarkdownV2)
62+
63+
return
64+
}
65+
66+
if (duration.duration > Duration.ofDays(90)) {
67+
bot.reply(to = pin.request, text = tooPositiveDuration(), parseMode = MarkdownV2)
68+
69+
return
70+
}
71+
72+
if (pins < pin.price) {
73+
bot.reply(to = pin.request, text = beggar(pins, pin.price), parseMode = MarkdownV2)
74+
75+
return
76+
}
77+
78+
if (pinDAO.findByChatId(pin.chat.id.chatId).size >= 5) {
79+
bot.reply(to = pin.request, text = tooManyPinnedMessages(), parseMode = MarkdownV2)
80+
81+
return
82+
}
83+
84+
try {
85+
bot.pinChatMessage(pin.message, disableNotification = true)
86+
pinDAO.save(Pin(pin.chat.id.chatId, pin.message.messageId, pin.user.id.chatId))
87+
moniesDAO.save(monies.copy(monies = monies.monies + (Money.PINS to (pins - pin.price))))
88+
unpinScheduler.scheduleUnpin(Unpin().apply {
89+
messageId = pin.message.messageId
90+
chatId = pin.chat.id.chatId
91+
userId = pin.user.id.chatId
92+
ttl = duration.duration.seconds
93+
})
94+
} catch (e: Exception) {
95+
logger.error("Failed to pin a message", e)
96+
}
97+
}
98+
}
99+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package by.jprof.telegram.bot.pins
2+
3+
import by.jprof.telegram.bot.core.UpdateProcessor
4+
import by.jprof.telegram.bot.monies.dao.MoniesDAO
5+
import by.jprof.telegram.bot.monies.model.Money
6+
import by.jprof.telegram.bot.monies.model.Monies
7+
import by.jprof.telegram.bot.pins.dao.PinDAO
8+
import dev.inmo.tgbotapi.types.message.abstracts.FromUserMessage
9+
import dev.inmo.tgbotapi.types.message.abstracts.PossiblyReplyMessage
10+
import dev.inmo.tgbotapi.types.update.MessageUpdate
11+
import dev.inmo.tgbotapi.types.update.abstracts.Update
12+
import org.apache.logging.log4j.LogManager
13+
14+
class PinReplyUpdateProcessor(
15+
private val moniesDAO: MoniesDAO,
16+
private val pinDAO: PinDAO,
17+
) : UpdateProcessor {
18+
companion object {
19+
private val logger = LogManager.getLogger(PinReplyUpdateProcessor::class.java)!!
20+
}
21+
22+
override suspend fun process(update: Update) {
23+
val replyTo = ((update as? MessageUpdate)?.data as? PossiblyReplyMessage)?.replyTo ?: return
24+
val replier = ((update as? MessageUpdate)?.data as? FromUserMessage)?.user ?: return
25+
val pin = pinDAO.get(replyTo.chat.id.chatId, replyTo.messageId) ?: return
26+
27+
if (replier.id.chatId == pin.userId) {
28+
return
29+
}
30+
31+
logger.info("{} replied to {}", replier, pin)
32+
33+
val monies = moniesDAO.get(pin.userId, replyTo.chat.id.chatId) ?: Monies(pin.userId, replyTo.chat.id.chatId)
34+
35+
moniesDAO.save(
36+
monies.copy(monies = monies.monies + (Money.PINS to (monies.monies[Money.PINS] ?: 0) + 1))
37+
)
38+
}
39+
}

0 commit comments

Comments
 (0)