Skip to content

Commit ab3c587

Browse files
authored
Merge pull request #121 from /issues/116
Some API bits
2 parents cd9146f + c3cf7f2 commit ab3c587

File tree

21 files changed

+307
-51
lines changed

21 files changed

+307
-51
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1-
package me.madhead.tyzenhaus.core.currencies
1+
package me.madhead.tyzenhaus.core.service
22

33
import me.madhead.tyzenhaus.repository.BalanceRepository
44

55
/**
66
* Lists currencies used in transactions of the group.
77
*/
8-
class ChatCurrenciesService(
8+
class GroupCurrenciesService(
99
private val balanceRepository: BalanceRepository,
1010
) {
1111
/**
1212
* Lists currencies used in transactions of the [group].
1313
*/
14-
fun groupCurrencies(group: Long): List<String> {
14+
fun groupCurrencies(group: Long): List<String>? {
1515
return balanceRepository
1616
.get(group)
1717
?.balance
1818
?.keys
1919
?.takeUnless { it.isEmpty() }
2020
?.toList()
21-
?: listOf("USD", "EUR", "RUB")
2221
}
2322
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package me.madhead.tyzenhaus.core.service
2+
3+
import dev.inmo.tgbotapi.bot.RequestsExecutor
4+
import dev.inmo.tgbotapi.types.ChatId
5+
import dev.inmo.tgbotapi.types.UserId
6+
import me.madhead.tyzenhaus.core.telegram.updates.expense.getChatMemberSafe
7+
import me.madhead.tyzenhaus.entity.group.members.GroupMember
8+
import me.madhead.tyzenhaus.entity.group.members.GroupMembers
9+
import me.madhead.tyzenhaus.repository.GroupConfigRepository
10+
11+
/**
12+
* Lists group members.
13+
*/
14+
class GroupMembersService(
15+
private val groupConfigRepository: GroupConfigRepository,
16+
private val requestsExecutor: RequestsExecutor,
17+
) {
18+
/**
19+
* Lists members of the [group].
20+
*/
21+
suspend fun groupMembers(group: Long): GroupMembers? {
22+
val groupConfig = groupConfigRepository.get(group) ?: return null
23+
val chatMembers = groupConfig.members.map { requestsExecutor.getChatMemberSafe(ChatId(groupConfig.id), UserId(it)) }
24+
25+
return GroupMembers(
26+
id = groupConfig.id,
27+
members = chatMembers.map {
28+
GroupMember(
29+
id = it.user.id.chatId,
30+
firstName = it.user.firstName,
31+
lastName = it.user.lastName,
32+
username = it.user.username?.usernameWithoutAt,
33+
)
34+
}
35+
)
36+
}
37+
}

core/src/main/kotlin/me/madhead/tyzenhaus/core/telegram/updates/expense/AmountReplyUpdateProcessor.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import dev.inmo.tgbotapi.types.message.content.TextContent
1010
import dev.inmo.tgbotapi.types.update.MessageUpdate
1111
import dev.inmo.tgbotapi.types.update.abstracts.Update
1212
import java.math.BigDecimal
13-
import me.madhead.tyzenhaus.core.currencies.ChatCurrenciesService
13+
import me.madhead.tyzenhaus.core.service.GroupCurrenciesService
1414
import me.madhead.tyzenhaus.core.telegram.updates.UpdateProcessor
1515
import me.madhead.tyzenhaus.core.telegram.updates.UpdateReaction
1616
import me.madhead.tyzenhaus.core.telegram.updates.groupId
@@ -29,7 +29,7 @@ import org.apache.logging.log4j.LogManager
2929
class AmountReplyUpdateProcessor(
3030
private val requestsExecutor: RequestsExecutor,
3131
private val dialogStateRepository: DialogStateRepository,
32-
private val chatCurrenciesService: ChatCurrenciesService,
32+
private val groupCurrenciesService: GroupCurrenciesService,
3333
) : UpdateProcessor {
3434
companion object {
3535
private val logger = LogManager.getLogger(AmountReplyUpdateProcessor::class.java)!!
@@ -110,8 +110,7 @@ class AmountReplyUpdateProcessor(
110110
parseMode = MarkdownV2,
111111
replyToMessageId = message.messageId,
112112
replyMarkup = dev.inmo.tgbotapi.types.buttons.ReplyKeyboardMarkup(
113-
keyboard = chatCurrenciesService
114-
.groupCurrencies(update.groupId)
113+
keyboard = (groupCurrenciesService.groupCurrencies(update.groupId) ?: listOf("USD", "EUR", "RUB"))
115114
.map { listOf(SimpleKeyboardButton(it)) },
116115
resizeKeyboard = true,
117116
oneTimeKeyboard = true,

core/src/main/kotlin/me/madhead/tyzenhaus/core/telegram/updates/history/HistoryCommandUpdateProcessor.kt

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ import dev.inmo.tgbotapi.types.message.content.TextContent
99
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
1010
import dev.inmo.tgbotapi.types.update.MessageUpdate
1111
import dev.inmo.tgbotapi.types.update.abstracts.Update
12+
import java.time.Duration
13+
import java.time.Instant
14+
import java.util.UUID
1215
import me.madhead.tyzenhaus.core.telegram.updates.UpdateProcessor
1316
import me.madhead.tyzenhaus.core.telegram.updates.UpdateReaction
1417
import me.madhead.tyzenhaus.core.telegram.updates.groupId
1518
import me.madhead.tyzenhaus.core.telegram.updates.userId
19+
import me.madhead.tyzenhaus.entity.api.token.APIToken
20+
import me.madhead.tyzenhaus.entity.api.token.Scope
1621
import me.madhead.tyzenhaus.entity.dialog.state.DialogState
1722
import me.madhead.tyzenhaus.entity.group.config.GroupConfig
1823
import me.madhead.tyzenhaus.i18.I18N
24+
import me.madhead.tyzenhaus.repository.APITokenRepository
1925
import org.apache.logging.log4j.LogManager
2026

2127
/**
@@ -24,6 +30,7 @@ import org.apache.logging.log4j.LogManager
2430
class HistoryCommandUpdateProcessor(
2531
private val requestsExecutor: RequestsExecutor,
2632
private val me: ExtendedBot,
33+
private val apiTokenRepository: APITokenRepository,
2734
) : UpdateProcessor {
2835
companion object {
2936
private val logger = LogManager.getLogger(HistoryCommandUpdateProcessor::class.java)!!
@@ -40,9 +47,18 @@ class HistoryCommandUpdateProcessor(
4047
{
4148
logger.debug("{} asked for history in {}", update.userId, update.groupId)
4249

50+
val token = APIToken(
51+
token = UUID.randomUUID(),
52+
groupId = update.groupId,
53+
scope = Scope.HISTORY,
54+
validUntil = Instant.now() + @Suppress("MagicNumber") Duration.ofMinutes(30)
55+
)
56+
57+
apiTokenRepository.save(token)
58+
4359
requestsExecutor.sendMessage(
4460
chatId = update.data.chat.id,
45-
text = I18N(groupConfig?.language)["history.response", me.username.usernameWithoutAt, "TOKEN"],
61+
text = I18N(groupConfig?.language)["history.response", me.username.usernameWithoutAt, token.token.toString()],
4662
parseMode = MarkdownV2,
4763
)
4864
}

detekt.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ comments:
4040
matchDeclarationsOrder: true
4141
allowParamOnConstructorProperties: false
4242
UndocumentedPublicClass:
43-
active: false
43+
active: true
4444
excludes:
4545
- '**/test/**'
4646
searchInNestedClass: true
4747
searchInInnerClass: true
4848
searchInInnerObject: true
4949
searchInInnerInterface: true
5050
UndocumentedPublicFunction:
51-
active: false
51+
active: true
5252
excludes:
5353
- '**/test/**'
5454
UndocumentedPublicProperty:

entity/src/main/kotlin/me/madhead/tyzenhaus/entity/api/token/APIToken.kt

+2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import java.util.UUID
88
*
99
* Available scopes:
1010
* - HISTORY: Allows to read group's transaction history.
11+
* - EXPENSE: Allows to create new transactions.
1112
*/
1213
enum class Scope {
1314
HISTORY,
15+
EXPENSE,
1416
}
1517

1618
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package me.madhead.tyzenhaus.entity.group.members
2+
3+
import kotlinx.serialization.Serializable
4+
5+
/**
6+
* Information about a member of a group.
7+
*/
8+
@Serializable
9+
data class GroupMember(
10+
val id: Long,
11+
val firstName: String,
12+
val lastName: String,
13+
val username: String?,
14+
)
15+
16+
/**
17+
* Information about group members.
18+
*/
19+
@Serializable
20+
data class GroupMembers(
21+
val id: Long,
22+
val members: Collection<GroupMember> = emptySet(),
23+
)

gradle/libs.versions.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin
2222
kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinx-coroutines" }
2323
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
2424
ktor-server-metrics-micrometer = { module = "io.ktor:ktor-server-metrics-micrometer", version.ref = "ktor" }
25+
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
26+
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
2527
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
2628
kotlinx-serialization-bom = { module = "org.jetbrains.kotlinx:kotlinx-serialization-bom", version.ref = "kotlinx-serialization" }
2729
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
@@ -45,4 +47,4 @@ picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
4547

4648
[bundles]
4749
boms = ["kotlin-bom", "kotlinx-serialization-bom", "kotlinx-coroutines-bom", "log4j-bom", "junit-bom"]
48-
ktor = ["ktor-server-netty", "ktor-server-metrics-micrometer", "koin-ktor"]
50+
ktor = ["ktor-server-netty", "ktor-server-metrics-micrometer", "ktor-server-auth", "koin-ktor", "ktor-serialization-kotlinx-json"]

launcher/fly/requests.http

+13
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,16 @@ Content-Type: application/json
88
{
99
"url": "https://{{ngrok}}/{{telegram_token}}"
1010
}
11+
12+
### Validate initData
13+
POST https://{{ngrok}}/app/api/auth/validation
14+
Authorization: Bearer {{api_token}}
15+
16+
17+
### Get group members
18+
GET https://{{ngrok}}/app/api/group/members
19+
Authorization: Bearer {{api_token}}
20+
21+
### Get group currencies
22+
GET https://{{ngrok}}/app/api/group/currencies
23+
Authorization: Bearer {{api_token}}

launcher/fly/src/main/kotlin/me/madhead/tyzenhaus/launcher/fly/koin/db.kt

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package me.madhead.tyzenhaus.launcher.fly.koin
33
import io.ktor.server.config.ApplicationConfig
44
import java.net.URI
55
import javax.sql.DataSource
6+
import me.madhead.tyzenhaus.repository.APITokenRepository
67
import me.madhead.tyzenhaus.repository.BalanceRepository
78
import me.madhead.tyzenhaus.repository.DialogStateRepository
89
import me.madhead.tyzenhaus.repository.GroupConfigRepository
@@ -11,6 +12,7 @@ import me.madhead.tyzenhaus.repository.SupergroupRepository
1112
import me.madhead.tyzenhaus.repository.TransactionRepository
1213
import org.koin.dsl.module
1314
import org.postgresql.ds.PGSimpleDataSource
15+
import me.madhead.tyzenhaus.repository.postgresql.api.token.APITokenRepository as PostgreSQLAPITokenRepository
1416
import me.madhead.tyzenhaus.repository.postgresql.balance.BalanceRepository as PostgreSQLBalanceRepository
1517
import me.madhead.tyzenhaus.repository.postgresql.dialog.state.DialogStateRepository as PostgreSQLDialogStateRepository
1618
import me.madhead.tyzenhaus.repository.postgresql.group.config.GroupConfigRepository as PostgreSQLGroupConfigRepository
@@ -52,4 +54,8 @@ val dbModule = module {
5254
single<SupergroupRepository> {
5355
PostgreSQLSupegroupRepository(get())
5456
}
57+
58+
single<APITokenRepository> {
59+
PostgreSQLAPITokenRepository(get())
60+
}
5561
}

launcher/fly/src/main/kotlin/me/madhead/tyzenhaus/launcher/fly/koin/pipeline.kt

+2-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package me.madhead.tyzenhaus.launcher.fly.koin
22

3-
import dev.inmo.tgbotapi.types.ChatId
43
import dev.inmo.tgbotapi.types.chat.ExtendedBot
5-
import io.ktor.server.config.ApplicationConfig
6-
import me.madhead.tyzenhaus.core.currencies.ChatCurrenciesService
74
import me.madhead.tyzenhaus.core.debts.DebtsCalculator
85
import me.madhead.tyzenhaus.core.telegram.updates.UpdateProcessingPipeline
96
import me.madhead.tyzenhaus.core.telegram.updates.expense.AmountReplyUpdateProcessor
@@ -27,11 +24,6 @@ import me.madhead.tyzenhaus.core.telegram.updates.support.IDCommandUpdateProcess
2724
import org.koin.dsl.module
2825

2926
val pipelineModule = module {
30-
single {
31-
ChatCurrenciesService(
32-
balanceRepository = get(),
33-
)
34-
}
3527
single {
3628
DebtsCalculator()
3729
}
@@ -75,7 +67,7 @@ val pipelineModule = module {
7567
AmountReplyUpdateProcessor(
7668
requestsExecutor = get(),
7769
dialogStateRepository = get(),
78-
chatCurrenciesService = get(),
70+
groupCurrenciesService = get(),
7971
)
8072
}
8173
single {
@@ -143,6 +135,7 @@ val pipelineModule = module {
143135
HistoryCommandUpdateProcessor(
144136
requestsExecutor = get(),
145137
me = get(),
138+
apiTokenRepository = get(),
146139
)
147140
}
148141
single {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package me.madhead.tyzenhaus.launcher.fly.koin
2+
3+
import me.madhead.tyzenhaus.core.service.GroupCurrenciesService
4+
import me.madhead.tyzenhaus.core.service.GroupMembersService
5+
import org.koin.dsl.module
6+
7+
val serviceModule = module {
8+
single {
9+
GroupCurrenciesService(
10+
balanceRepository = get(),
11+
)
12+
}
13+
14+
single {
15+
GroupMembersService(
16+
requestsExecutor = get(),
17+
groupConfigRepository = get(),
18+
)
19+
}
20+
}

launcher/fly/src/main/kotlin/me/madhead/tyzenhaus/launcher/fly/modules/Tyzenhaus.kt

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
package me.madhead.tyzenhaus.launcher.fly.modules
22

3+
import io.ktor.serialization.kotlinx.json.json
34
import io.ktor.server.application.Application
45
import io.ktor.server.application.install
6+
import io.ktor.server.auth.Authentication
7+
import io.ktor.server.auth.bearer
58
import io.ktor.server.metrics.micrometer.MicrometerMetrics
69
import io.ktor.server.plugins.callloging.CallLogging
710
import io.ktor.server.plugins.compression.Compression
11+
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
812
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
913
import io.ktor.server.routing.routing
1014
import io.micrometer.prometheus.PrometheusMeterRegistry
15+
import java.time.Instant
16+
import java.util.UUID
1117
import me.madhead.tyzenhaus.launcher.fly.koin.configModule
1218
import me.madhead.tyzenhaus.launcher.fly.koin.dbModule
1319
import me.madhead.tyzenhaus.launcher.fly.koin.jsonModule
1420
import me.madhead.tyzenhaus.launcher.fly.koin.metricsModule
1521
import me.madhead.tyzenhaus.launcher.fly.koin.pipelineModule
22+
import me.madhead.tyzenhaus.launcher.fly.koin.serviceModule
1623
import me.madhead.tyzenhaus.launcher.fly.koin.telegramModule
1724
import me.madhead.tyzenhaus.launcher.fly.routes.metrics
1825
import me.madhead.tyzenhaus.launcher.fly.routes.miniApp
1926
import me.madhead.tyzenhaus.launcher.fly.routes.miniAppAPI
2027
import me.madhead.tyzenhaus.launcher.fly.routes.webhook
28+
import me.madhead.tyzenhaus.launcher.fly.security.APITokenPrincipal
29+
import me.madhead.tyzenhaus.repository.APITokenRepository
2130
import org.koin.ktor.ext.get
31+
import org.koin.ktor.ext.inject
2232
import org.koin.ktor.plugin.Koin
2333

34+
/**
35+
* Initializes and configures the application.
36+
*/
2437
fun Application.tyzenhaus() {
2538
install(DefaultHeaders)
2639
install(CallLogging)
@@ -29,18 +42,38 @@ fun Application.tyzenhaus() {
2942
install(Koin) {
3043
modules(
3144
configModule(environment.config),
45+
dbModule,
46+
serviceModule,
47+
pipelineModule,
3248
metricsModule,
3349
telegramModule,
3450
jsonModule,
35-
pipelineModule,
36-
dbModule,
3751
)
3852
}
3953

54+
install(ContentNegotiation) {
55+
json(this@tyzenhaus.get())
56+
}
57+
4058
install(MicrometerMetrics) {
4159
this.registry = this@tyzenhaus.get<PrometheusMeterRegistry>()
4260
}
4361

62+
install(Authentication) {
63+
bearer("api") {
64+
authenticate { credential ->
65+
val token = try {
66+
UUID.fromString(credential.token)
67+
} catch (_: Exception) {
68+
return@authenticate null
69+
}
70+
val tokenRepository by inject<APITokenRepository>()
71+
72+
tokenRepository.get(token)?.takeIf { it.validUntil > Instant.now() }?.let { APITokenPrincipal(it.groupId, it.scope) }
73+
}
74+
}
75+
}
76+
4477
routing {
4578
webhook()
4679
metrics()

0 commit comments

Comments
 (0)