Skip to content

Commit

Permalink
Expose group members API
Browse files Browse the repository at this point in the history
  • Loading branch information
madhead committed Oct 18, 2023
1 parent 4478bb8 commit 4524868
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
import dev.inmo.tgbotapi.types.update.MessageUpdate
import dev.inmo.tgbotapi.types.update.abstracts.Update
import java.time.Duration
import java.time.Instant
import java.util.UUID
import me.madhead.tyzenhaus.core.telegram.updates.UpdateProcessor
import me.madhead.tyzenhaus.core.telegram.updates.UpdateReaction
import me.madhead.tyzenhaus.core.telegram.updates.groupId
import me.madhead.tyzenhaus.core.telegram.updates.userId
import me.madhead.tyzenhaus.entity.api.token.APIToken
import me.madhead.tyzenhaus.entity.api.token.Scope
import me.madhead.tyzenhaus.entity.dialog.state.DialogState
import me.madhead.tyzenhaus.entity.group.config.GroupConfig
import me.madhead.tyzenhaus.i18.I18N
import me.madhead.tyzenhaus.repository.APITokenRepository
import org.apache.logging.log4j.LogManager

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

val token = APIToken(
token = UUID.randomUUID(),
groupId = update.groupId,
scope = Scope.HISTORY,
validUntil = Instant.now() + Duration.ofMinutes(30)
)

apiTokenRepository.save(token)

