Skip to content

Commit d302bd8

Browse files
authored
Merge pull request #921 from DimensionDev/feature/server
add backend server
2 parents d17c718 + 9edada5 commit d302bd8

File tree

26 files changed

+701
-58
lines changed

26 files changed

+701
-58
lines changed

.github/workflows/android.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
env:
5252
BUILD_NUMBER: ${{github.run_number}}
5353
BUILD_VERSION: ${{github.ref_name}}
54-
run: ./gradlew assembleRelease bundleRelease check lint --stacktrace
54+
run: ./gradlew :app:assembleRelease :app:bundleRelease :app:check :app:lint --stacktrace
5555

5656
# Archive android artifacts
5757
- name: Archive android artifacts Product

.github/workflows/linux.yml

Lines changed: 0 additions & 57 deletions
This file was deleted.

.github/workflows/server.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Server CI
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- release
8+
- develop
9+
tags:
10+
- '**'
11+
paths-ignore:
12+
- '**.md'
13+
- 'docs/**/*.yml'
14+
pull_request:
15+
branches:
16+
- master
17+
- release
18+
- develop
19+
20+
jobs:
21+
build:
22+
runs-on: [ubuntu-latest]
23+
timeout-minutes: 30
24+
25+
steps:
26+
- uses: actions/checkout@v3
27+
with:
28+
submodules: true
29+
30+
- name: Set up JDK
31+
uses: actions/setup-java@v3
32+
with:
33+
distribution: 'zulu'
34+
java-version: 21
35+
36+
- name: Build
37+
env:
38+
BUILD_NUMBER: ${{github.run_number}}
39+
BUILD_VERSION: ${{github.ref_name}}
40+
run: ./gradlew :server:build --stacktrace
41+
42+
- name: Test
43+
env:
44+
BUILD_NUMBER: ${{github.run_number}}
45+
BUILD_VERSION: ${{github.ref_name}}
46+
run: ./gradlew :server:check --stacktrace

gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ android.nonTransitiveRClass=true
66
#kotlin.experimental.tryK2=true
77
android.lint.useK2Uast=true
88
#kotlin.native.cacheKind=none ios faster build?
9+
# https://github.com/ajalt/clikt/discussions/571
10+
kotlin.native.cacheKind.linuxX64=none
911

1012
#MPP
1113
kotlin.mpp.stability.nowarn=true

gradle/libs.versions.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[versions]
2+
clikt = "5.0.3"
23
collection = "1.5.0"
34
compileSdk = "35"
45
lifecycleViewmodelCompose = "2.8.4"
@@ -14,6 +15,7 @@ lifecycle-runtime-ktx = "2.9.0"
1415
activity-compose = "1.10.1"
1516
compose-bom = "2025.05.00"
1617
ksp = "2.1.20-1.0.31"
18+
openaiClient = "4.0.1"
1719
paging = "3.3.6"
1820
navigation = "2.9.0"
1921
kotlinx-datetime = "0.6.2"
@@ -49,10 +51,12 @@ materialKolor = "2.1.1"
4951
room = "2.7.1"
5052
sqlite = "2.5.1"
5153
compose-multiplatform = "1.8.0"
54+
logback = "1.4.14"
5255

5356
[libraries]
5457
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
5558
bluesky = { module = "sh.christian.ozone:bluesky", version.ref = "bluesky" }
59+
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
5660
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
5761
junit = { group = "junit", name = "junit", version.ref = "junit" }
5862
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
@@ -63,6 +67,7 @@ lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-ru
6367
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
6468
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
6569
lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
70+
openai-client = { module = "com.aallam.openai:openai-client", version.ref = "openaiClient" }
6671
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
6772
stately-iso-collections = { module = "co.touchlab:stately-iso-collections", version.ref = "stately" }
6873
stately-isolate = { module = "co.touchlab:stately-isolate", version.ref = "stately" }
@@ -141,6 +146,7 @@ ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version
141146
ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" }
142147
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
143148
ktor-client-curl = { group = "io.ktor", name = "ktor-client-curl", version.ref = "ktor" }
149+
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
144150

