From aa16be77220048437052c688c8e2b4a9a2036196 Mon Sep 17 00:00:00 2001 From: Eneko Nieto Date: Wed, 9 Mar 2022 00:16:30 +0100 Subject: [PATCH] Working Exposed and yaml config --- .gitignore | 3 +- build.gradle.kts | 5 ++ gradle.properties | 3 +- src/main/kotlin/eu/fosil/okupamicoche/Main.kt | 20 ++++--- .../fosil/okupamicoche/cli/CommandParser.kt | 49 ++++++++---------- .../eu/fosil/okupamicoche/config/Config.kt | 8 +++ .../fosil/okupamicoche/config/ConfigReader.kt | 42 +++++++++++++++ .../eu/fosil/okupamicoche/createAppService.kt | 16 ++++-- src/main/resources/config.yaml | 4 ++ test.mv.db | Bin 77824 -> 0 bytes 10 files changed, 110 insertions(+), 40 deletions(-) create mode 100644 src/main/kotlin/eu/fosil/okupamicoche/config/Config.kt create mode 100644 src/main/kotlin/eu/fosil/okupamicoche/config/ConfigReader.kt create mode 100644 src/main/resources/config.yaml delete mode 100644 test.mv.db diff --git a/.gitignore b/.gitignore index 9aafcc9..3250943 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,7 @@ build/ !**/src/test/**/build/ # App service DB -matrix.mv.db -matrix.trace.db +okupamicoche.mv.db # Synapse data docker/synapse/data diff --git a/build.gradle.kts b/build.gradle.kts index 137e239..3e595ff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,11 @@ dependencies { // H2 database implementation("com.h2database:h2:2.1.210") + // Jackson (YAML parser) + val jacksonVersion: String by project + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion") + // Logger implementation("io.github.microutils:kotlin-logging-jvm:2.1.21") implementation("ch.qos.logback:logback-classic:1.2.10") diff --git a/gradle.properties b/gradle.properties index 673d6f8..acf4df6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ kotlin.code.style=official exposedVersion=0.37.3 -ktorVersion=1.6.7 \ No newline at end of file +ktorVersion=1.6.7 +jacksonVersion=2.13.1 \ No newline at end of file diff --git a/src/main/kotlin/eu/fosil/okupamicoche/Main.kt b/src/main/kotlin/eu/fosil/okupamicoche/Main.kt index 2086996..efba08b 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/Main.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/Main.kt @@ -1,5 +1,6 @@ package eu.fosil.okupamicoche +import eu.fosil.okupamicoche.config.ConfigReader import io.ktor.http.* import io.ktor.server.engine.* import io.ktor.server.netty.* @@ -18,20 +19,27 @@ private val logger = KotlinLogging.logger {} suspend fun main() { + val config = ConfigReader.load() + + if (config === null) { + logger.error { "Unable to load config" } + return + } + val matrixApiClient = MatrixApiClient( - baseUrl = Url("http://okupamicoche-synapse:8008/"), - ).apply { accessToken.value = "30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46" } + baseUrl = Url(config.homeServerUrl), + ).apply { accessToken.value = config.asToken } coroutineScope { launch { val roomAliasRes = - matrixApiClient.rooms.getRoomAlias(RoomAliasId("#viajes:okupamicoche-synapse")).getOrNull() + matrixApiClient.rooms.getRoomAlias(RoomAliasId(config.mainRoomId)).getOrNull() if (roomAliasRes == null) { - logger.info("Creating #viajes:okupamicoche-synapse public room") + logger.info("Creating ${config.mainRoomId} public room") matrixApiClient.rooms.createRoom( visibility = Visibility.PUBLIC, - roomAliasId = RoomAliasId("#viajes:okupamicoche-synapse") + roomAliasId = RoomAliasId(config.mainRoomId) ) } else { val mainRoomId = roomAliasRes.roomId @@ -48,7 +56,7 @@ suspend fun main() { embeddedServer(Netty, port = 8080, host = "0.0.0.0") { matrixAppserviceModule( - properties = MatrixAppserviceProperties("312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e"), + properties = MatrixAppserviceProperties(config.hsToken), appserviceService = createAppService(matrixApiClient) ) }.start(wait = true) diff --git a/src/main/kotlin/eu/fosil/okupamicoche/cli/CommandParser.kt b/src/main/kotlin/eu/fosil/okupamicoche/cli/CommandParser.kt index 16e1aae..dc9b177 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/cli/CommandParser.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/cli/CommandParser.kt @@ -1,17 +1,14 @@ package eu.fosil.okupamicoche.cli +import eu.fosil.okupamicoche.db import eu.fosil.okupamicoche.model.Travel -import eu.fosil.okupamicoche.model.Travels import io.ktor.http.* import mu.KotlinLogging import net.folivo.trixnity.client.api.MatrixApiClient import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.SchemaUtils +import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent import org.jetbrains.exposed.sql.transactions.transaction -import java.text.DateFormat -import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter @@ -26,7 +23,7 @@ object CommandParser { fun isCommand(body: String) = body.matches("![a-zA-Z]+( .*)?".toRegex()) - fun parse( + suspend fun parse( user: UserId, room: RoomId, body: String @@ -37,21 +34,25 @@ object CommandParser { handleCommand(user, room, command, args) } - private fun handleCommand( + private suspend fun handleCommand( user: UserId, room: RoomId, command: String, args: List ) { logger.info("command=$command args=$args") - when (command) { - "create" -> handleCreate(user, room, args) + try { + when (command) { + "create" -> handleCreate(user, room, args) + } + } catch (e: Exception) { + logger.error { "Exception captured" } } } - private fun handleCreate( - user: UserId, - room: RoomId, + private suspend fun handleCreate( + userId: UserId, + roomId: RoomId, args: List ) { val origin = args[0] @@ -63,30 +64,26 @@ object CommandParser { val places = args[4].toIntOrNull() ?: 0 val description = args[5] - Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver") - - transaction { - SchemaUtils.create(Travels) - } - - transaction { + transaction(db) { Travel.new { - this.roomId = room.full - this.driver = user.full + this.roomId = roomId.full + this.driver = userId.full this.origin = origin this.destination = destination this.time = unixTime this.places = places this.description = description } - } - - transaction { - Travel.all().forEach { travel -> - logger.info("travel id=${travel.id} origin=${travel.origin} dest=${travel.destination}") + Travel.all().forEachIndexed { index, travel -> + logger.info("#$index: travel id=${travel.id} origin=${travel.origin} dest=${travel.destination} travel=$travel") } } + val displayName = matrixApiClient.users.getDisplayName(userId).getOrNull() ?: userId.full + matrixApiClient.rooms.sendMessageEvent( + roomId, + RoomMessageEventContent.TextMessageEventContent("$displayName created a new travel! $origin-$destination on $date $time with $places free places.") + ) } diff --git a/src/main/kotlin/eu/fosil/okupamicoche/config/Config.kt b/src/main/kotlin/eu/fosil/okupamicoche/config/Config.kt new file mode 100644 index 0000000..7333d21 --- /dev/null +++ b/src/main/kotlin/eu/fosil/okupamicoche/config/Config.kt @@ -0,0 +1,8 @@ +package eu.fosil.okupamicoche.config + +data class Config( + val mainRoomId: String, + val homeServerUrl: String, + val asToken: String, + val hsToken: String +) \ No newline at end of file diff --git a/src/main/kotlin/eu/fosil/okupamicoche/config/ConfigReader.kt b/src/main/kotlin/eu/fosil/okupamicoche/config/ConfigReader.kt new file mode 100644 index 0000000..6c3ef94 --- /dev/null +++ b/src/main/kotlin/eu/fosil/okupamicoche/config/ConfigReader.kt @@ -0,0 +1,42 @@ +package eu.fosil.okupamicoche.config + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException +import mu.KotlinLogging +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.Path + +private val logger = KotlinLogging.logger {} + +object ConfigReader { + + var config: Config? = null + + fun load(): Config? { + val path = this.javaClass.getResource("/config.yaml")?.path + val path2 = Paths.get("/", "config.yaml") + val mapper = ObjectMapper(YAMLFactory()) + mapper.registerModule(KotlinModule()) + + if (path == null) { + logger.error { "Error getting config path" } + return null + } + + config = try { + Files.newBufferedReader(Path(path)).use { + mapper.readValue(it, Config::class.java) + } + } catch (exception: MissingKotlinParameterException) { + logger.error("Could not read YAML file!") + logger.error(exception.message) + null + } + + return config + } + +} \ No newline at end of file diff --git a/src/main/kotlin/eu/fosil/okupamicoche/createAppService.kt b/src/main/kotlin/eu/fosil/okupamicoche/createAppService.kt index 11c885a..5112f85 100644 --- a/src/main/kotlin/eu/fosil/okupamicoche/createAppService.kt +++ b/src/main/kotlin/eu/fosil/okupamicoche/createAppService.kt @@ -1,6 +1,7 @@ package eu.fosil.okupamicoche import eu.fosil.okupamicoche.cli.CommandParser +import eu.fosil.okupamicoche.model.Travels import eu.fosil.okupamicoche.services.EventTnxService import eu.fosil.okupamicoche.services.RoomService import eu.fosil.okupamicoche.services.UserService @@ -11,11 +12,21 @@ import net.folivo.trixnity.client.api.MatrixApiClient import net.folivo.trixnity.core.model.events.Event import net.folivo.trixnity.core.model.events.m.room.CreateEventContent import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction + +val db = Database.connect("jdbc:h2:./okupamicoche", driver = "org.h2.Driver") private val logger = KotlinLogging.logger {} fun createAppService(matrixApiClient: MatrixApiClient): AppserviceService { + transaction(db) { + SchemaUtils.create(Travels) + logger.info { "Travels table created" } + } + return DefaultAppserviceService( EventTnxService(), UserService(matrixApiClient), RoomService(matrixApiClient) ).apply { @@ -40,11 +51,6 @@ fun createAppService(matrixApiClient: MatrixApiClient): AppserviceService { roomId, body ) -// matrixApiClient.rooms.joinRoom(roomId) -// matrixApiClient.rooms.sendMessageEvent( -// roomId, -// RoomMessageEventContent.TextMessageEventContent("Viaje creado!") -// ) } } } diff --git a/src/main/resources/config.yaml b/src/main/resources/config.yaml new file mode 100644 index 0000000..f85dbfe --- /dev/null +++ b/src/main/resources/config.yaml @@ -0,0 +1,4 @@ +mainRoomId: "#viajes:okupamicoche-synapse" +homeServerUrl: "http://okupamicoche-synapse:8008/" +asToken: "30c05ae90a248a4188e620216fa72e349803310ec83e2a77b34fe90be6081f46" +hsToken: "312df522183efd404ec1cd22d2ffa4bbc76a8c1ccf541dd692eef281356bb74e" diff --git a/test.mv.db b/test.mv.db deleted file mode 100644 index e46fcdf831fc01bb3b587b7b10030e48612defc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77824 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