Skip to content

Commit c01cbf9

Browse files
authored
Merge pull request #37 from JavaBy/feature/leetcode
Expand LeetCode links
2 parents 1167c97 + 0020783 commit c01cbf9

File tree

16 files changed

+289
-3
lines changed

16 files changed

+289
-3
lines changed

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Before you begin:
66

7-
- This project is powered by [Kotlin](https://kotlinlang.org), [TelegramBotAPI](https://github.com/InsanusMokrassar/TelegramBotAPI), [Koin](https://insert-koin.io), and [Skija](https://github.com/JetBrains/skija) libraries, [AWS](https://aws.amazon.com) services and [CDK](https://aws.amazon.com/cdk) with [TypeScript](https://www.typescriptlang.org), and [GitHub Actions](https://github.com/features/actions).
7+
- This project is powered by [Kotlin](https://kotlinlang.org), [TelegramBotAPI](https://github.com/InsanusMokrassar/TelegramBotAPI), [Koin](https://insert-koin.io), [Skija](https://github.com/JetBrains/skija), and [GraphQL](https://graphql.org) technologies, [AWS](https://aws.amazon.com) services and [CDK](https://aws.amazon.com/cdk) with [TypeScript](https://www.typescriptlang.org), and [GitHub Actions](https://github.com/features/actions).
88
- Have you read the [code of conduct](CODE_OF_CONDUCT.md)?
99
- Check out the [existing issues](https://github.com/JavaBy/jprof_by_bot/issues).
1010
- Discuss your plans in [the chat](https://t.me/jprof_by) or in an issue.

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Official Telegram bot of Java Professionals BY community.
1111
* Allows users to create polls with reply buttons (just like https://t.me/like[`@like`] bot)
1212
* Converts some currencies to EUR and USD
1313
* Posts scheduled messages from this repo's `posts` branch
14+
* Expand LeetCode links
1415

1516
So, it just brings some fun and interactivity in our chat.
1617

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ koin = "3.1.2"
1010

1111
ktor = "1.6.8"
1212

13+
graphql-kotlin = "5.5.0"
14+
1315
kotlinx-serialization = "1.3.0"
1416
jackson = "2.13.0"
1517
kaml = "0.43.0"
@@ -43,6 +45,8 @@ ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" }
4345
ktor-client-apache = { group = "io.ktor", name = "ktor-client-apache" }
4446
ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization" }
4547

48+
graphql-kotlin-ktor-client = { group = "com.expediagroup", name = "graphql-kotlin-ktor-client", version.ref = "graphql-kotlin" }
49+
4650
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
4751
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
4852
jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" }

leetcode/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
= LeetCode
2+
3+
This feature expands https://leetcode.com[LeetCode] links.

leetcode/build.gradle.kts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
plugins {
2+
kotlin("jvm")
3+
kotlin("plugin.serialization")
4+
}
5+
6+
dependencies {
7+
api(project.projects.core)
8+
api(libs.tgbotapi.core)
9+
implementation(libs.tgbotapi.extensions.api)
10+
implementation(libs.log4j.api)
11+
implementation(libs.graphql.kotlin.ktor.client)
12+
implementation(libs.kotlinx.serialization.core)
13+
14+
testImplementation(libs.junit.jupiter.api)
15+
testImplementation(libs.junit.jupiter.params)
16+
testImplementation(libs.mockk)
17+
testRuntimeOnly(libs.junit.jupiter.engine)
18+
testRuntimeOnly(libs.log4j.core)
19+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package by.jprof.telegram.bot.leetcode
2+
3+
import com.expediagroup.graphql.client.ktor.GraphQLKtorClient
4+
import com.expediagroup.graphql.client.types.GraphQLClientRequest
5+
import kotlinx.serialization.Required
6+
import kotlinx.serialization.Serializable
7+
import org.apache.logging.log4j.LogManager
8+
import java.io.Closeable
9+
import java.net.URL
10+
import kotlin.reflect.KClass
11+
12+
class GraphQLLeetCodeClient : LeetCodeClient, Closeable {
13+
companion object {
14+
private val logger = LogManager.getLogger(GraphQLLeetCodeClient::class.java)!!
15+
}
16+
17+
private val client = GraphQLKtorClient(
18+
url = URL("https://leetcode.com/graphql"),
19+
)
20+
21+
override suspend fun questionData(slug: String): Question? {
22+
val request = QuestionDataQuery(QuestionDataQuery.Variables(slug))
23+
val response = client.execute(request)
24+
25+
response.errors?.let { errors ->
26+
errors.forEach { error -> logger.error(error.message) }
27+
return null
28+
}
29+
30+
return response.data?.question
31+
}
32+
33+
override fun close() {
34+
client.close()
35+
}
36+
}
37+
38+
@Serializable
39+
private class QuestionDataQuery(
40+
override val variables: Variables,
41+
) : GraphQLClientRequest<QuestionData> {
42+
@Required
43+
override val query: String = """
44+
query questionData(${"$"}slug: String!) {
45+
question(titleSlug: ${"$"}slug) {
46+
title
47+
titleSlug
48+
content
49+
isPaidOnly
50+
difficulty
51+
likes
52+
dislikes
53+
categoryTitle
54+
}
55+
}
56+
""".trimIndent()
57+
58+
@Required
59+
override val operationName: String = "questionData"
60+
61+
override fun responseType(): KClass<QuestionData> = QuestionData::class
62+
63+
@Serializable
64+
data class Variables(
65+
val slug: String
66+
)
67+
}
68+
69+
@Serializable
70+
private data class QuestionData(
71+
val question: Question?,
72+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package by.jprof.telegram.bot.leetcode
2+
3+
interface LeetCodeClient {
4+
suspend fun questionData(slug: String): Question?
5+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package by.jprof.telegram.bot.leetcode
2+
3+
import by.jprof.telegram.bot.core.UpdateProcessor
4+
import dev.inmo.tgbotapi.bot.RequestsExecutor
5+
import dev.inmo.tgbotapi.extensions.api.send.reply
6+
import dev.inmo.tgbotapi.types.MessageEntity.textsources.TextLinkTextSource
7+
import dev.inmo.tgbotapi.types.MessageEntity.textsources.URLTextSource
8+
import dev.inmo.tgbotapi.types.ParseMode.MarkdownV2ParseMode
9+
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
10+
import dev.inmo.tgbotapi.types.message.abstracts.Message
11+
import dev.inmo.tgbotapi.types.message.content.TextContent
12+
import dev.inmo.tgbotapi.types.update.MessageUpdate
13+
import dev.inmo.tgbotapi.types.update.abstracts.Update
14+
import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common
15+
import kotlinx.coroutines.async
16+
import kotlinx.coroutines.awaitAll
17+
import kotlinx.coroutines.coroutineScope
18+
import org.apache.logging.log4j.LogManager
19+
20+
class LeetCodeUpdateProcessor(
21+
private val bot: RequestsExecutor,
22+
) : UpdateProcessor {
23+
companion object {
24+
private val logger = LogManager.getLogger(LeetCodeUpdateProcessor::class.java)!!
25+
private val slugExtractor: SlugExtractor = ::NaiveRegexSlugExtractor
26+
private val leetCodeClient: LeetCodeClient = GraphQLLeetCodeClient()
27+
}
28+
29+
override suspend fun process(update: Update) {
30+
@Suppress("NAME_SHADOWING") val update = update as? MessageUpdate ?: return
31+
32+
val slugs = extractSlugs(update.data)?.takeUnless { it.isEmpty() }?.distinct() ?: return
33+
34+
logger.debug("LeetCode slugs: {}", slugs)
35+
36+
val questions = coroutineScope {
37+
slugs.map { slug -> async { leetCodeClient.questionData(slug) } }.awaitAll()
38+
}.filterNotNull()
39+
40+
logger.debug("Questions: {}", questions)
41+
42+
questions.forEach { question ->
43+
bot.reply(
44+
to = update.data,
45+
text = questionInfo(question),
46+
parseMode = MarkdownV2ParseMode,
47+
)
48+
}
49+
}
50+
51+
private fun extractSlugs(message: Message): List<String>? =
52+
(message as? ContentMessage<*>)?.let { contentMessage ->
53+
(contentMessage.content as? TextContent)?.let { content ->
54+
content
55+
.textSources
56+
.mapNotNull {
57+
(it as? URLTextSource)?.source ?: (it as? TextLinkTextSource)?.url
58+
}
59+
.mapNotNull {
60+
slugExtractor(it)
61+
}
62+
}
63+
}
64+
65+
private fun questionInfo(question: Question): String {
66+
return """
67+
|${question.level()} *${question.title.escapeMarkdownV2Common()}* \(${question.categoryTitle}\) ${question.paidIndicator()}
68+
|
69+
|${question.markdownContent()}
70+
|
71+
|_Likes: ${question.likes} / Dislikes: ${question.dislikes}_
72+
""".trimMargin()
73+
}
74+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package by.jprof.telegram.bot.leetcode
2+
3+
private val slugRegex = "https?://leetcode\\.com/problems/(?<slug>.+?)/?".toRegex()
4+
5+
@Suppress("FunctionName")
6+
fun NaiveRegexSlugExtractor(message: String): String? {
7+
return slugRegex.matchEntire(message)?.groups?.get("slug")?.value
8+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package by.jprof.telegram.bot.leetcode
2+
3+
import dev.inmo.tgbotapi.utils.extensions.escapeMarkdownV2Common
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class Question(
8+
val title: String,
9+
val titleSlug: String,
10+
val content: String,
11+
val isPaidOnly: Boolean,
12+
val difficulty: String,
13+
val likes: Int,
14+
val dislikes: Int,
15+
val categoryTitle: String,
16+
) {
17+
fun level(): String = when (this.difficulty) {
18+
"Easy" -> "\uD83E\uDD64"
19+
"Medium" -> "\uD83E\uDD14"
20+
"Hard" -> "\uD83E\uDD18"
21+
else -> ""
22+
}
23+
24+
fun paidIndicator() = if (isPaidOnly) {
25+
"\uD83E\uDD11"
26+
} else {
27+
""
28+
}
29+
30+
fun markdownContent(): String = this.content
31+
.replace("&nbsp;", " ")
32+
.replace("&lt;", "<")
33+
.replace("&gt;", ">")
34+
.replace(Regex("<sup>(?<sup>.+?)</sup>")) {
35+
"^${it.groups["sup"]!!.value}"
36+
}
37+
38+
.escapeMarkdownV2Common()
39+
40+
.replace("<code\\>", "`")
41+
.replace("</code\\>", "`")
42+
43+
.replace("<pre\\>", "```")
44+
.replace("</pre\\>", "```")
45+
46+
.replace("<strong\\>", "*")
47+
.replace("</strong\\>", "*")
48+
49+
.replace("<em\\>", "_")
50+
.replace("</em\\>", "_")
51+
52+
.replace("<li\\>", "")
53+
.replace("</li\\>", "")
54+
55+
.replace("<p\\>", "")
56+
.replace("</p\\>", "")
57+
.replace("<ul\\>", "")
58+
.replace("</ul\\>", "")
59+
60+
.replace("\\n", "\n")
61+
.replace("\\t", "\t")
62+
}

0 commit comments

Comments
 (0)