Skip to content

Commit

Permalink
Introduce AuthenticatedXrpcBlueskyApi convenience implementation (#14)
Browse files Browse the repository at this point in the history
This implementation will transparently manage the active session and
refresh tokens for that BlueskyApi instance as it interacts with
session-related APIs.
  • Loading branch information
christiandeange authored Dec 26, 2024
1 parent f767732 commit 5fc7e8c
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ import io.ktor.client.plugins.logging.Logging
import io.ktor.http.takeFrom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.BlueskyApi
import sh.christian.ozone.XrpcBlueskyApi
import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.login.LoginRepository
Expand All @@ -29,29 +26,27 @@ class ApiProvider(
private val apiRepository: ServerRepository,
private val loginRepository: LoginRepository,
) : Supervisor() {

private val apiHost = MutableStateFlow(apiRepository.server.host)
private val auth = MutableStateFlow(loginRepository.auth)
private val tokens = MutableStateFlow(loginRepository.auth?.toTokens())

private val client = HttpClient(engine) {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.NONE
}

install(XrpcAuthPlugin) {
authTokens = tokens
}

install(DefaultRequest) {
url.takeFrom(apiHost.value)
}

expectSuccess = false
}

val api: BlueskyApi = XrpcBlueskyApi(client)
private val _api = AuthenticatedXrpcBlueskyApi(
httpClient = client,
initialTokens = loginRepository.auth?.toTokens(),
)

val api: BlueskyApi = _api

override suspend fun CoroutineScope.onStart() {
coroutineScope {
Expand All @@ -62,15 +57,7 @@ class ApiProvider(
}

launch(OzoneDispatchers.IO) {
loginRepository.authFlow().collect {
tokens.value = it?.toTokens()
yield()
auth.value = it
}
}

launch(OzoneDispatchers.IO) {
tokens.collect { tokens ->
_api.authTokens.collect { tokens ->
if (tokens != null) {
loginRepository.auth = loginRepository.auth?.withTokens(tokens)
} else {
Expand All @@ -81,11 +68,13 @@ class ApiProvider(
}
}

fun auth(): Flow<AuthInfo?> = auth
fun signOut() {
_api.clearCredentials()
}

private fun AuthInfo.toTokens() = Tokens(accessJwt, refreshJwt)
private fun AuthInfo.toTokens() = BlueskyAuthPlugin.Tokens(accessJwt, refreshJwt)

private fun AuthInfo.withTokens(tokens: Tokens) = copy(
private fun AuthInfo.withTokens(tokens: BlueskyAuthPlugin.Tokens) = copy(
accessJwt = tokens.auth,
refreshJwt = tokens.refresh,
)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class AppWorkflow(
context: RenderContext,
): AppScreen = when (renderState) {
is ShowingLogin -> {
context.runningWorker(apiProvider.auth().filterNotNull().asWorker(), "has-auth") { auth ->
context.runningWorker(loginRepository.authFlow().filterNotNull().asWorker(), "has-auth") { auth ->
action {
state = ShowingLoggedIn(HomeProps(auth, 0))
}
Expand All @@ -68,7 +68,7 @@ class AppWorkflow(
state = ShowingLoggedIn(renderState.props.copy(unreadNotificationCount = unread))
}
}
context.runningWorker(apiProvider.auth().filter { it == null }.asWorker(), "no-auth") {
context.runningWorker(loginRepository.authFlow().filter { it == null }.asWorker(), "no-auth") {
action {
state = ShowingLogin
}
Expand All @@ -78,7 +78,7 @@ class AppWorkflow(
action {
when (output) {
is HomeOutput.CloseApp -> setOutput(Unit)
is HomeOutput.SignOut -> loginRepository.auth = null
is HomeOutput.SignOut -> apiProvider.signOut()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.error.ErrorProps
import sh.christian.ozone.error.toErrorProps
import sh.christian.ozone.login.LoginRepository
import sh.christian.ozone.model.Notifications
import sh.christian.ozone.model.TimelinePost
import sh.christian.ozone.model.toNotification
Expand All @@ -43,6 +44,7 @@ import kotlin.time.Duration.Companion.minutes
@SingleInApp
class NotificationsRepository(
private val apiProvider: ApiProvider,
private val loginRepository: LoginRepository,
) : Supervisor() {
private val latest: MutableStateFlow<Notifications> = MutableStateFlow(EMPTY_VALUE)
private val loadErrors: MutableSharedFlow<ErrorProps> = MutableSharedFlow()
Expand All @@ -59,7 +61,7 @@ class NotificationsRepository(

@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun CoroutineScope.onStart() {
apiProvider.auth()
loginRepository.authFlow()
.flatMapLatest { auth ->
if (auth != null) {
flow {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.error.ErrorProps
import sh.christian.ozone.error.toErrorProps
import sh.christian.ozone.login.LoginRepository
import sh.christian.ozone.model.Timeline
import sh.christian.ozone.util.toReadOnlyList

@Inject
@SingleInApp
class TimelineRepository(
private val apiProvider: ApiProvider,
private val loginRepository: LoginRepository,
): Supervisor() {
private val latest: MutableStateFlow<Timeline?> = MutableStateFlow(null)
private val loadErrors: MutableSharedFlow<ErrorProps> = MutableSharedFlow()
Expand All @@ -29,7 +31,7 @@ class TimelineRepository(
val errors: Flow<ErrorProps> = loadErrors

override suspend fun CoroutineScope.onStart() {
apiProvider.auth().filter { it == null }.collect {
loginRepository.authFlow().filter { it == null }.collect {
latest.value = null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@ import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.api.ApiProvider
import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.login.LoginRepository
import sh.christian.ozone.model.FullProfile

@Inject
@SingleInApp
class MyProfileRepository(
private val apiProvider: ApiProvider,
private val userDatabase: UserDatabase,
private val loginRepository: LoginRepository,
) : Supervisor() {
private val profileFlow = MutableStateFlow<FullProfile?>(null)

@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun CoroutineScope.onStart() {
apiProvider.auth().flatMapLatest { auth ->
loginRepository.authFlow().flatMapLatest { auth ->
auth?.did
?.let { did -> userDatabase.profile(UserDid(did)) }
?: flowOf(null)
Expand Down
Loading

0 comments on commit 5fc7e8c

Please sign in to comment.