/* * Copyright (C) 2020 Conny Duck * * This file is part of Pixelcat. * * Pixelcat is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Pixelcat is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package at.connyduck.pixelcat.components.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.connyduck.pixelcat.config.Config import at.connyduck.pixelcat.db.AccountManager import at.connyduck.pixelcat.db.entitity.AccountAuthData import at.connyduck.pixelcat.network.FediverseApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.launch import okhttp3.HttpUrl import java.util.Locale import javax.inject.Inject @FlowPreview @ExperimentalCoroutinesApi class LoginViewModel @Inject constructor( private val api: FediverseApi, private val accountManager: AccountManager ) : ViewModel() { private val loginState = ConflatedBroadcastChannel(LoginModel(state = LoginState.NO_ERROR)) fun observe() = loginState.asFlow() fun startLogin(input: String) { viewModelScope.launch { val domainInput = canonicalizeDomain(input) try { HttpUrl.Builder().host(domainInput).scheme("https").build() } catch (e: IllegalArgumentException) { loginState.send(LoginModel(input, LoginState.INVALID_DOMAIN)) return@launch } val exceptionMatch = Config.domainExceptions.any { exception -> domainInput.equals(exception, true) || domainInput.endsWith(".$exception", true) } if (exceptionMatch) { loginState.send(LoginModel(input, LoginState.AUTH_ERROR)) return@launch } loginState.send(LoginModel(input, LoginState.LOADING)) api.authenticateAppAsync( domain = domainInput, clientName = "Pixelcat", clientWebsite = Config.website, redirectUris = Config.oAuthRedirect, scopes = Config.oAuthScopes ).fold( { appData -> loginState.send(LoginModel(input, LoginState.SUCCESS, domainInput, appData.clientId, appData.clientSecret)) }, { loginState.send(LoginModel(input, LoginState.AUTH_ERROR)) } ) } } fun authCode(authCode: String) { viewModelScope.launch { val loginModel = loginState.value api.fetchOAuthToken( domain = loginModel.domain!!, clientId = loginModel.clientId!!, clientSecret = loginModel.clientSecret!!, redirectUri = Config.oAuthRedirect, code = authCode ).fold( { tokenResponse -> val authData = AccountAuthData( accessToken = tokenResponse.accessToken, refreshToken = tokenResponse.refreshToken, tokenExpiresAt = tokenResponse.createdAt ?: 0 + ( tokenResponse.expiresIn ?: 0 ), clientId = loginModel.clientId, clientSecret = loginModel.clientSecret ) accountManager.addAccount(loginModel.domain, authData) loginState.send(loginState.value.copy(state = LoginState.SUCCESS_FINAL)) }, { loginState.send(loginState.value.copy(state = LoginState.AUTH_ERROR)) } ) } } fun removeError() { viewModelScope.launch { loginState.send(loginState.value.copy(state = LoginState.NO_ERROR)) } } private fun canonicalizeDomain(domain: String): String { // Strip any schemes out. var s = domain.replaceFirst("http://", "") .replaceFirst("https://", "") // If a username was included (e.g. username@example.com), just take what's after the '@'. val at = s.lastIndexOf('@') if (at != -1) { s = s.substring(at + 1) } return s.trim().toLowerCase(Locale.ROOT) } }