diff --git a/apps/giga-wear/backend/.gitignore b/apps/giga-wear/backend/.gitignore new file mode 100644 index 0000000..dd86a94 --- /dev/null +++ b/apps/giga-wear/backend/.gitignore @@ -0,0 +1,4 @@ +/build +/videobox +/bin +back.env \ No newline at end of file diff --git a/apps/giga-wear/backend/build.gradle.kts b/apps/giga-wear/backend/build.gradle.kts new file mode 100644 index 0000000..d5f48b6 --- /dev/null +++ b/apps/giga-wear/backend/build.gradle.kts @@ -0,0 +1,101 @@ +plugins { + alias(libs.plugins.convention.backend.app) + alias(libs.plugins.ktor) + alias(libs.plugins.sqldelight) + alias(libs.plugins.serialization) + alias(libs.plugins.convention.koin) +} + +sqldelight { + databases { + create("AppDatabase") { + packageName.set("ru.shadowsparky.gigawear.backend") + dialect("app.cash.sqldelight:postgresql-dialect:2.0.2") + } + } +} + +dependencies { + implementation(project(":apps:giga-wear:common")) + implementation(project(":libs:backend-base")) + implementation(project(":libs:base")) + implementation(project(":libs:http-client")) + implementation(project(":feature:updater:updater-common")) + implementation(project(":feature:chat:chat-common")) + implementation(project(":feature:chat:chat-backend")) + + implementation(libs.ktor.server.core.jvm) + implementation(libs.ktor.server.host.common.jvm) + implementation(libs.ktor.server.status.pages.jvm) + implementation(libs.ktor.server.content.negotiation.jvm) + implementation(libs.ktor.serialization.kotlinx.json.jvm) + implementation(libs.ktor.server.netty.jvm) + implementation(libs.ktor.server.auth) + implementation(libs.ktor.server.auth.jwt) + implementation(libs.ktor.server.websockets.jvm) + implementation(libs.ktor.server.cors) + + implementation(libs.androidx.datastore.preferences.core) + implementation(libs.jdbc.driver) + implementation(libs.postgresql) + implementation(libs.coroutines.extensions) +} + +java { + sourceCompatibility = JavaVersion.toVersion(libs.versions.proj.server.java.get()) + targetCompatibility = JavaVersion.toVersion(libs.versions.proj.server.java.get()) +} + +group = "ru.shadowsparky.gigawear" +version = "1.0" + +val mainClassName = "ru.shadowsparky.gigawear.backend.MainKt" + +application { + mainClass.set(mainClassName) + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +ktor { + fatJar { + archiveFileName.set("server-fat.jar") + } +} + +tasks { + named("build") { + dependsOn("buildFatJar") + } + register("deployRemote") { + dependsOn("buildFatJar") + commandLine( + getCommandLine( + "docker context use remote", + "docker build . -t gigawear-server", + "docker context use default" + ) + ) + notCompatibleWithConfigurationCache("This task uses Exec which is not compatible with configuration cache") + } + register("deployLocal") { + dependsOn("buildFatJar") + commandLine( + getCommandLine( + "docker compose down", + "docker compose up -d --build --force-recreate" + ) + ) + notCompatibleWithConfigurationCache("This task uses Exec which is not compatible with configuration cache") + } +} + +private fun getCommandLine(vararg commands: String): List { + val osName = System.getProperty("os.name").lowercase() + return if (osName.contains("win")) { + listOf("cmd", "/c") + commands.joinToString(" && ") + } else { + listOf("sh", "-c") + commands.joinToString(" && ") + } +} diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/BackendModule.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/BackendModule.kt new file mode 100644 index 0000000..a2c75ad --- /dev/null +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/BackendModule.kt @@ -0,0 +1,23 @@ +package ru.shadowsparky.gigawear.backend +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import ru.shadowsparky.backend.data.EnvFetcher +import ru.shadowsparky.backend.domain.JwtInfo + +@Module +@ComponentScan +@Configuration +class BackendModule { + + @Single + fun provideJwtInfo(envFetcher: EnvFetcher): JwtInfo { + return JwtInfo( + envFetcher.get("GIGA_WEAR_SECRET", "secret"), + "https://shadowsparky.ru/", + "https://wear.shadowsparky.ru/", + "wear" + ) + } +} diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt new file mode 100644 index 0000000..3c7b088 --- /dev/null +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt @@ -0,0 +1,27 @@ +package ru.shadowsparky.gigawear.backend + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.response.respond +import io.ktor.server.routing.get +import io.ktor.server.routing.routing +import org.koin.core.annotation.KoinApplication +import org.koin.core.component.KoinComponent +import org.koin.plugin.module.dsl.startKoin + +@KoinApplication +object GigaWearApp + +fun main(args: Array) { + startKoin() + io.ktor.server.netty.EngineMain.main(args) +} + +fun Application.module() { + val koin = object : KoinComponent {} + this.routing { + get("health") { + this.call.respond(HttpStatusCode.OK) + } + } +} diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/ClientIdProviderImpl.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/ClientIdProviderImpl.kt new file mode 100644 index 0000000..2616999 --- /dev/null +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/ClientIdProviderImpl.kt @@ -0,0 +1,11 @@ +package ru.shadowsparky.gigawear.backend.data + +import org.koin.core.annotation.Factory +import ru.shadowsparky.chat.backend.domain.ChatService + +@Factory +class ClientIdProviderImpl : ChatService.ClientIdProvider { + override suspend fun provide(userId: Long): String { + return "gigawear-$userId" + } +} diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/LoginVerifierImpl.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/LoginVerifierImpl.kt new file mode 100644 index 0000000..c7a3715 --- /dev/null +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/LoginVerifierImpl.kt @@ -0,0 +1,10 @@ +package ru.shadowsparky.gigawear.backend.data + +import org.koin.core.annotation.Factory +import ru.shadowsparky.backend.domain.LoginVerifier + +@Factory +class LoginVerifierImpl : LoginVerifier { + override suspend fun verify(login: String) { + } +} diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/MessageStorageImpl.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/MessageStorageImpl.kt new file mode 100644 index 0000000..37ca3d2 --- /dev/null +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/MessageStorageImpl.kt @@ -0,0 +1,48 @@ +package ru.shadowsparky.gigawear.backend.data + +import org.koin.core.annotation.Single +import ru.shadowsparky.chat.backend.domain.MessageStorage +import ru.shadowsparky.chat.domain.Message + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +@Single +class MessageStorageImpl : MessageStorage { + private val storage = ConcurrentHashMap>() + private val idGenerator = AtomicLong(1) + + override suspend fun query( + userId: Long, + afterId: Long?, + limit: Int + ): List { + val userMessages = storage[userId] ?: return emptyList() + synchronized(userMessages) { + val filteredList = if (afterId != null) { + val index = userMessages.indexOfFirst { it.id == afterId } + if (index != -1 && index + 1 < userMessages.size) { + userMessages.subList(index + 1, userMessages.size) + } else { + emptyList() + } + } else { + userMessages + } + return filteredList.take(limit).toList() + } + } + + override suspend fun insert( + userId: Long, + message: Message + ): Long { + val userMessages = storage.computeIfAbsent(userId) { ArrayList() } + val newId = idGenerator.getAndIncrement() + val messageToInsert = message.copy(id = newId) + synchronized(userMessages) { + userMessages.add(messageToInsert) + } + return newId + } +} diff --git a/apps/giga-wear/backend/src/main/resources/application.conf b/apps/giga-wear/backend/src/main/resources/application.conf new file mode 100644 index 0000000..f1e9e53 --- /dev/null +++ b/apps/giga-wear/backend/src/main/resources/application.conf @@ -0,0 +1,8 @@ +ktor { + deployment { + port = 8181 + } + application { + modules = [ ru.shadowsparky.gigawear.backend.MainKt.module ] + } +} diff --git a/apps/giga-wear/backend/src/main/resources/logback.xml b/apps/giga-wear/backend/src/main/resources/logback.xml new file mode 100644 index 0000000..678bbd4 --- /dev/null +++ b/apps/giga-wear/backend/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/apps/giga-wear/client/build.gradle.kts b/apps/giga-wear/client/build.gradle.kts index 911969d..fb13cca 100644 --- a/apps/giga-wear/client/build.gradle.kts +++ b/apps/giga-wear/client/build.gradle.kts @@ -25,6 +25,7 @@ android { dependencies { implementation(platform(libs.androidx.compose.bom)) + implementation(project(":apps:giga-wear:common")) implementation(project(":libs:ui")) implementation(project(":libs:http-client")) implementation(project(":libs:base")) diff --git a/apps/giga-wear/common/.gitignore b/apps/giga-wear/common/.gitignore new file mode 100644 index 0000000..c795b05 --- /dev/null +++ b/apps/giga-wear/common/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/apps/giga-wear/common/build.gradle.kts b/apps/giga-wear/common/build.gradle.kts new file mode 100644 index 0000000..cd77565 --- /dev/null +++ b/apps/giga-wear/common/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.convention.kmp) + alias(libs.plugins.convention.android.lib) + alias(libs.plugins.convention.ktor.client) + alias(libs.plugins.convention.compose) + alias(libs.plugins.convention.serialization) + alias(libs.plugins.convention.koin) +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(project(":libs:base")) + implementation(project(":libs:http-client")) + } + } + android { + namespace = "ru.shadowsparky.gigawear.common" + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 4dfeb47..289e7dc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,8 +38,9 @@ include(":apps:experimentarium:androidApp") include(":apps:experimentarium:shared") include(":apps:experimentarium:web") -include(":apps:giga-wear") include(":apps:giga-wear:client") +include(":apps:giga-wear:common") +include(":apps:giga-wear:backend") include(":libs:backend-base") include(":libs:base")