From e26e15f98f7f62dc96b718bd0762118c15b55f32 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 13 Apr 2026 13:59:26 -0400 Subject: [PATCH] init project --- apps/android/app/build.gradle.kts | 71 ++ apps/android/app/src/main/AndroidManifest.xml | 22 + .../app/src/main/graphql/Chats.graphql | 19 + .../app/src/main/graphql/Login.graphql | 10 + .../app/src/main/graphql/SendMessage.graphql | 7 + .../main/java/com/tlm/android/MainActivity.kt | 193 +++++ .../com/tlm/android/data/OutboxRepository.kt | 23 + .../tlm/android/push/FcmRegistrationWorker.kt | 15 + .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/themes.xml | 4 + .../main/res/xml/network_security_config.xml | 8 + apps/android/build.gradle.kts | 6 + apps/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 7 + apps/android/settings.gradle.kts | 18 + apps/api/.env.example | 12 + apps/api/load/k6-graphql-smoke.js | 15 + apps/api/nest-cli.json | 8 + apps/api/package.json | 54 ++ .../20260413000000_init/migration.sql | 171 ++++ .../api/prisma/migrations/migration_lock.toml | 1 + apps/api/prisma/schema.prisma | 152 ++++ apps/api/src/app.module.ts | 96 +++ apps/api/src/auth/auth.module.ts | 18 + apps/api/src/auth/auth.service.ts | 77 ++ apps/api/src/auth/current-user.decorator.ts | 15 + apps/api/src/auth/gql-auth.guard.ts | 42 + apps/api/src/auth/jwt.strategy.ts | 27 + apps/api/src/auth/public.decorator.ts | 4 + apps/api/src/health.controller.ts | 11 + apps/api/src/main.ts | 14 + apps/api/src/messenger/messenger.resolver.ts | 412 ++++++++++ apps/api/src/messenger/messenger.service.ts | 753 ++++++++++++++++++ apps/api/src/prisma/prisma.module.ts | 9 + apps/api/src/prisma/prisma.service.ts | 16 + apps/api/src/pubsub.provider.ts | 22 + apps/api/src/signaling/signal.gateway.ts | 67 ++ apps/api/tsconfig.build.json | 4 + apps/api/tsconfig.json | 20 + apps/sample-bot/package.json | 8 + apps/sample-bot/server.mjs | 24 + apps/web/index.html | 12 + apps/web/package.json | 25 + apps/web/src/App.tsx | 360 +++++++++ apps/web/src/apollo.ts | 44 + apps/web/src/main.tsx | 13 + apps/web/src/vite-env.d.ts | 10 + apps/web/tsconfig.json | 15 + apps/web/vite.config.ts | 13 + docker-compose.yml | 33 + package.json | 25 + packages/graphql-schema/codegen.ts | 12 + packages/graphql-schema/package.json | 6 + packages/graphql-schema/schema.graphql | 250 ++++++ 54 files changed, 3280 insertions(+) create mode 100644 apps/android/app/build.gradle.kts create mode 100644 apps/android/app/src/main/AndroidManifest.xml create mode 100644 apps/android/app/src/main/graphql/Chats.graphql create mode 100644 apps/android/app/src/main/graphql/Login.graphql create mode 100644 apps/android/app/src/main/graphql/SendMessage.graphql create mode 100644 apps/android/app/src/main/java/com/tlm/android/MainActivity.kt create mode 100644 apps/android/app/src/main/java/com/tlm/android/data/OutboxRepository.kt create mode 100644 apps/android/app/src/main/java/com/tlm/android/push/FcmRegistrationWorker.kt create mode 100644 apps/android/app/src/main/res/values/strings.xml create mode 100644 apps/android/app/src/main/res/values/themes.xml create mode 100644 apps/android/app/src/main/res/xml/network_security_config.xml create mode 100644 apps/android/build.gradle.kts create mode 100644 apps/android/gradle.properties create mode 100644 apps/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 apps/android/settings.gradle.kts create mode 100644 apps/api/.env.example create mode 100644 apps/api/load/k6-graphql-smoke.js create mode 100644 apps/api/nest-cli.json create mode 100644 apps/api/package.json create mode 100644 apps/api/prisma/migrations/20260413000000_init/migration.sql create mode 100644 apps/api/prisma/migrations/migration_lock.toml create mode 100644 apps/api/prisma/schema.prisma create mode 100644 apps/api/src/app.module.ts create mode 100644 apps/api/src/auth/auth.module.ts create mode 100644 apps/api/src/auth/auth.service.ts create mode 100644 apps/api/src/auth/current-user.decorator.ts create mode 100644 apps/api/src/auth/gql-auth.guard.ts create mode 100644 apps/api/src/auth/jwt.strategy.ts create mode 100644 apps/api/src/auth/public.decorator.ts create mode 100644 apps/api/src/health.controller.ts create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/src/messenger/messenger.resolver.ts create mode 100644 apps/api/src/messenger/messenger.service.ts create mode 100644 apps/api/src/prisma/prisma.module.ts create mode 100644 apps/api/src/prisma/prisma.service.ts create mode 100644 apps/api/src/pubsub.provider.ts create mode 100644 apps/api/src/signaling/signal.gateway.ts create mode 100644 apps/api/tsconfig.build.json create mode 100644 apps/api/tsconfig.json create mode 100644 apps/sample-bot/package.json create mode 100644 apps/sample-bot/server.mjs create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/apollo.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 packages/graphql-schema/codegen.ts create mode 100644 packages/graphql-schema/package.json create mode 100644 packages/graphql-schema/schema.graphql diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts new file mode 100644 index 0000000..fe5f214 --- /dev/null +++ b/apps/android/app/build.gradle.kts @@ -0,0 +1,71 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") + id("com.apollographql.apollo3") +} + +android { + namespace = "com.tlm.android" + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + applicationId = "com.tlm.android" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.0.1" + val gqlUrl = project.findProperty("TLM_GRAPHQL_URL") as String? ?: "http://10.0.2.2:4000/graphql" + buildConfigField("String", "GRAPHQL_URL", "\"$gqlUrl\"") + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + buildFeatures { + compose = true + buildConfig = true + } + + kotlinOptions { + jvmTarget = "17" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +apollo { + service("tlm") { + packageName.set("com.tlm.graphql") + schemaFile.set(file("../../../packages/graphql-schema/schema.graphql")) + srcDir("src/main/graphql") + } +} + +dependencies { + val composeBom = platform("androidx.compose:compose-bom:2024.09.03") + implementation(composeBom) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + implementation("androidx.activity:activity-compose:1.9.3") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.work:work-runtime-ktx:2.9.1") + + implementation("com.apollographql.apollo3:apollo-runtime:3.8.5") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.android.material:material:1.12.0") +} diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2498026 --- /dev/null +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/apps/android/app/src/main/graphql/Chats.graphql b/apps/android/app/src/main/graphql/Chats.graphql new file mode 100644 index 0000000..5f6153e --- /dev/null +++ b/apps/android/app/src/main/graphql/Chats.graphql @@ -0,0 +1,19 @@ +query Chats { + chats { + __typename + id + ... on DirectThread { + peer { + id + displayName + } + } + ... on Group { + title + } + ... on Channel { + title + } + unreadCount + } +} diff --git a/apps/android/app/src/main/graphql/Login.graphql b/apps/android/app/src/main/graphql/Login.graphql new file mode 100644 index 0000000..8cc366b --- /dev/null +++ b/apps/android/app/src/main/graphql/Login.graphql @@ -0,0 +1,10 @@ +mutation Login($email: String!, $password: String!) { + login(email: $email, password: $password) { + accessToken + refreshToken + user { + id + displayName + } + } +} diff --git a/apps/android/app/src/main/graphql/SendMessage.graphql b/apps/android/app/src/main/graphql/SendMessage.graphql new file mode 100644 index 0000000..128dc2c --- /dev/null +++ b/apps/android/app/src/main/graphql/SendMessage.graphql @@ -0,0 +1,7 @@ +mutation SendMessage($chatId: ID!, $body: String) { + sendMessage(chatId: $chatId, body: $body) { + id + seq + body + } +} diff --git a/apps/android/app/src/main/java/com/tlm/android/MainActivity.kt b/apps/android/app/src/main/java/com/tlm/android/MainActivity.kt new file mode 100644 index 0000000..d4cb0ec --- /dev/null +++ b/apps/android/app/src/main/java/com/tlm/android/MainActivity.kt @@ -0,0 +1,193 @@ +package com.tlm.android + +import android.content.Context +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.exception.ApolloException +import com.tlm.android.data.OutboxRepository +import com.tlm.graphql.ChatsQuery +import com.tlm.graphql.LoginMutation +import com.tlm.graphql.SendMessageMutation +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val prefs = getSharedPreferences("tlm", Context.MODE_PRIVATE) + val ok = + OkHttpClient.Builder() + .addInterceptor { chain -> + val t = prefs.getString("accessToken", null) + val b = chain.request().newBuilder() + if (!t.isNullOrBlank()) b.addHeader("Authorization", "Bearer $t") + chain.proceed(b.build()) + } + .build() + val apollo = + ApolloClient.Builder() + .serverUrl(BuildConfig.GRAPHQL_URL) + .okHttpClient(ok) + .build() + val outbox = OutboxRepository() + + setContent { + MaterialTheme { + val scope = rememberCoroutineScope() + val email = remember { mutableStateOf("alice@example.com") } + val password = remember { mutableStateOf("password123") } + val tokenPresent = remember { mutableStateOf(!prefs.getString("accessToken", null).isNullOrBlank()) } + val chats = remember { mutableStateOf>(emptyList()) } + val activeId = remember { mutableStateOf(null) } + val draft = remember { mutableStateOf("") } + val status = remember { mutableStateOf("") } + + fun loadChats() { + scope.launch { + try { + val res = apollo.query(ChatsQuery()).execute() + if (res.data != null) { + chats.value = res.data!!.chats.filterNotNull() + } else { + status.value = res.errors?.joinToString { it.message } ?: "chats error" + } + } catch (e: ApolloException) { + status.value = e.message ?: "network" + } + } + } + + LaunchedEffect(tokenPresent.value) { + if (tokenPresent.value) loadChats() + } + + Column(Modifier.fillMaxSize().padding(12.dp)) { + Text("TLM (dev)", style = MaterialTheme.typography.headlineSmall) + if (!tokenPresent.value) { + OutlinedTextField( + value = email.value, + onValueChange = { email.value = it }, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = password.value, + onValueChange = { password.value = it }, + label = { Text("Password") }, + modifier = Modifier.fillMaxWidth(), + ) + Button( + onClick = { + scope.launch { + try { + val res = + apollo + .mutation( + LoginMutation( + email = email.value, + password = password.value, + ), + ) + .execute() + val t = res.data?.login?.accessToken + if (!t.isNullOrBlank()) { + prefs.edit().putString("accessToken", t).apply() + tokenPresent.value = true + status.value = "ok" + } else { + status.value = res.errors?.joinToString { it.message } ?: "login failed" + } + } catch (e: ApolloException) { + status.value = e.message ?: "network" + } + } + }, + ) { + Text("Login") + } + } else { + Row { + Button(onClick = { loadChats() }) { Text("Refresh chats") } + Button( + onClick = { + prefs.edit().remove("accessToken").apply() + tokenPresent.value = false + }, + ) { + Text("Logout") + } + } + LazyColumn(modifier = Modifier.weight(1f)) { + items(chats.value) { c -> + Button( + onClick = { activeId.value = c.id }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("${c.__typename} ${c.id} (${c.unreadCount} unread)") + } + } + } + OutlinedTextField( + value = draft.value, + onValueChange = { draft.value = it }, + label = { Text("Message") }, + modifier = Modifier.fillMaxWidth(), + ) + Button( + onClick = { + val id = activeId.value + if (id == null) { + status.value = "pick a chat" + return@Button + } + val body = draft.value + outbox.enqueue("send:$id:${body.length}") + scope.launch { + try { + apollo + .mutation( + SendMessageMutation( + chatId = id, + body = + if (body.isBlank()) Optional.absent() + else Optional.present(body), + ), + ) + .execute() + draft.value = "" + loadChats() + } catch (e: ApolloException) { + status.value = e.message ?: "send failed" + } + } + }, + ) { + Text("Send") + } + } + Text(status.value, modifier = Modifier.padding(top = 8.dp)) + } + } + } + } +} diff --git a/apps/android/app/src/main/java/com/tlm/android/data/OutboxRepository.kt b/apps/android/app/src/main/java/com/tlm/android/data/OutboxRepository.kt new file mode 100644 index 0000000..6f70be3 --- /dev/null +++ b/apps/android/app/src/main/java/com/tlm/android/data/OutboxRepository.kt @@ -0,0 +1,23 @@ +package com.tlm.android.data + +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * Minimal outbox for optimistic sends; drain with WorkManager in a real build. + */ +class OutboxRepository { + private val q = ConcurrentLinkedQueue() + + fun enqueue(payload: String) { + q.add(payload) + } + + fun drain(max: Int = 50): List { + val out = ArrayList() + repeat(max) { + val v = q.poll() ?: return out + out.add(v) + } + return out + } +} diff --git a/apps/android/app/src/main/java/com/tlm/android/push/FcmRegistrationWorker.kt b/apps/android/app/src/main/java/com/tlm/android/push/FcmRegistrationWorker.kt new file mode 100644 index 0000000..6b87b69 --- /dev/null +++ b/apps/android/app/src/main/java/com/tlm/android/push/FcmRegistrationWorker.kt @@ -0,0 +1,15 @@ +package com.tlm.android.push + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters + +/** + * Placeholder for FCM token registration against your GraphQL `registerDevice` mutation. + */ +class FcmRegistrationWorker( + appContext: Context, + params: WorkerParameters, +) : CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result = Result.success() +} diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..ce823c7 --- /dev/null +++ b/apps/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + TLM + diff --git a/apps/android/app/src/main/res/values/themes.xml b/apps/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1e00874 --- /dev/null +++ b/apps/android/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +