Skip to content

Commit b1def5f

Browse files
committed
Urban Word of the Day handler
1 parent 960841a commit b1def5f

File tree

23 files changed

+395
-17
lines changed

23 files changed

+395
-17
lines changed

.deploy/lambda/bin/JProfByBotStack.ts

+5
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ if (process.env.TOKEN_YOUTUBE_API == null) {
1111
throw new Error('Undefined TOKEN_YOUTUBE_API')
1212
}
1313

14+
if (process.env.EMAIL_DAILY_URBAN_DICTIONARY == null) {
15+
throw new Error('Undefined EMAIL_DAILY_URBAN_DICTIONARY')
16+
}
17+
1418
const app = new cdk.App();
1519
new JProfByBotStack(
1620
app,
1721
'JProfByBotStack',
1822
{
1923
telegramToken: process.env.TOKEN_TELEGRAM_BOT,
2024
youtubeToken: process.env.TOKEN_YOUTUBE_API,
25+
dailyUrbanDictionaryEmail: process.env.EMAIL_DAILY_URBAN_DICTIONARY,
2126
env: {
2227
region: 'us-east-1'
2328
}

.deploy/lambda/lib/JProfByBotStack.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
55
import * as lambda from 'aws-cdk-lib/aws-lambda';
66
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
77
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';
8+
import * as ses from 'aws-cdk-lib/aws-ses';
9+
import * as sesActions from 'aws-cdk-lib/aws-ses-actions';
810
import {JProfByBotStackProps} from "./JProfByBotStackProps";
911

1012
export class JProfByBotStack extends cdk.Stack {
@@ -137,7 +139,7 @@ export class JProfByBotStack extends cdk.Stack {
137139
layerLibfontconfig,
138140
],
139141
timeout: cdk.Duration.seconds(30),
140-
memorySize: 1024,
142+
memorySize: 512,
141143
code: lambda.Code.fromAsset('../../launchers/lambda/build/libs/jprof_by_bot-launchers-lambda-all.jar'),
142144
handler: 'by.jprof.telegram.bot.launchers.lambda.JProf',
143145
environment: {
@@ -157,6 +159,35 @@ export class JProfByBotStack extends cdk.Stack {
157159
},
158160
});
159161

162+
const lambdaDailyUrbanDictionary = new lambda.Function(this, 'jprof-by-bot-lambda-daily-urban-dictionary', {
163+
functionName: 'jprof-by-bot-lambda-daily-urban-dictionary',
164+
runtime: lambda.Runtime.JAVA_11,
165+
timeout: cdk.Duration.seconds(30),
166+
memorySize: 512,
167+
code: lambda.Code.fromAsset('../../english/urban-dictionary-daily/build/libs/jprof_by_bot-english-urban-dictionary-daily-all.jar'),
168+
handler: 'by.jprof.telegram.bot.english.urban_dictionary_daily.Handler',
169+
environment: {
170+
'LOG_THRESHOLD': 'DEBUG',
171+
'TABLE_URBAN_WORDS_OF_THE_DAY': urbanWordsOfTheDayTable.tableName,
172+
'TABLE_LANGUAGE_ROOMS': languageRoomsTable.tableName,
173+
'TOKEN_TELEGRAM_BOT': props.telegramToken,
174+
'STATE_MACHINE_UNPINS': stateMachineUnpin.stateMachineArn,
175+
}
176+
});
177+
178+
new ses.ReceiptRuleSet(this, 'jprof-by-bot-receipt-rule-set-daily-urbandictionary', {
179+
receiptRuleSetName: 'jprof-by-bot-receipt-rule-set-daily-urbandictionary',
180+
rules: [
181+
{
182+
receiptRuleName: 'jprof-by-bot-receipt-rule-daily-urbandictionary',
183+
recipients: [props.dailyUrbanDictionaryEmail],
184+
actions: [
185+
new sesActions.Lambda({function: lambdaDailyUrbanDictionary})
186+
]
187+
}
188+
],
189+
});
190+
160191
votesTable.grantReadWriteData(lambdaWebhook);
161192

162193
youtubeChannelsWhitelistTable.grantReadData(lambdaWebhook);
@@ -175,8 +206,12 @@ export class JProfByBotStack extends cdk.Stack {
175206
timezonesTable.grantReadWriteData(lambdaWebhook);
176207

177208
languageRoomsTable.grantReadWriteData(lambdaWebhook);
209+
languageRoomsTable.grantReadData(lambdaDailyUrbanDictionary);
210+
211+
urbanWordsOfTheDayTable.grantWriteData(lambdaDailyUrbanDictionary);
178212

179213
stateMachineUnpin.grantStartExecution(lambdaWebhook);
214+
stateMachineUnpin.grantStartExecution(lambdaDailyUrbanDictionary);
180215

181216
const api = new apigateway.RestApi(this, 'jprof-by-bot-api', {
182217
restApiName: 'jprof-by-bot-api',

.deploy/lambda/lib/JProfByBotStackProps.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ import * as cdk from 'aws-cdk-lib';
33
export interface JProfByBotStackProps extends cdk.StackProps {
44
readonly telegramToken: string;
55
readonly youtubeToken: string;
6+
readonly dailyUrbanDictionaryEmail: string;
67
}

.deploy/lambda/package-lock.json

+50-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.deploy/lambda/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
"@types/prettier": "2.6.0",
1717
"jest": "^27.5.1",
1818
"ts-jest": "^27.1.4",
19-
"aws-cdk": "2.49.0",
19+
"aws-cdk": "2.53.0",
2020
"ts-node": "^10.9.1",
2121
"typescript": "~3.9.7",
2222
"jest-junit": "^14.0.1"
2323
},
2424
"dependencies": {
25-
"aws-cdk-lib": "2.49.0",
25+
"aws-cdk-lib": "2.53.0",
2626
"constructs": "^10.0.0",
2727
"source-map-support": "^0.5.21"
2828
}

.deploy/lambda/test/JProfByBotStack.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('JProfByBotStack', () => {
77
const stack = new JProfByBotStack(app, 'JProfByBotStack', {
88
telegramToken: 'TOKEN_TELEGRAM_BOT',
99
youtubeToken: 'TOKEN_YOUTUBE_API',
10+
dailyUrbanDictionaryEmail: 'EMAIL_DAILY_URBAN_DICTIONARY',
1011
env: {region: 'us-east-1'}
1112
});
1213
const template = Template.fromStack(stack);

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ build
44
.idea
55

66
*.private.env.json
7+
8+
.env
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
= English / Urban Dictionary Daily
2+
3+
Handler for https://urbandictionary.com[Urban Dictionary's] "Word of the Day" mails.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
kotlin("jvm")
3+
kotlin("plugin.serialization")
4+
id("com.github.johnrengelman.shadow")
5+
}
6+
7+
dependencies {
8+
implementation(libs.kotlinx.serialization.core)
9+
implementation(libs.kotlinx.serialization.json)
10+
implementation(libs.tgbotapi)
11+
implementation(projects.english.urbanDictionary)
12+
implementation(projects.english.urbanWordOfTheDay.dynamodb)
13+
implementation(projects.english.languageRooms.dynamodb)
14+
implementation(projects.english.urbanWordOfTheDayFormatter)
15+
implementation(projects.pins.sfn)
16+
17+
implementation(libs.bundles.aws.lambda)
18+
implementation(libs.koin.core)
19+
implementation(libs.kotlinx.coroutines.jdk8)
20+
implementation(libs.bundles.log4j)
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package by.jprof.telegram.bot.english.urban_dictionary_daily
2+
3+
import by.jprof.telegram.bot.english.language_rooms.dao.LanguageRoomDAO
4+
import by.jprof.telegram.bot.english.language_rooms.model.Language
5+
import by.jprof.telegram.bot.english.urban_dictionary.UrbanDictionaryClient
6+
import by.jprof.telegram.bot.english.urban_dictionary_daily.config.databaseModule
7+
import by.jprof.telegram.bot.english.urban_dictionary_daily.config.envModule
8+
import by.jprof.telegram.bot.english.urban_dictionary_daily.config.jsonModule
9+
import by.jprof.telegram.bot.english.urban_dictionary_daily.config.sfnModule
10+
import by.jprof.telegram.bot.english.urban_dictionary_daily.config.telegramModule
11+
import by.jprof.telegram.bot.english.urban_dictionary_daily.config.urbanDictionaryModule
12+
import by.jprof.telegram.bot.english.urban_dictionary_daily.model.Event
13+
import by.jprof.telegram.bot.english.urban_dictionary_definition_formatter.format
14+
import by.jprof.telegram.bot.english.urban_word_of_the_day.dao.UrbanWordOfTheDayDAO
15+
import by.jprof.telegram.bot.english.urban_word_of_the_day.model.UrbanWordOfTheDay
16+
import by.jprof.telegram.bot.pins.dto.Unpin
17+
import by.jprof.telegram.bot.pins.scheduler.UnpinScheduler
18+
import com.amazonaws.services.lambda.runtime.Context
19+
import com.amazonaws.services.lambda.runtime.RequestStreamHandler
20+
import dev.inmo.tgbotapi.bot.RequestsExecutor
21+
import dev.inmo.tgbotapi.extensions.api.chat.modify.pinChatMessage
22+
import dev.inmo.tgbotapi.extensions.api.send.sendMessage
23+
import dev.inmo.tgbotapi.types.ChatId
24+
import dev.inmo.tgbotapi.types.ChatIdWithThreadId
25+
import dev.inmo.tgbotapi.types.message.MarkdownV2
26+
import java.io.InputStream
27+
import java.io.OutputStream
28+
import java.time.LocalDate
29+
import kotlinx.coroutines.runBlocking
30+
import kotlinx.serialization.ExperimentalSerializationApi
31+
import kotlinx.serialization.json.Json
32+
import kotlinx.serialization.json.decodeFromStream
33+
import org.apache.logging.log4j.LogManager
34+
import org.koin.core.component.KoinComponent
35+
import org.koin.core.component.inject
36+
import org.koin.core.context.startKoin
37+
38+
@ExperimentalSerializationApi
39+
@Suppress("unused")
40+
class Handler : RequestStreamHandler, KoinComponent {
41+
companion object {
42+
private val logger = LogManager.getLogger(Handler::class.java)
43+
}
44+
45+
init {
46+
startKoin {
47+
modules(
48+
envModule,
49+
jsonModule,
50+
urbanDictionaryModule,
51+
databaseModule,
52+
telegramModule,
53+
sfnModule,
54+
)
55+
}
56+
}
57+
58+
private val json: Json by inject()
59+
private val urbanDictionaryClient: UrbanDictionaryClient by inject()
60+
private val urbanWordOfTheDayDAO: UrbanWordOfTheDayDAO by inject()
61+
private val languageRoomDAO: LanguageRoomDAO by inject()
62+
private val bot: RequestsExecutor by inject()
63+
private val unpinScheduler: UnpinScheduler by inject()
64+
65+
override fun handleRequest(input: InputStream, output: OutputStream, context: Context) = runBlocking {
66+
val event = json.decodeFromStream<Event>(input)
67+
68+
logger.debug("Parsed event: {}", event)
69+
70+
val term = event.records.firstOrNull()?.ses?.mail?.subject ?: return@runBlocking
71+
val definition = urbanDictionaryClient.define(term).maxByOrNull { it.thumbsUp } ?: return@runBlocking
72+
73+
logger.debug("Definition: {}", definition)
74+
75+
val urbanWordOfTheDay = UrbanWordOfTheDay(
76+
date = LocalDate.now(),
77+
word = definition.word,
78+
definition = definition.definition,
79+
example = definition.example,
80+
permalink = definition.permalink,
81+
)
82+
83+
urbanWordOfTheDayDAO.save(urbanWordOfTheDay)
84+
languageRoomDAO.getAll().filter { it.language == Language.ENGLISH && it.urbanWordOfTheDay }.forEach { languageRoom ->
85+
val message = bot.sendMessage(
86+
chatId = languageRoom.threadId?.let { ChatIdWithThreadId(languageRoom.chatId, it) } ?: ChatId(languageRoom.chatId),
87+
text = urbanWordOfTheDay.format().also { logger.debug("Formatted message: {}", it) },
88+
parseMode = MarkdownV2,
89+
disableWebPagePreview = true,
90+
)
91+
92+
bot.pinChatMessage(message, disableNotification = true)
93+
unpinScheduler.scheduleUnpin(
94+
Unpin().apply {
95+
chatId = languageRoom.chatId
96+
messageId = message.messageId
97+
ttl = (24 * 1.5 * 60 * 60).toLong()
98+
}
99+
)
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)