requestsExecutor.sendMessage(
chatId = update.data.chat.id,
text = I18N(groupConfig?.language)["history.response", me.username.usernameWithoutAt, "TOKEN"],
text = I18N(groupConfig?.language)["history.response", me.username.usernameWithoutAt, token.token.toString()],
parseMode = MarkdownV2,
)
}
Expand Down
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin
kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinx-coroutines" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
ktor-server-metrics-micrometer = { module = "io.ktor:ktor-server-metrics-micrometer", version.ref = "ktor" }
ktor-server-auth = { module = "io.ktor:ktor-server-auth", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
kotlinx-serialization-bom = { module = "org.jetbrains.kotlinx:kotlinx-serialization-bom", version.ref = "kotlinx-serialization" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
Expand All @@ -45,4 +47,4 @@ picocli = { module = "info.picocli:picocli", version.ref = "picocli" }

[bundles]
boms = ["kotlin-bom", "kotlinx-serialization-bom", "kotlinx-coroutines-bom", "log4j-bom", "junit-bom"]
ktor = ["ktor-server-netty", "ktor-server-metrics-micrometer", "koin-ktor"]
ktor = ["ktor-server-netty", "ktor-server-metrics-micrometer", "ktor-server-auth", "koin-ktor", "ktor-serialization-kotlinx-json"]
4 changes: 4 additions & 0 deletions launcher/fly/requests.http
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ Content-Type: application/json
{
"url": "https://{{ngrok}}/{{telegram_token}}"
}

### Get group members
GET http://{{local}}/app/api/group/members
Authorization: Bearer {{api_token}}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package me.madhead.tyzenhaus.launcher.fly.koin
import io.ktor.server.config.ApplicationConfig
import java.net.URI
import javax.sql.DataSource
import me.madhead.tyzenhaus.repository.APITokenRepository
import me.madhead.tyzenhaus.repository.BalanceRepository
import me.madhead.tyzenhaus.repository.DialogStateRepository
import me.madhead.tyzenhaus.repository.GroupConfigRepository
Expand All @@ -11,6 +12,7 @@ import me.madhead.tyzenhaus.repository.SupergroupRepository
import me.madhead.tyzenhaus.repository.TransactionRepository
import org.koin.dsl.module
import org.postgresql.ds.PGSimpleDataSource
import me.madhead.tyzenhaus.repository.postgresql.api.token.APITokenRepository as PostgreSQLAPITokenRepository
import me.madhead.tyzenhaus.repository.postgresql.balance.BalanceRepository as PostgreSQLBalanceRepository
import me.madhead.tyzenhaus.repository.postgresql.dialog.state.DialogStateRepository as PostgreSQLDialogStateRepository
import me.madhead.tyzenhaus.repository.postgresql.group.config.GroupConfigRepository as PostgreSQLGroupConfigRepository
Expand Down Expand Up @@ -52,4 +54,8 @@ val dbModule = module {
single<SupergroupRepository> {
PostgreSQLSupegroupRepository(get())
}

single<APITokenRepository> {
PostgreSQLAPITokenRepository(get())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ val pipelineModule = module {
HistoryCommandUpdateProcessor(
requestsExecutor = get(),
me = get(),
apiTokenRepository = get(),
)
}
single {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
package me.madhead.tyzenhaus.launcher.fly.modules

import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.bearer
import io.ktor.server.metrics.micrometer.MicrometerMetrics
import io.ktor.server.plugins.callloging.CallLogging
import io.ktor.server.plugins.compression.Compression
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
import io.ktor.server.routing.routing
import io.micrometer.prometheus.PrometheusMeterRegistry
import java.time.Instant
import java.util.UUID
import me.madhead.tyzenhaus.launcher.fly.koin.configModule
import me.madhead.tyzenhaus.launcher.fly.koin.dbModule
import me.madhead.tyzenhaus.launcher.fly.koin.jsonModule
import me.madhead.tyzenhaus.launcher.fly.koin.metricsModule
import me.madhead.tyzenhaus.launcher.fly.koin.pipelineModule
import me.madhead.tyzenhaus.launcher.fly.koin.serviceModule
import me.madhead.tyzenhaus.launcher.fly.koin.telegramModule
import me.madhead.tyzenhaus.launcher.fly.routes.metrics
import me.madhead.tyzenhaus.launcher.fly.routes.miniApp
import me.madhead.tyzenhaus.launcher.fly.routes.miniAppAPI
import me.madhead.tyzenhaus.launcher.fly.routes.webhook
import me.madhead.tyzenhaus.launcher.fly.security.APITokenPrincipal
import me.madhead.tyzenhaus.repository.APITokenRepository
import org.koin.ktor.ext.get
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin

/**
Expand All @@ -32,18 +42,38 @@ fun Application.tyzenhaus() {
install(Koin) {
modules(
configModule(environment.config),
dbModule,
serviceModule,
pipelineModule,
metricsModule,
telegramModule,
jsonModule,
pipelineModule,
dbModule,
)
}

install(ContentNegotiation) {
json(this@tyzenhaus.get())
}

install(MicrometerMetrics) {
this.registry = this@tyzenhaus.get<PrometheusMeterRegistry>()
}

install(Authentication) {
bearer("api") {
authenticate { credential ->
val token = try {
UUID.fromString(credential.token)
} catch (_: Exception) {
return@authenticate null
}
val tokenRepository by inject<APITokenRepository>()

tokenRepository.get(token)?.takeIf { it.validUntil > Instant.now() }?.let { APITokenPrincipal(it.groupId, it.scope) }
}
}
}

routing {
webhook()
metrics()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.server.config.ApplicationConfig
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.localPort
import io.micrometer.prometheus.PrometheusMeterRegistry
import org.koin.ktor.ext.inject

Expand All @@ -14,10 +15,9 @@ import org.koin.ktor.ext.inject
fun Route.metrics() {
val registry by inject<PrometheusMeterRegistry>()
val config by inject<ApplicationConfig>()
val managementPort = config.property("deployment.managementPort").getString().toInt()

get("metrics") {
if (call.request.local.port == managementPort) {
localPort(config.property("deployment.managementPort").getString().toInt()) {
get("metrics") {
this.call.respond(registry.scrape())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.ktor.server.config.ApplicationConfig
import io.ktor.server.http.content.react
import io.ktor.server.http.content.singlePageApplication
import io.ktor.server.routing.Route
import io.ktor.server.routing.localPort
import org.koin.ktor.ext.inject

/**
Expand All @@ -13,8 +14,10 @@ fun Route.miniApp() {
val config by inject<ApplicationConfig>()
val miniAppPath = config.property("telegram.miniApp.path").getString()

singlePageApplication {
applicationRoute = "app"
react(miniAppPath)
localPort(config.property("deployment.port").getString().toInt()) {
singlePageApplication {
applicationRoute = "app"
react(miniAppPath)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
package me.madhead.tyzenhaus.launcher.fly.routes

import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.principal
import io.ktor.server.config.ApplicationConfig
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.localPort
import io.ktor.server.routing.route
import java.time.LocalDateTime
import me.madhead.tyzenhaus.core.service.GroupMembersService
import me.madhead.tyzenhaus.launcher.fly.security.APITokenPrincipal
import org.koin.ktor.ext.inject

/**
* Routes for [Telegram Mini App](https://core.telegram.org/bots/webapps) API.
*/
fun Route.miniAppAPI() {
route("/app/api") {
get("test") {
call.respondText("""{"result":"ok", "date":"${LocalDateTime.now()}"}""", ContentType.Application.Json)
val config by inject<ApplicationConfig>()
val groupMembersService by inject<GroupMembersService>()

localPort(config.property("deployment.port").getString().toInt()) {
route("/app/api") {
authenticate("api") {
get("/group/members") {
val principal = call.principal<APITokenPrincipal>()!!
val members = groupMembersService.groupMembers(principal.groupId)

if (members != null) {
call.respond(members)
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.ktor.server.config.ApplicationConfig
import io.ktor.server.request.receiveText
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.localPort
import io.ktor.server.routing.post
import kotlinx.serialization.json.Json
import me.madhead.tyzenhaus.core.telegram.updates.UpdateProcessingPipeline
Expand All @@ -19,29 +20,26 @@ import org.koin.ktor.ext.inject
fun Route.webhook() {
val logger = LogManager.getLogger("me.madhead.tyzenhaus.launcher.fly.routes.Webhook")
val config by inject<ApplicationConfig>()
val port = config.property("deployment.port").getString().toInt()
val json by inject<Json>()
val pipeline by inject<UpdateProcessingPipeline>()

post(config.property("telegram.token").getString()) {
if (call.request.local.port != port) {
return@post
}
localPort(config.property("deployment.port").getString().toInt()) {
post(config.property("telegram.token").getString()) {
try {
val payload = call.receiveText()

try {
val payload = call.receiveText()
logger.debug("Request payload: {}", payload)

logger.debug("Request payload: {}", payload)
val update = json.decodeFromString(UpdateDeserializationStrategy, payload)

val update = json.decodeFromString(UpdateDeserializationStrategy, payload)
logger.info("Update object: {}", update)

logger.info("Update object: {}", update)
pipeline.process(update)
} catch (ignored: Exception) {
logger.error("Failed to handle the request", ignored)
}

pipeline.process(update)
} catch (ignored: Exception) {
logger.error("Failed to handle the request", ignored)
call.respond(HttpStatusCode.OK)
}

call.respond(HttpStatusCode.OK)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package me.madhead.tyzenhaus.launcher.fly.security

import io.ktor.server.auth.Principal
import me.madhead.tyzenhaus.entity.api.token.Scope

data class APITokenPrincipal(
val groupId: Long,
val scope: Scope,
) : Principal
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package me.madhead.tyzenhaus.launcher.fly.security

import io.ktor.http.HttpStatusCode
import io.ktor.server.application.createRouteScopedPlugin
import io.ktor.server.auth.AuthenticationChecked
import io.ktor.server.auth.principal
import io.ktor.server.response.respond
import me.madhead.tyzenhaus.entity.api.token.Scope

val AuthorizationPlugin = createRouteScopedPlugin(
name = "AuthorizationPlugin",
createConfiguration = ::AuthorizationPluginConfiguration,
) {
pluginConfig.apply {
on(AuthenticationChecked) { call ->
val principal = call.principal<APITokenPrincipal>()

if (principal?.scope != pluginConfig.scope) {
call.respond(HttpStatusCode.Forbidden)
}
}
}
}

data class AuthorizationPluginConfiguration(
var scope: Scope? = null
)

0 comments on commit 4524868

Please sign in to comment.