145151
twitter-parser = { group = "moe.tlaster", name = "twitter-parser", version.ref = "twitter-parser" }
146152
swiper = { group = "com.github.tlaster", name = "swiper", version = "0.7.1" }
@@ -195,6 +201,19 @@ jSystemThemeDetector = { module = "com.github.Dansoftowner:jSystemThemeDetector"
195201
jetbrains-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.8.0-alpha10" }
196202
krypto = { module = "com.soywiz.korlibs.krypto:krypto", version = "4.0.10" }
197203

204+
### Server ###
205+
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
206+
ktor-resources = { module = "io.ktor:ktor-resources", version.ref = "ktor" }
207+
ktor-server-resources = { module = "io.ktor:ktor-server-resources", version.ref = "ktor" }
208+
ktor-client-resources = { module = "io.ktor:ktor-client-resources", version.ref = "ktor" }
209+
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
210+
ktor-server-hsts = { module = "io.ktor:ktor-server-hsts", version.ref = "ktor" }
211+
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
212+
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" }
213+
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
214+
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
215+
216+
198217
[bundles]
199218
compose = ["ui", "ui-util", "ui-graphics", "ui-tooling", "ui-tooling-preview", "material3", "material3WindowSizeClass", "material3-adaptive-navigation-suite", "material3-adaptive", "material3-adaptive-navigation", "material3-adaptive-layout"]
200219
navigation = ["navigation-compose"]
@@ -209,6 +228,9 @@ media3 = ["media3-exoplayer", "media3-ui", "media3-hls", "media3-session", "medi
209228
firebase = ["firebase-analytics-ktx", "firebase-crashlytics-ktx"]
210229
ktorfit = ["ktorfit-lib", "ktorfit-converters-response", "ktorfit-converters-flow", "ktorfit-converters-call"]
211230

231+
### Server ###
232+
ktor-server = ["ktor-server-core", "ktor-server-resources", "ktor-server-content-negotiation", "ktor-serialization-kotlinx-json", "ktor-server-hsts", "ktor-server-cio", "ktor-server-config-yaml"]
233+
212234
[plugins]
213235
android-application = { id = "com.android.application", version.ref = "agp" }
214236
android-library = { id = "com.android.library", version.ref = "agp" }

server/build.gradle.kts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
plugins {
2+
alias(libs.plugins.kotlin.multiplatform)
3+
alias(libs.plugins.kotlin.serialization)
4+
}
5+
6+
kotlin {
7+
explicitApi()
8+
applyDefaultHierarchyTemplate()
9+
listOf(
10+
macosX64(),
11+
macosArm64(),
12+
linuxX64(),
13+
mingwX64(),
14+
).forEach { nativeTarget ->
15+
nativeTarget.apply {
16+
binaries {
17+
executable {
18+
entryPoint = "dev.dimension.flare.server.main"
19+
}
20+
}
21+
}
22+
}
23+
sourceSets {
24+
val commonMain by getting {
25+
dependencies {
26+
implementation(libs.clikt)
27+
implementation(libs.bundles.ktor.server)
28+
implementation(libs.ktor.client.cio)
29+
implementation(libs.ktor.client.content.negotiation)
30+
implementation(libs.ktor.client.logging)
31+
implementation(libs.logback.classic)
32+
implementation(projects.shared.api)
33+
implementation(libs.kotlinx.serialization.json)
34+
implementation(libs.openai.client)
35+
}
36+
37+
}
38+
val commonTest by getting {
39+
dependencies {
40+
implementation(kotlin("test"))
41+
implementation(libs.ktor.server.test.host)
42+
implementation(libs.ktor.client.resources)
43+
implementation(libs.ktor.client.content.negotiation)
44+
}
45+
}
46+
}
47+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dev.dimension.flare.server
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.core.main
5+
import com.github.ajalt.clikt.parameters.options.help
6+
import com.github.ajalt.clikt.parameters.options.option
7+
import com.github.ajalt.clikt.parameters.options.required
8+
import dev.dimension.flare.server.service.ai.AIService
9+
import io.ktor.server.cio.CIO
10+
import io.ktor.server.config.yaml.YamlConfig
11+
import io.ktor.server.engine.embeddedServer
12+
13+
public fun main(args: Array<String>) {
14+
Server().main(args)
15+
}
16+
17+
internal class Server : CliktCommand() {
18+
val configPath: String by option().required().help("Path to the configuration file")
19+
20+
override fun run() {
21+
val config = YamlConfig(configPath) ?:
22+
throw IllegalStateException("Failed to load configuration")
23+
val aiService = AIService.create(config)
24+
val context = ServerContext(
25+
aiService = aiService,
26+
)
27+
embeddedServer(
28+
factory = CIO,
29+
port = 8080,
30+
host = "localhost",
31+
module = {
32+
modules(
33+
context = context,
34+
)
35+
}
36+
).start(wait = true)
37+
}
38+
}
39+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.dimension.flare.server
2+
3+
import dev.dimension.flare.server.common.JSON
4+
import dev.dimension.flare.server.routing.configureRouting
5+
import io.ktor.serialization.kotlinx.json.json
6+
import io.ktor.server.application.Application
7+
import io.ktor.server.application.install
8+
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
9+
import io.ktor.server.plugins.hsts.HSTS
10+
11+
internal fun Application.modules(
12+
context: ServerContext,
13+
) {
14+
install(ContentNegotiation) {
15+
json(JSON)
16+
}
17+
install(HSTS) {
18+
includeSubDomains = true
19+
}
20+
configureRouting(context)
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package dev.dimension.flare.server
2+
3+
import dev.dimension.flare.server.service.TLDRService
4+
import dev.dimension.flare.server.service.TranslatorService
5+
import dev.dimension.flare.server.service.ai.AIService
6+
7+
internal data class ServerContext private constructor(
8+
val aiService: AIService,
9+
val translatorService: TranslatorService,
10+
val tldrService: TLDRService,
11+
) {
12+
constructor(aiService: AIService) : this(
13+
aiService = aiService,
14+
translatorService = TranslatorService(aiService),
15+
tldrService = TLDRService(aiService),
16+
)
17+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dev.dimension.flare.server.common
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.HttpClientConfig
5+
import io.ktor.client.plugins.HttpTimeout
6+
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
7+
import io.ktor.client.plugins.logging.LogLevel
8+
import io.ktor.client.plugins.logging.Logger
9+
import io.ktor.client.plugins.logging.Logging
10+
import io.ktor.serialization.kotlinx.json.json
11+
import kotlin.time.Duration.Companion.minutes
12+
13+
14+
internal fun ktorClient(
15+
config: HttpClientConfig<*>.() -> Unit = {
16+
install(ContentNegotiation) {
17+
json(JSON)
18+
}
19+
},
20+
) = HttpClient {
21+
config.invoke(this)
22+
install(Logging) {
23+
logger = object : Logger {
24+
override fun log(message: String) {
25+
Log.d("KtorClient", message)
26+
}
27+
}
28+
level = LogLevel.ALL
29+
}
30+
install(HttpTimeout) {
31+
connectTimeoutMillis = 2.minutes.inWholeMilliseconds
32+
requestTimeoutMillis = 2.minutes.inWholeMilliseconds
33+
socketTimeoutMillis = 2.minutes.inWholeMilliseconds
34+
}
35+
}

0 commit comments

Comments
 (0)