package com.jovan.assistant.data import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.database.DataSnapshot import com.google.firebase.database.DatabaseError import com.google.firebase.database.FirebaseDatabase import com.google.firebase.database.ValueEventListener import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.tasks.await /** * Database layout (Realtime Database): * users/{uid}/config/wakeWord * users/{uid}/config/ttsReply * users/{uid}/device/status "online" | "offline" * users/{uid}/logs/{pushId} LogEntry */ object FirebaseRepo { private val auth get() = FirebaseAuth.getInstance() private val root get() = FirebaseDatabase.getInstance().reference val uid: String? get() = auth.currentUser?.uid fun authState(): Flow = callbackFlow { val l = FirebaseAuth.AuthStateListener { trySend(it.currentUser) } auth.addAuthStateListener(l) awaitClose { auth.removeAuthStateListener(l) } } suspend fun login(email: String, password: String) { auth.signInWithEmailAndPassword(email.trim(), password).await() log("AUTH", "Signed in") } suspend fun register(email: String, password: String) { auth.createUserWithEmailAndPassword(email.trim(), password).await() val id = uid ?: return root.child("users/$id/config").setValue( mapOf("wakeWord" to Prefs.wakeWord, "ttsReply" to "Yes, tell me") ).await() log("AUTH", "Account created") } /** Re-checks the password; used before lifting a protective block. */ suspend fun reauthenticate(password: String): Boolean { val user = auth.currentUser ?: return false val email = user.email ?: return false return try { user.reauthenticate(EmailAuthProvider.getCredential(email, password)).await() true } catch (e: Exception) { false } } fun logout() { setDeviceStatus("offline") auth.signOut() } fun log(type: String, message: String, level: String = "info") { val id = uid ?: return val ref = root.child("users/$id/logs").push() ref.setValue(LogEntry(ref.key ?: "", System.currentTimeMillis(), type, message, level)) } fun logsFlow(uid: String, limit: Int = 60): Flow> = callbackFlow { val q = root.child("users/$uid/logs").orderByChild("ts").limitToLast(limit) val l = object : ValueEventListener { override fun onDataChange(s: DataSnapshot) { trySend(s.children.mapNotNull { it.getValue(LogEntry::class.java) }.sortedByDescending { it.ts }) } override fun onCancelled(e: DatabaseError) { close(e.toException()) } } q.addValueEventListener(l) awaitClose { q.removeEventListener(l) } } fun connectedFlow(): Flow = callbackFlow { val ref = FirebaseDatabase.getInstance().getReference(".info/connected") val l = object : ValueEventListener { override fun onDataChange(s: DataSnapshot) { trySend(s.getValue(Boolean::class.java) == true) } override fun onCancelled(e: DatabaseError) { trySend(false) } } ref.addValueEventListener(l) awaitClose { ref.removeEventListener(l) } } fun saveWakeWord(word: String) { Prefs.wakeWord = word val id = uid ?: return root.child("users/$id/config/wakeWord").setValue(Prefs.wakeWord) } /** Live-sync wake word edited from another device. Returns a function that removes the listener. */ fun observeWakeWord(): () -> Unit { val id = uid ?: return {} val ref = root.child("users/$id/config/wakeWord") val l = object : ValueEventListener { override fun onDataChange(s: DataSnapshot) { s.getValue(String::class.java)?.takeIf { it.isNotBlank() }?.let { Prefs.wakeWord = it } } override fun onCancelled(e: DatabaseError) {} } ref.addValueEventListener(l) return { ref.removeEventListener(l) } } fun setDeviceStatus(status: String) { val id = uid ?: return val ref = root.child("users/$id/device/status") ref.setValue(status) if (status == "online") ref.onDisconnect().setValue("offline") } }