Private
Public Access
1
0

Much better error/badconfig handling

This commit is contained in:
2024-03-23 19:01:20 -07:00
parent 611ad15997
commit f266e04895
7 changed files with 101 additions and 45 deletions

View File

@@ -5,6 +5,7 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import net.buzzert.kordophone.backend.db.CachedChatDatabase import net.buzzert.kordophone.backend.db.CachedChatDatabase
import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.Authentication import net.buzzert.kordophone.backend.server.Authentication
import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophone.backend.server.RetrofitAPIClient import net.buzzert.kordophone.backend.server.RetrofitAPIClient
@@ -19,18 +20,10 @@ object AppModule {
@Provides @Provides
fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository { fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository {
val serverConfig = configRepository.serverConfig.value val serverConfig = configRepository.serverConfig.value
val authentication = serverConfig.authentication?.let {
Authentication(it.username, it.password)
}
// TODO: This is really bad error handling... val server = serverConfig.serverName
val baseURL: URL = try { URL(serverConfig.serverName) } val authentication = serverConfig.authentication?.toBackendAuthentication()
catch (e: java.net.MalformedURLException) { URL("http://localhost") } val client = APIClientFactory.createClient(server, authentication)
val client = RetrofitAPIClient(
baseURL = baseURL,
authentication = authentication ?: Authentication("", "")
)
val database = CachedChatDatabase.liveDatabase() val database = CachedChatDatabase.liveDatabase()
return ChatRepository(client, database) return ChatRepository(client, database)

View File

@@ -20,6 +20,8 @@ import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.messagelist.MVM_LOG import net.buzzert.kordophonedroid.ui.messagelist.MVM_LOG
import javax.inject.Inject import javax.inject.Inject
const val AVM_LOG: String = "AttachmentViewModel"
data class AttachmentFetchData( data class AttachmentFetchData(
val guid: String, val guid: String,
val preview: Boolean = false val preview: Boolean = false
@@ -72,7 +74,7 @@ private class AttachmentFetcher(
val data: AttachmentFetchData val data: AttachmentFetchData
): Fetcher { ): Fetcher {
override suspend fun fetch(): FetchResult { override suspend fun fetch(): FetchResult {
Log.d(MVM_LOG, "Loading attachment ${data.guid} from network") Log.d(AVM_LOG, "Loading attachment ${data.guid} from network")
val source = repository.fetchAttachmentDataSource(data.guid, data.preview) val source = repository.fetchAttachmentDataSource(data.guid, data.preview)
return SourceResult( return SourceResult(
source = ImageSource(source, context), source = ImageSource(source, context),

View File

@@ -10,10 +10,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.buzzert.kordophone.backend.model.Conversation import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.Authentication import net.buzzert.kordophone.backend.server.Authentication
import net.buzzert.kordophone.backend.server.ChatRepository import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophone.backend.server.RetrofitAPIClient import net.buzzert.kordophone.backend.server.RetrofitAPIClient
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
@@ -37,21 +40,11 @@ class ConversationListViewModel @Inject constructor(
serverConfigRepository.serverConfig.collect { config -> serverConfigRepository.serverConfig.collect { config ->
Log.d(CL_VM_LOG, "Got settings change.") Log.d(CL_VM_LOG, "Got settings change.")
// Check settings
try {
val baseURL = URL(config.serverName)
val authentication = config.authentication?.let { serverAuth ->
Authentication(serverAuth.username, serverAuth.password)
} ?: throw Error("No authentication data.")
// Make new APIClient // Make new APIClient
val apiClient = RetrofitAPIClient(baseURL, authentication) val baseURL = config.serverName
val authentication = config.authentication?.toBackendAuthentication()
val apiClient = APIClientFactory.createClient(baseURL, authentication)
chatRepository.updateAPIClient(apiClient) chatRepository.updateAPIClient(apiClient)
} catch (e: Error) {
Log.e(CL_VM_LOG, "Error re-creating API client for settings change: $e")
} catch (e: java.net.MalformedURLException) {
Log.e(CL_VM_LOG, "Malformed server URL")
}
// Perform db synchronization // Perform db synchronization
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@@ -8,6 +8,7 @@ import androidx.security.crypto.MasterKey
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import net.buzzert.kordophone.backend.server.Authentication
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -84,6 +85,10 @@ data class ServerAuthentication(
apply() apply()
} }
} }
fun toBackendAuthentication(): Authentication {
return Authentication(username, password)
}
} }
@Singleton @Singleton

View File

@@ -1,12 +1,16 @@
package net.buzzert.kordophone.backend.server package net.buzzert.kordophone.backend.server
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophone.backend.model.Message
import okhttp3.Authenticator import okhttp3.Authenticator
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.Route import okhttp3.Route
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
@@ -71,13 +75,15 @@ class TokenAuthenticator(
apiInterface.authenticate(request).body() apiInterface.authenticate(request).body()
} }
if (token == null) { when (token) {
null -> {
// Auth failure. // Auth failure.
// TODO: How to bubble this up? // TODO: How to bubble this up?
return null return null
} }
// Update token store // Update token store
else -> {
tokenStore.authenticationToken = token.serializedToken tokenStore.authenticationToken = token.serializedToken
return response.request().newBuilder() return response.request().newBuilder()
@@ -85,6 +91,57 @@ class TokenAuthenticator(
.build() .build()
} }
} }
}
}
class APIClientFactory {
companion object {
fun createClient(serverString: String?, authentication: Authentication?): APIClient {
if (serverString == null || authentication == null) {
return InvalidConfigurationAPIClient()
}
// Try to parse server string
val serverURL = HttpUrl.parse(serverString) ?: return InvalidConfigurationAPIClient()
return RetrofitAPIClient(serverURL.url(), authentication)
}
}
}
// TODO: Is this a dumb idea?
class InvalidConfigurationAPIClient: APIClient {
private class InvalidConfigurationAPIInterface: APIInterface {
private fun throwError(): Nothing {
throw Error("Invalid Configuration.")
}
override suspend fun getVersion(): ResponseBody = throwError()
override suspend fun getConversations(): retrofit2.Response<List<Conversation>> = throwError()
override suspend fun sendMessage(request: SendMessageRequest): retrofit2.Response<SendMessageResponse> = throwError()
override suspend fun markConversation(conversationGUID: String): retrofit2.Response<Void> = throwError()
override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody = throwError()
override suspend fun authenticate(request: AuthenticationRequest): retrofit2.Response<AuthenticationResponse> = throwError()
override suspend fun getMessages(
conversationGUID: String,
limit: Int?,
beforeMessageGUID: GUID?,
afterMessageGUID: GUID?
): retrofit2.Response<List<Message>> = throwError()
}
override fun getAPIInterface(): APIInterface {
return InvalidConfigurationAPIInterface()
}
override fun getWebSocketClient(
serverPath: String,
queryParams: Map<String, String>?,
listener: WebSocketListener
): WebSocket {
throw Error("Invalid configuration.")
}
}
class RetrofitAPIClient( class RetrofitAPIClient(
private val baseURL: URL, private val baseURL: URL,

View File

@@ -41,9 +41,9 @@ class ChatRepository(
open val title: String = "Error" open val title: String = "Error"
open val description: String = "Generic Error" open val description: String = "Generic Error"
data class ConnectionError(val exception: java.lang.Exception): Error() { data class ConnectionError(val message: String?): Error() {
override val title: String = "Connection Error" override val title: String = "Connection Error"
override val description: String = exception.message ?: "???" override val description: String = message ?: "???"
} }
} }
@@ -172,7 +172,9 @@ class ChatRepository(
synchronizeConversation(conversation) synchronizeConversation(conversation)
} }
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
_errorEncounteredChannel.emit(Error.ConnectionError(e)) _errorEncounteredChannel.emit(Error.ConnectionError(e.message))
} catch (e: java.lang.Error) {
_errorEncounteredChannel.emit(Error.ConnectionError(e.message))
} }
suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = try { suspend fun synchronizeConversation(conversation: Conversation, limit: Int = 15) = try {
@@ -181,7 +183,7 @@ class ChatRepository(
val messages = fetchMessages(conversation, limit = limit) val messages = fetchMessages(conversation, limit = limit)
database.writeMessages(messages, conversation) database.writeMessages(messages, conversation)
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
_errorEncounteredChannel.emit(Error.ConnectionError(e)) _errorEncounteredChannel.emit(Error.ConnectionError(e.message))
} }
suspend fun markConversationAsRead(conversation: Conversation) = try { suspend fun markConversationAsRead(conversation: Conversation) = try {

View File

@@ -41,11 +41,15 @@ class UpdateMonitor(private val client: APIClient) : WebSocketListener() {
fun beginMonitoringUpdates() { fun beginMonitoringUpdates() {
Log.d(UPMON_LOG, "Opening websocket connection") Log.d(UPMON_LOG, "Opening websocket connection")
try {
this.webSocket = client.getWebSocketClient( this.webSocket = client.getWebSocketClient(
serverPath = "updates", serverPath = "updates",
queryParams = mapOf("seq" to messageSeq.toString()), queryParams = mapOf("seq" to messageSeq.toString()),
listener = this listener = this
) )
} catch (e: Error) {
Log.e(UPMON_LOG, "Error getting websocket client: ${e.message}")
}
} }
fun stopMonitoringForUpdates() { fun stopMonitoringForUpdates() {