SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt

235 lines
8.6 KiB
Kotlin

package jp.juggler.subwaytooter.action
import android.app.Dialog
import android.os.Build
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.actmain.afterAccountVerify
import jp.juggler.subwaytooter.actmain.defaultInsertPosition
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.auth.Auth2Result
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.*
import jp.juggler.subwaytooter.dialog.DlgCreateAccount.Companion.showUserCreateDialog
import jp.juggler.subwaytooter.dialog.LoginForm.Companion.showLoginForm
import jp.juggler.subwaytooter.pref.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.encodePercent
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.network.toFormRequestBody
import jp.juggler.util.network.toPost
import jp.juggler.util.ui.dismissSafe
import kotlinx.coroutines.*
private val log = LogCategory("Action_Account")
// Androidでは \w や \d がUnicode文字にマッチしてしまうので、IDEの警告を無視する
@Suppress("RegExpSimplifiable")
private val mailRegex =
"""\A[a-z0-9_+&*-]+(?:\.[a-z0-9_+&*-]+)*@(?:[a-z0-9-]+\.)+[a-z]{2,12}\z""".toRegex(
RegexOption.IGNORE_CASE
)
fun isAndroid7TlsBug(errorText: String) =
if (!errorText.contains("SSLHandshakeException")) {
false
} else {
val release = Build.VERSION.RELEASE
when {
release.startsWith("7.1.") -> false // 含まない 7.1.x
release.startsWith("7.1") -> true // 含む 7.1
release.startsWith("7.0") -> true // 含む 7.0
else -> false
}
}
/**
* サイドメニューで「アカウントの追加」を選ぶと呼び出される。
* - サーバ名とアクションを指定するダイアログを開く。
* - 選択されたアクションに応じて分岐する。
*/
fun ActMain.accountAdd() {
showLoginForm { dialogHost, apiHost, serverInfo, action ->
launchMain {
try {
when (action) {
LoginForm.Action.Login -> {
val authUri = runApiTask2(apiHost) { it.authStep1() }
openBrowser(authUri)
dialogHost.dismissSafe()
}
LoginForm.Action.Pseudo -> {
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
addPseudoAccount(apiHost, tootInstance)?.let { a ->
showToast(false, R.string.server_confirmed)
addColumn(defaultInsertPosition, a, ColumnType.LOCAL)
dialogHost.dismissSafe()
}
}
LoginForm.Action.Create ->
createUser(apiHost, serverInfo) { dialogHost.dismissSafe() }
LoginForm.Action.Token ->
accessTokenPrompt(apiHost) { dialogHost.dismissSafe() }
}
} catch (ex: Throwable) {
showApiError(ex)
}
}
}
}
private suspend fun ActMain.createUser(
apiHost: Host,
serverInfo: TootInstance?,
onComplete: (() -> Unit)? = null,
) {
serverInfo ?: error(
getString(
R.string.user_creation_not_supported,
apiHost.pretty,
"(unknown)",
)
)
fun TootApiClient.authUserCreate() =
AuthBase.findAuthForCreateUser(this, serverInfo)
?: error(
getString(
R.string.user_creation_not_supported,
apiHost.pretty,
serverInfo.instanceType.toString(),
)
)
// クライアント情報を取得。サーバ種別によってはユーザ作成ができないのでエラーとなる
val clientInfo = runApiTask2(apiHost) {
it.authUserCreate().prepareClient(serverInfo)
}
showUserCreateDialog(apiHost) { dialogCreate, params ->
launchMain {
try {
val auth2Result = runApiTask2(apiHost) {
it.authUserCreate().createUser(clientInfo = clientInfo, params = params)
}
if (afterAccountVerify(auth2Result)) {
dialogCreate.dismissSafe()
onComplete?.invoke()
}
} catch (ex: Throwable) {
showApiError(ex)
}
}
}
}
/**
* アクセストークンを手動入力する。
*
* @param apiHost アクセストークンと関係のあるサーバのAPIホスト
* @param onComplete 非nullならアカウント認証が終わったタイミングで呼ばれる
*/
// アクセストークンの手動入力(更新)
fun ActMain.accessTokenPrompt(
apiHost: Host,
onComplete: (() -> Unit)? = null,
) {
DlgTextInput.show(
this,
getString(R.string.access_token_or_api_token),
null,
callback = object : DlgTextInput.Callback {
override fun onEmptyError() {
showToast(true, R.string.token_not_specified)
}
override fun onOK(dialog: Dialog, text: String) {
launchMain {
try {
val accessToken = text.trim()
val auth2Result = runApiTask2(apiHost) { client ->
val ti =
TootInstance.getExOrThrow(client, forceAccessToken = accessToken)
val tokenJson = JsonObject()
val userJson = client.verifyAccount(
accessToken,
outTokenInfo = tokenJson, // 更新される
misskeyVersion = ti.misskeyVersionMajor
)
val parser = TootParser(this, linkHelper = LinkHelper.create(ti))
Auth2Result(
tootInstance = ti,
tokenJson = tokenJson,
accountJson = userJson,
tootAccount = parser.account(userJson)
?: error("can't parse user information."),
)
}
if (afterAccountVerify(auth2Result)) {
dialog.dismissSafe()
onComplete?.invoke()
}
} catch (ex: Throwable) {
showApiError(ex)
}
}
}
}
)
}
// アカウント設定
fun ActMain.accountOpenSetting() {
launchMain {
pickAccount(
bAllowPseudo = true,
bAuto = true,
message = getString(R.string.account_picker_open_setting)
)?.let {
arAccountSetting.launch(ActAccountSetting.createIntent(this@accountOpenSetting, it))
}
}
}
fun ActMain.accountResendConfirmMail(accessInfo: SavedAccount) {
DlgConfirmMail(
this,
accessInfo
) { email ->
launchMain {
runApiTask(accessInfo) { client ->
email?.let {
if (!mailRegex.matches(it)) {
return@runApiTask TootApiResult("email address is not valid.")
}
}
client.request(
"/api/v1/emails/confirmations",
ArrayList<String>().apply {
if (email != null) add("email=${email.encodePercent()}")
}.joinToString("&").toFormRequestBody().toPost()
)
}?.let { result ->
when (val error = result.error) {
null -> showToast(true, R.string.resend_confirm_mail_requested)
else -> showToast(true, error)
}
}
}
}.show()
}