feat(android): support self-host & multi channels (#12095)
This commit is contained in:
@@ -53,14 +53,10 @@ android {
|
|||||||
}
|
}
|
||||||
flavorDimensions = ['chanel']
|
flavorDimensions = ['chanel']
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable
|
||||||
buildConfigField 'String', 'BASE_URL', '"https://app.affine.pro"'
|
beta
|
||||||
resValue 'string', 'host', '"app.affine.pro"'
|
internal
|
||||||
}
|
canary
|
||||||
canary {
|
|
||||||
buildConfigField 'String', 'BASE_URL', '"https://affine.fail"'
|
|
||||||
resValue 'string', 'host', '"affine.fail"'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package app.affine.pro
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.affine.pro.service.CookieStore
|
|
||||||
import app.affine.pro.utils.dataStore
|
|
||||||
import app.affine.pro.utils.get
|
|
||||||
import app.affine.pro.utils.logger.AffineDebugTree
|
import app.affine.pro.utils.logger.AffineDebugTree
|
||||||
import app.affine.pro.utils.logger.CrashlyticsTree
|
import app.affine.pro.utils.logger.CrashlyticsTree
|
||||||
import app.affine.pro.utils.logger.FileTree
|
import app.affine.pro.utils.logger.FileTree
|
||||||
@@ -13,11 +10,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics
|
|||||||
import com.google.firebase.crashlytics.setCustomKeys
|
import com.google.firebase.crashlytics.setCustomKeys
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@@ -39,28 +31,6 @@ class AffineApp : Application() {
|
|||||||
Firebase.crashlytics.setCustomKeys {
|
Firebase.crashlytics.setCustomKeys {
|
||||||
key("affine_version", CapacitorConfig.getAffineVersion())
|
key("affine_version", CapacitorConfig.getAffineVersion())
|
||||||
}
|
}
|
||||||
// init cookies from local
|
|
||||||
MainScope().launch(Dispatchers.IO) {
|
|
||||||
val sessionCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_SESSION)
|
|
||||||
val userIdCookieStr = applicationContext.dataStore.get(CookieStore.AFFINE_USER_ID)
|
|
||||||
if (sessionCookieStr.isEmpty() || userIdCookieStr.isEmpty()) {
|
|
||||||
Timber.i("[init] user has not signed in yet.")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
Timber.i("[init] user already signed in.")
|
|
||||||
try {
|
|
||||||
val cookies = listOf(
|
|
||||||
Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), sessionCookieStr)
|
|
||||||
?: error("Parse session cookie fail:[ cookie = $sessionCookieStr ]"),
|
|
||||||
Cookie.parse(BuildConfig.BASE_URL.toHttpUrl(), userIdCookieStr)
|
|
||||||
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
|
|
||||||
)
|
|
||||||
CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies)
|
|
||||||
FileTree.get()?.checkAndUploadOldLogs()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.w(e, "[init] load persistent cookies fail.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package app.affine.pro
|
||||||
|
|
||||||
|
import android.webkit.WebView
|
||||||
|
import app.affine.pro.service.CookieStore
|
||||||
|
import app.affine.pro.utils.dataStore
|
||||||
|
import app.affine.pro.utils.get
|
||||||
|
import app.affine.pro.utils.getCurrentServerBaseUrl
|
||||||
|
import app.affine.pro.utils.logger.FileTree
|
||||||
|
import com.getcapacitor.Bridge
|
||||||
|
import com.getcapacitor.WebViewListener
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object AuthInitializer {
|
||||||
|
|
||||||
|
fun initialize(bridge: Bridge) {
|
||||||
|
bridge.addWebViewListener(object : WebViewListener() {
|
||||||
|
override fun onPageLoaded(webView: WebView?) {
|
||||||
|
bridge.removeWebViewListener(this)
|
||||||
|
MainScope().launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val server = bridge.getCurrentServerBaseUrl().toHttpUrl()
|
||||||
|
val sessionCookieStr = AffineApp.context().dataStore
|
||||||
|
.get(server.host + CookieStore.AFFINE_SESSION)
|
||||||
|
val userIdCookieStr = AffineApp.context().dataStore
|
||||||
|
.get(server.host + CookieStore.AFFINE_USER_ID)
|
||||||
|
if (sessionCookieStr.isEmpty() || userIdCookieStr.isEmpty()) {
|
||||||
|
Timber.i("[init] user has not signed in yet.")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
Timber.i("[init] user already signed in.")
|
||||||
|
val cookies = listOf(
|
||||||
|
Cookie.parse(server, sessionCookieStr)
|
||||||
|
?: error("Parse session cookie fail:[ cookie = $sessionCookieStr ]"),
|
||||||
|
Cookie.parse(server, userIdCookieStr)
|
||||||
|
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
|
||||||
|
)
|
||||||
|
CookieStore.saveCookies(server.host, cookies)
|
||||||
|
FileTree.get()?.checkAndUploadOldLogs(server)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e, "[init] load persistent cookies fail.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ import app.affine.pro.plugin.AffineThemePlugin
|
|||||||
import app.affine.pro.plugin.AuthPlugin
|
import app.affine.pro.plugin.AuthPlugin
|
||||||
import app.affine.pro.plugin.HashCashPlugin
|
import app.affine.pro.plugin.HashCashPlugin
|
||||||
import app.affine.pro.plugin.NbStorePlugin
|
import app.affine.pro.plugin.NbStorePlugin
|
||||||
import app.affine.pro.repo.WebRepo
|
import app.affine.pro.service.GraphQLService
|
||||||
|
import app.affine.pro.service.SSEService
|
||||||
|
import app.affine.pro.service.WebService
|
||||||
import app.affine.pro.utils.dp
|
import app.affine.pro.utils.dp
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
@@ -27,7 +29,13 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AffineThemePlugi
|
|||||||
View.OnClickListener {
|
View.OnClickListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var webRepo: WebRepo
|
lateinit var webService: WebService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sseService: SSEService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var graphQLService: GraphQLService
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerPlugins(
|
registerPlugins(
|
||||||
@@ -56,6 +64,11 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AffineThemePlugi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun load() {
|
||||||
|
super.load()
|
||||||
|
AuthInitializer.initialize(bridge)
|
||||||
|
}
|
||||||
|
|
||||||
override fun present() {
|
override fun present() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
fab.show()
|
fab.show()
|
||||||
@@ -85,7 +98,9 @@ class MainActivity : BridgeActivity(), AIButtonPlugin.Callback, AffineThemePlugi
|
|||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
webRepo.init(bridge)
|
webService.update(bridge)
|
||||||
|
sseService.updateServer(bridge)
|
||||||
|
graphQLService.updateServer(bridge)
|
||||||
AIActivity.open(this@MainActivity)
|
AIActivity.open(this@MainActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package app.affine.pro.ai.chat
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.affine.pro.repo.GraphQLRepo
|
import app.affine.pro.service.GraphQLService
|
||||||
import app.affine.pro.repo.SSERepo
|
import app.affine.pro.service.SSEService
|
||||||
import app.affine.pro.repo.WebRepo
|
import app.affine.pro.service.WebService
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -18,9 +18,9 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ChatViewModel @Inject constructor(
|
class ChatViewModel @Inject constructor(
|
||||||
private val webRepo: WebRepo,
|
private val webService: WebService,
|
||||||
private val graphQLRepo: GraphQLRepo,
|
private val graphQLService: GraphQLService,
|
||||||
private val sseRepo: SSERepo,
|
private val sseService: SSEService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private lateinit var sessionId: String
|
private lateinit var sessionId: String
|
||||||
@@ -32,17 +32,17 @@ class ChatViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
sessionId = graphQLRepo.createCopilotSession(
|
sessionId = graphQLService.createCopilotSession(
|
||||||
workspaceId = webRepo.workspaceId(),
|
workspaceId = webService.workspaceId(),
|
||||||
docId = webRepo.docId(),
|
docId = webService.docId(),
|
||||||
).getOrElse {
|
).getOrElse {
|
||||||
Timber.w(it, "Create session failed")
|
Timber.w(it, "Create session failed")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Timber.i("Create session success:[ sessionId = $sessionId].")
|
Timber.i("Create session success:[ sessionId = $sessionId].")
|
||||||
val historyMessages = graphQLRepo.getCopilotHistories(
|
val historyMessages = graphQLService.getCopilotHistories(
|
||||||
workspaceId = webRepo.workspaceId(),
|
workspaceId = webService.workspaceId(),
|
||||||
docId = webRepo.docId(),
|
docId = webService.docId(),
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
).getOrDefault(emptyList()).map {
|
).getOrDefault(emptyList()).map {
|
||||||
ChatMessage.from(it)
|
ChatMessage.from(it)
|
||||||
@@ -55,12 +55,12 @@ class ChatViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun sendMessage(message: String) {
|
fun sendMessage(message: String) {
|
||||||
val sendMessage = suspend {
|
val sendMessage = suspend {
|
||||||
graphQLRepo.createCopilotMessage(
|
graphQLService.createCopilotMessage(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
message = message,
|
message = message,
|
||||||
).onSuccess { messageId ->
|
).onSuccess { messageId ->
|
||||||
Timber.i("send message: $messageId")
|
Timber.i("send message: $messageId")
|
||||||
sseRepo.messageStream(sessionId, messageId)
|
sseService.messageStream(sessionId, messageId)
|
||||||
.onEach {
|
.onEach {
|
||||||
Timber.d("On sse message: ${it.getOrNull()}")
|
Timber.d("On sse message: ${it.getOrNull()}")
|
||||||
}
|
}
|
||||||
@@ -70,9 +70,9 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!this@ChatViewModel::sessionId.isInitialized) {
|
if (!this@ChatViewModel::sessionId.isInitialized) {
|
||||||
graphQLRepo.getCopilotSession(
|
graphQLService.getCopilotSession(
|
||||||
workspaceId = webRepo.workspaceId(),
|
workspaceId = webService.workspaceId(),
|
||||||
docId = webRepo.docId(),
|
docId = webService.docId(),
|
||||||
).onSuccess { id ->
|
).onSuccess { id ->
|
||||||
sessionId = id
|
sessionId = id
|
||||||
Timber.i("Create session: $id")
|
Timber.i("Create session: $id")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.getcapacitor.PluginMethod
|
|||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
@@ -144,7 +145,7 @@ class AuthPlugin : Plugin() {
|
|||||||
call.reject(response.body.string())
|
call.reject(response.body.string())
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
CookieStore.getCookie(endpoint, CookieStore.AFFINE_SESSION)?.let {
|
CookieStore.getCookie(endpoint.toHttpUrl(), CookieStore.AFFINE_SESSION)?.let {
|
||||||
Timber.i("$method sign in success.")
|
Timber.i("$method sign in success.")
|
||||||
Timber.d("Update session [$it]")
|
Timber.d("Update session [$it]")
|
||||||
call.resolve(JSObject().put("token", it))
|
call.resolve(JSObject().put("token", it))
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package app.affine.pro.repo
|
|
||||||
|
|
||||||
import com.getcapacitor.Bridge
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class WebRepo @Inject constructor() {
|
|
||||||
|
|
||||||
suspend fun init(bridge: Bridge) {
|
|
||||||
_workspaceId = eval(bridge, "window.getCurrentWorkspaceId()")
|
|
||||||
_docId = eval(bridge, "window.getCurrentDocId()")
|
|
||||||
_docContentInMD = eval(bridge, "window.getCurrentDocContentInMarkdown()")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun eval(bridge: Bridge, js: String): String {
|
|
||||||
return suspendCoroutine { continuation ->
|
|
||||||
bridge.eval(js) { result ->
|
|
||||||
continuation.resume(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var _workspaceId: String
|
|
||||||
private lateinit var _docId: String
|
|
||||||
private lateinit var _docContentInMD: String
|
|
||||||
|
|
||||||
fun workspaceId() = _workspaceId
|
|
||||||
|
|
||||||
fun docId() = _docId
|
|
||||||
|
|
||||||
fun docContentInMD() = _docContentInMD
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package app.affine.pro.service
|
|
||||||
|
|
||||||
import app.affine.pro.BuildConfig
|
|
||||||
import com.apollographql.apollo.ApolloClient
|
|
||||||
import com.apollographql.apollo.api.Mutation
|
|
||||||
import com.apollographql.apollo.api.Query
|
|
||||||
import com.apollographql.apollo.api.Subscription
|
|
||||||
import com.apollographql.apollo.network.okHttpClient
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class GraphQLClient @Inject constructor() {
|
|
||||||
|
|
||||||
private val _client: ApolloClient by lazy {
|
|
||||||
ApolloClient.Builder().serverUrl("${BuildConfig.BASE_URL}/graphql")
|
|
||||||
.okHttpClient(OkHttp.client)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <D : Query.Data> query(query: Query<D>) = withContext(Dispatchers.IO) {
|
|
||||||
runCatching {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
_client.query(query).execute().dataOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <D : Mutation.Data> mutation(mutation: Mutation<D>) = withContext(Dispatchers.IO) {
|
|
||||||
runCatching {
|
|
||||||
_client.mutation(mutation).execute().dataOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <D : Subscription.Data> subscription(subscription: Subscription<D>) =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
runCatching {
|
|
||||||
_client.subscription(subscription).execute().dataOrThrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
package app.affine.pro.repo
|
package app.affine.pro.service
|
||||||
|
|
||||||
import app.affine.pro.Prompt
|
import app.affine.pro.Prompt
|
||||||
import app.affine.pro.service.GraphQLClient
|
import app.affine.pro.utils.getCurrentServerBaseUrl
|
||||||
import com.affine.pro.graphql.CreateCopilotMessageMutation
|
import com.affine.pro.graphql.CreateCopilotMessageMutation
|
||||||
import com.affine.pro.graphql.CreateCopilotSessionMutation
|
import com.affine.pro.graphql.CreateCopilotSessionMutation
|
||||||
import com.affine.pro.graphql.GetCopilotHistoriesQuery
|
import com.affine.pro.graphql.GetCopilotHistoriesQuery
|
||||||
import com.affine.pro.graphql.GetCopilotSessionsQuery
|
import com.affine.pro.graphql.GetCopilotSessionsQuery
|
||||||
import com.affine.pro.graphql.type.CreateChatMessageInput
|
import com.affine.pro.graphql.type.CreateChatMessageInput
|
||||||
import com.affine.pro.graphql.type.CreateChatSessionInput
|
import com.affine.pro.graphql.type.CreateChatSessionInput
|
||||||
|
import com.apollographql.apollo.ApolloClient
|
||||||
|
import com.apollographql.apollo.api.Mutation
|
||||||
import com.apollographql.apollo.api.Optional
|
import com.apollographql.apollo.api.Optional
|
||||||
|
import com.apollographql.apollo.api.Query
|
||||||
|
import com.apollographql.apollo.api.Subscription
|
||||||
|
import com.apollographql.apollo.network.okHttpClient
|
||||||
|
import com.getcapacitor.Bridge
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class GraphQLRepo @Inject constructor(
|
class GraphQLService @Inject constructor() {
|
||||||
private val client: GraphQLClient
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun getCopilotSession(workspaceId: String, docId: String) = client.query(
|
suspend fun getCopilotSession(workspaceId: String, docId: String) = query(
|
||||||
GetCopilotSessionsQuery(
|
GetCopilotSessionsQuery(
|
||||||
workspaceId = workspaceId,
|
workspaceId = workspaceId,
|
||||||
docId = Optional.present(docId)
|
docId = Optional.present(docId)
|
||||||
@@ -30,7 +36,7 @@ class GraphQLRepo @Inject constructor(
|
|||||||
workspaceId: String,
|
workspaceId: String,
|
||||||
docId: String,
|
docId: String,
|
||||||
prompt: Prompt = Prompt.ChatWithAFFiNEAI
|
prompt: Prompt = Prompt.ChatWithAFFiNEAI
|
||||||
) = client.mutation(
|
) = mutation(
|
||||||
CreateCopilotSessionMutation(
|
CreateCopilotSessionMutation(
|
||||||
CreateChatSessionInput(
|
CreateChatSessionInput(
|
||||||
docId = docId,
|
docId = docId,
|
||||||
@@ -46,7 +52,7 @@ class GraphQLRepo @Inject constructor(
|
|||||||
workspaceId: String,
|
workspaceId: String,
|
||||||
docId: String,
|
docId: String,
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
) = client.query(
|
) = query(
|
||||||
GetCopilotHistoriesQuery(
|
GetCopilotHistoriesQuery(
|
||||||
workspaceId = workspaceId,
|
workspaceId = workspaceId,
|
||||||
docId = Optional.present(docId),
|
docId = Optional.present(docId),
|
||||||
@@ -60,15 +66,49 @@ class GraphQLRepo @Inject constructor(
|
|||||||
suspend fun createCopilotMessage(
|
suspend fun createCopilotMessage(
|
||||||
sessionId: String,
|
sessionId: String,
|
||||||
message: String,
|
message: String,
|
||||||
) = client.mutation(CreateCopilotMessageMutation(
|
) = mutation(
|
||||||
CreateChatMessageInput(
|
CreateCopilotMessageMutation(
|
||||||
sessionId = sessionId,
|
CreateChatMessageInput(
|
||||||
content = Optional.present(message)
|
sessionId = sessionId,
|
||||||
|
content = Optional.present(message)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)).mapCatching { data ->
|
).mapCatching { data ->
|
||||||
data.createCopilotMessage
|
data.createCopilotMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateServer(bridge: Bridge) {
|
||||||
|
val server = bridge.getCurrentServerBaseUrl()
|
||||||
|
if (this::_client.isInitialized && _client.newBuilder().httpServerUrl == server) return
|
||||||
|
_client = ApolloClient.Builder().serverUrl("$server/graphql")
|
||||||
|
.okHttpClient(OkHttp.client)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var _client: ApolloClient
|
||||||
|
|
||||||
|
private suspend fun <D : Query.Data> query(query: Query<D>) = withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
_client.query(query).execute().dataOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <D : Mutation.Data> mutation(mutation: Mutation<D>) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
_client.mutation(mutation).execute().dataOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <D : Subscription.Data> subscription(subscription: Subscription<D>) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
_client.subscription(subscription).execute().dataOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ERROR_NULL_SESSION_ID = "null session id."
|
private const val ERROR_NULL_SESSION_ID = "null session id."
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.affine.pro.service
|
package app.affine.pro.service
|
||||||
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import app.affine.pro.AffineApp
|
import app.affine.pro.AffineApp
|
||||||
import app.affine.pro.utils.dataStore
|
import app.affine.pro.utils.dataStore
|
||||||
import app.affine.pro.utils.set
|
import app.affine.pro.utils.set
|
||||||
@@ -53,11 +52,11 @@ object CookieStore {
|
|||||||
_cookies[host] = cookies
|
_cookies[host] = cookies
|
||||||
MainScope().launch(Dispatchers.IO) {
|
MainScope().launch(Dispatchers.IO) {
|
||||||
cookies.find { it.name == AFFINE_SESSION }?.let {
|
cookies.find { it.name == AFFINE_SESSION }?.let {
|
||||||
AffineApp.context().dataStore.set(AFFINE_SESSION, it.toString())
|
AffineApp.context().dataStore.set(host + AFFINE_SESSION, it.toString())
|
||||||
}
|
}
|
||||||
cookies.find { it.name == AFFINE_USER_ID }?.let {
|
cookies.find { it.name == AFFINE_USER_ID }?.let {
|
||||||
Timber.d("Update user id [${it.value}]")
|
Timber.d("Update user id [${it.value}]")
|
||||||
AffineApp.context().dataStore.set(AFFINE_USER_ID, it.toString())
|
AffineApp.context().dataStore.set(host + AFFINE_USER_ID, it.toString())
|
||||||
Firebase.crashlytics.setUserId(it.value)
|
Firebase.crashlytics.setUserId(it.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,8 +64,8 @@ object CookieStore {
|
|||||||
|
|
||||||
fun getCookies(host: String) = _cookies[host] ?: emptyList()
|
fun getCookies(host: String) = _cookies[host] ?: emptyList()
|
||||||
|
|
||||||
fun getCookie(url: String, name: String) = url.toUri().host
|
fun getCookie(url: HttpUrl, name: String) = url.host
|
||||||
?.let { _cookies[it] }
|
.let { _cookies[it] }
|
||||||
?.find { cookie -> cookie.name == name }
|
?.find { cookie -> cookie.name == name }
|
||||||
?.value
|
?.value
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.affine.pro.repo
|
package app.affine.pro.service
|
||||||
|
|
||||||
import app.affine.pro.BuildConfig
|
import app.affine.pro.utils.getCurrentServerBaseUrl
|
||||||
import app.affine.pro.service.OkHttp
|
import com.getcapacitor.Bridge
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
@@ -17,10 +17,16 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SSERepo @Inject constructor() {
|
class SSEService @Inject constructor() {
|
||||||
|
|
||||||
|
private lateinit var _server: String
|
||||||
|
|
||||||
|
suspend fun updateServer(bridge: Bridge) {
|
||||||
|
_server = bridge.getCurrentServerBaseUrl()
|
||||||
|
}
|
||||||
|
|
||||||
fun messageStream(sessionId: String, messageId: String) =
|
fun messageStream(sessionId: String, messageId: String) =
|
||||||
"${BuildConfig.BASE_URL}/api/copilot/chat/$sessionId/stream?messageId=$messageId".eventSource()
|
"$_server/api/copilot/chat/$sessionId/stream?messageId=$messageId".eventSource()
|
||||||
|
|
||||||
data class Event(val id: String?, val type: String?, val data: String)
|
data class Event(val id: String?, val type: String?, val data: String)
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.affine.pro.service
|
||||||
|
|
||||||
|
import app.affine.pro.utils.getCurrentDocContentInMarkdown
|
||||||
|
import app.affine.pro.utils.getCurrentDocId
|
||||||
|
import app.affine.pro.utils.getCurrentWorkspaceId
|
||||||
|
import com.getcapacitor.Bridge
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class WebService @Inject constructor() {
|
||||||
|
|
||||||
|
suspend fun update(bridge: Bridge) {
|
||||||
|
_workspaceId = bridge.getCurrentWorkspaceId()
|
||||||
|
_docId = bridge.getCurrentDocId()
|
||||||
|
_docContentInMD = bridge.getCurrentDocContentInMarkdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var _workspaceId: String
|
||||||
|
private lateinit var _docId: String
|
||||||
|
private lateinit var _docContentInMD: String
|
||||||
|
|
||||||
|
fun workspaceId() = _workspaceId
|
||||||
|
|
||||||
|
fun docId() = _docId
|
||||||
|
|
||||||
|
fun docContentInMD() = _docContentInMD
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package app.affine.pro.service.interceptor
|
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
|
||||||
import app.affine.pro.BuildConfig
|
|
||||||
import com.apollographql.apollo.api.http.HttpRequest
|
|
||||||
import com.apollographql.apollo.network.http.HttpInterceptor
|
|
||||||
import com.apollographql.apollo.network.http.HttpInterceptorChain
|
|
||||||
|
|
||||||
object CookieInterceptor : HttpInterceptor {
|
|
||||||
override suspend fun intercept(
|
|
||||||
request: HttpRequest,
|
|
||||||
chain: HttpInterceptorChain
|
|
||||||
) = chain.proceed(
|
|
||||||
request.newBuilder().addHeader(
|
|
||||||
"Cookie", CookieManager.getInstance().getCookie(BuildConfig.BASE_URL)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -19,12 +19,4 @@ suspend fun DataStore<Preferences>.set(key: String, value: String) {
|
|||||||
|
|
||||||
suspend fun DataStore<Preferences>.get(key: String) = data.map {
|
suspend fun DataStore<Preferences>.get(key: String) = data.map {
|
||||||
it[stringPreferencesKey(key)] ?: ""
|
it[stringPreferencesKey(key)] ?: ""
|
||||||
}.first()
|
}.first()
|
||||||
|
|
||||||
suspend fun DataStore<Preferences>.clear(vararg keys: String) {
|
|
||||||
edit { prefs ->
|
|
||||||
keys.forEach { key ->
|
|
||||||
prefs[stringPreferencesKey(key)] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package app.affine.pro.utils
|
||||||
|
|
||||||
|
import com.getcapacitor.Bridge
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun Bridge.getCurrentServerBaseUrl() = eval("window.getCurrentServerBaseUrl()").strict()
|
||||||
|
|
||||||
|
suspend fun Bridge.getCurrentWorkspaceId() = eval("window.getCurrentWorkspaceId()").strict()
|
||||||
|
|
||||||
|
suspend fun Bridge.getCurrentDocId() = eval("window.getCurrentDocId()").strict()
|
||||||
|
|
||||||
|
suspend fun Bridge.getCurrentDocContentInMarkdown() =
|
||||||
|
eval("window.getCurrentDocContentInMarkdown()").strict()
|
||||||
|
|
||||||
|
private suspend fun Bridge.eval(js: String): String {
|
||||||
|
return suspendCoroutine { continuation ->
|
||||||
|
eval(js) { result ->
|
||||||
|
continuation.resume(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.strict() = let {
|
||||||
|
if (startsWith("\"") && endsWith("\"")) {
|
||||||
|
substring(1, lastIndex)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.MainScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -41,19 +42,19 @@ class FileTree(context: Context) : Timber.Tree() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkAndUploadOldLogs() {
|
suspend fun checkAndUploadOldLogs(server: HttpUrl) {
|
||||||
val today = dateFormat.format(Date())
|
val today = dateFormat.format(Date())
|
||||||
logDirectory.listFiles()?.forEach { file ->
|
logDirectory.listFiles()?.forEach { file ->
|
||||||
val fileName = file.name
|
val fileName = file.name
|
||||||
if (fileName.endsWith(".log") && !fileName.startsWith(today)) {
|
if (fileName.endsWith(".log") && !fileName.startsWith(today)) {
|
||||||
uploadLogToFirebase(file)
|
uploadLogToFirebase(server, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun uploadLogToFirebase(file: File) =
|
private suspend fun uploadLogToFirebase(server: HttpUrl, file: File) =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val user = CookieStore.getCookie(BuildConfig.BASE_URL, CookieStore.AFFINE_USER_ID)
|
val user = CookieStore.getCookie(server, CookieStore.AFFINE_USER_ID)
|
||||||
?: return@suspendCancellableCoroutine
|
?: return@suspendCancellableCoroutine
|
||||||
val storageRef = Firebase.storage.reference
|
val storageRef = Firebase.storage.reference
|
||||||
val logFileRef = storageRef.child("android_log/$user/${file.name}")
|
val logFileRef = storageRef.child("android_log/$user/${file.name}")
|
||||||
|
|||||||
Reference in New Issue
Block a user