package jp.juggler.subwaytooter.action import androidx.appcompat.app.AlertDialog import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.dialog.DlgConfirm import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.UserRelation import jp.juggler.util.* fun ActMain.clickFollow( pos: Int, accessInfo: SavedAccount, who: TootAccount, whoRef: TootAccountRef, relation: UserRelation, ) { when { accessInfo.isPseudo -> followFromAnotherAccount(pos, accessInfo, who) accessInfo.isMisskey && relation.getRequested(who) && !relation.getFollowing(who) -> followRequestDelete( pos, accessInfo, whoRef, callback = cancelFollowRequestCompleteCallback ) else -> { val bSet = !(relation.getRequested(who) || relation.getFollowing(who)) follow( pos, accessInfo, whoRef, bFollow = bSet, callback = when (bSet) { true -> followCompleteCallback else -> unfollowCompleteCallback } ) } } } fun ActMain.follow( pos: Int, accessInfo: SavedAccount, whoRef: TootAccountRef, bFollow: Boolean = true, bConfirmMoved: Boolean = false, bConfirmed: Boolean = false, callback: () -> Unit = {}, ) { val activity = this@follow val who = whoRef.get() if (accessInfo.isMe(who)) { showToast(false, R.string.it_is_you) return } if (!bConfirmMoved && bFollow && who.moved != null) { AlertDialog.Builder(activity) .setMessage( getString( R.string.jump_moved_user, accessInfo.getFullAcct(who), accessInfo.getFullAcct(who.moved) ) ) .setPositiveButton(R.string.ok) { _, _ -> userProfileFromAnotherAccount( pos, accessInfo, who.moved ) } .setNeutralButton(R.string.ignore_suggestion) { _, _ -> follow( pos, accessInfo, whoRef, bFollow = bFollow, bConfirmMoved = true, // CHANGED bConfirmed = bConfirmed, callback = callback ) } .setNegativeButton(R.string.cancel, null) .show() return } if (!bConfirmed) { if (bFollow && who.locked) { DlgConfirm.open( activity, activity.getString( R.string.confirm_follow_request_who_from, whoRef.decoded_display_name, AcctColor.getNickname(accessInfo) ), object : DlgConfirm.Callback { override fun onOK() { follow( pos, accessInfo, whoRef, bFollow = bFollow, bConfirmMoved = bConfirmMoved, bConfirmed = true, // CHANGED callback = callback ) } override var isConfirmEnabled: Boolean get() = accessInfo.confirm_follow_locked set(value) { accessInfo.confirm_follow_locked = value accessInfo.saveSetting() activity.reloadAccountSetting(accessInfo) } }) return } else if (bFollow) { DlgConfirm.open( activity, getString( R.string.confirm_follow_who_from, whoRef.decoded_display_name, AcctColor.getNickname(accessInfo) ), object : DlgConfirm.Callback { override fun onOK() { follow( pos, accessInfo, whoRef, bFollow = bFollow, bConfirmMoved = bConfirmMoved, bConfirmed = true, //CHANGED callback = callback ) } override var isConfirmEnabled: Boolean get() = accessInfo.confirm_follow set(value) { accessInfo.confirm_follow = value accessInfo.saveSetting() activity.reloadAccountSetting(accessInfo) } }) return } else { DlgConfirm.open( activity, getString( R.string.confirm_unfollow_who_from, whoRef.decoded_display_name, AcctColor.getNickname(accessInfo) ), object : DlgConfirm.Callback { override fun onOK() { follow( pos, accessInfo, whoRef, bFollow = bFollow, bConfirmMoved = bConfirmMoved, bConfirmed = true, // CHANGED callback = callback ) } override var isConfirmEnabled: Boolean get() = accessInfo.confirm_unfollow set(value) { accessInfo.confirm_unfollow = value accessInfo.saveSetting() activity.reloadAccountSetting(accessInfo) } }) return } } launchMain { var resultRelation: UserRelation? = null runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> val parser = TootParser(activity, accessInfo) var userId = who.id if (who.isRemote) { // リモートユーザの確認 val skipAccountSync = if (accessInfo.isMisskey) { // Misskey の /users/show はリモートユーザに関して404を返すので // userIdからリモートユーザを照合することはできない。 // ただし検索APIがエラーになるかどうかは未確認 false } else { // https://github.com/tateisu/SubwayTooter/issues/124 // によると、閉じたタンスのユーザを同期しようとすると検索APIがエラーを返す // この問題を回避するため、手持ちのuserIdで照合したユーザのacctが目的のユーザと同じなら // 検索APIを呼び出さないようにする val result = client.request("/api/v1/accounts/$userId") ?: return@runApiTask null who.acct == parser.account(result.jsonObject)?.acct } if (!skipAccountSync) { // 同タンスのIDではなかった場合、検索APIを使う val (result, ar) = client.syncAccountByAcct(accessInfo, who.acct) val user = ar?.get() ?: return@runApiTask result userId = user.id } } if (accessInfo.isMisskey) { client.request( when { bFollow -> "/api/following/create" else -> "/api/following/delete" }, accessInfo.putMisskeyApiToken().apply { put("userId", userId) } .toPostRequestBuilder() )?.also { result -> fun saveFollow(f: Boolean) { val ur = UserRelation.load(accessInfo.db_id, userId) ur.following = f UserRelation.save1Misskey( System.currentTimeMillis(), accessInfo.db_id, userId.toString(), ur ) resultRelation = ur } val error = result.error when { // success error == null -> saveFollow(bFollow) // already followed/unfollowed error.contains("already following") -> saveFollow(bFollow) error.contains("already not following") -> saveFollow(bFollow) // else something error } } } else { client.request( "/api/v1/accounts/$userId/${if (bFollow) "follow" else "unfollow"}", "".toFormRequestBody().toPost() )?.also { result -> val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject) resultRelation = accessInfo.saveUserRelation(newRelation) } } }?.let { result -> val relation = resultRelation when { relation != null -> { when { // 鍵付きアカウントにフォローリクエストを申請した状態 bFollow && relation.getRequested(who) -> showToast(false, R.string.follow_requested) !bFollow && relation.getRequested(who) -> showToast(false, R.string.follow_request_cant_remove_by_sender) // ローカル操作成功、もしくはリモートフォロー成功 else -> callback() } showColumnMatchAccount(accessInfo) } bFollow && who.locked && (result.response?.code ?: -1) == 422 -> showToast(false, R.string.cant_follow_locked_user) else -> showToast(false, result.error) } } } } // acct で指定したユーザをリモートフォローする private fun ActMain.followRemote( accessInfo: SavedAccount, acct: Acct, locked: Boolean, bConfirmed: Boolean = false, callback: () -> Unit = {}, ) { val activity = this@followRemote if (accessInfo.isMe(acct)) { showToast(false, R.string.it_is_you) return } if (!bConfirmed) { if (locked) { DlgConfirm.open( activity, getString( R.string.confirm_follow_request_who_from, AcctColor.getNickname(acct), AcctColor.getNickname(accessInfo) ), object : DlgConfirm.Callback { override fun onOK() { followRemote( accessInfo, acct, locked, bConfirmed = true, //CHANGE callback = callback ) } override var isConfirmEnabled: Boolean get() = accessInfo.confirm_follow_locked set(value) { accessInfo.confirm_follow_locked = value accessInfo.saveSetting() reloadAccountSetting(accessInfo) } }) return } else { DlgConfirm.open( activity, getString( R.string.confirm_follow_who_from, AcctColor.getNickname(acct), AcctColor.getNickname(accessInfo) ), object : DlgConfirm.Callback { override fun onOK() { followRemote( accessInfo, acct, locked, bConfirmed = true, //CHANGE callback = callback ) } override var isConfirmEnabled: Boolean get() = accessInfo.confirm_follow set(value) { accessInfo.confirm_follow = value accessInfo.saveSetting() reloadAccountSetting(accessInfo) } }) return } } launchMain { var resultRelation: UserRelation? = null runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> val parser = TootParser(this, accessInfo) val (r2, ar) = client.syncAccountByAcct(accessInfo, acct) val user = ar?.get() ?: return@runApiTask r2 val userId = user.id if (accessInfo.isMisskey) { client.request( "/api/following/create", accessInfo.putMisskeyApiToken().apply { put("userId", userId) }.toPostRequestBuilder() ).also { result -> if (result?.error?.contains("already following") == true || result?.error?.contains("already not following") == true ) { // DBから読み直して値を変更する resultRelation = UserRelation.load(accessInfo.db_id, userId) .apply { following = true } } else { // parserに残ってるRelationをDBに保存する parser.account(result?.jsonObject)?.let { resultRelation = accessInfo.saveUserRelationMisskey(it.id, parser) } } } } else { client.request( "/api/v1/accounts/$userId/follow", "".toFormRequestBody().toPost() )?.also { result -> parseItem(::TootRelationShip, parser, result.jsonObject)?.let { resultRelation = accessInfo.saveUserRelation(it) } } } }?.let { result -> when { resultRelation != null -> { callback() showColumnMatchAccount(accessInfo) } locked && (result.response?.code ?: -1) == 422 -> showToast(false, R.string.cant_follow_locked_user) else -> showToast(false, result.error) } } } } fun ActMain.followFromAnotherAccount( pos: Int, accessInfo: SavedAccount, account: TootAccount?, bConfirmMoved: Boolean = false, ) { account ?: return if (!bConfirmMoved && account.moved != null) { AlertDialog.Builder(this) .setMessage( getString( R.string.jump_moved_user, accessInfo.getFullAcct(account), accessInfo.getFullAcct(account.moved) ) ) .setPositiveButton(R.string.ok) { _, _ -> userProfileFromAnotherAccount(pos, accessInfo, account.moved) } .setNeutralButton(R.string.ignore_suggestion) { _, _ -> followFromAnotherAccount( pos, accessInfo, account, bConfirmMoved = true //CHANGED ) } .setNegativeButton(android.R.string.cancel, null) .show() return } val whoAcct = accessInfo.getFullAcct(account) launchMain { pickAccount( bAuto = false, message = getString(R.string.account_picker_follow), accountListArg = accountListNonPseudo(account.apiHost) )?.let { followRemote( it, whoAcct, account.locked, callback = followCompleteCallback ) } } } fun ActMain.followRequestAuthorize( accessInfo: SavedAccount, whoRef: TootAccountRef, bAllow: Boolean, ) { val who = whoRef.get() if (accessInfo.isMe(who)) { showToast(false, R.string.it_is_you) return } launchMain { runApiTask(accessInfo) { client -> val parser = TootParser(this, accessInfo) if (accessInfo.isMisskey) { client.request( "/api/following/requests/${if (bAllow) "accept" else "reject"}", accessInfo.putMisskeyApiToken().apply { put("userId", who.id) } .toPostRequestBuilder() ).also { result -> val user = parser.account(result?.jsonObject) if (user != null) { // parserに残ってるRelationをDBに保存する accessInfo.saveUserRelationMisskey(user.id, parser) } // 読めなくてもエラー処理は行わない } } else { client.request( "/api/v1/follow_requests/${who.id}/${if (bAllow) "authorize" else "reject"}", "".toFormRequestBody().toPost() )?.also { result -> // Mastodon 3.0.0 から更新されたリレーションを返す // https//github.com/tootsuite/mastodon/pull/11800 val newRelation = parseItem(::TootRelationShip, parser, result.jsonObject) accessInfo.saveUserRelation(newRelation) // 読めなくてもエラー処理は行わない } } }?.let { result -> when (result.jsonObject) { null -> showToast(false, result.error) else -> { for (column in appState.columnList) { column.removeUser(accessInfo, ColumnType.FOLLOW_REQUESTS, who.id) // 他のカラムでもフォロー状態の表示更新が必要 if (column.accessInfo == accessInfo && column.type != ColumnType.FOLLOW_REQUESTS ) { column.fireRebindAdapterItems() } } showToast( false, if (bAllow) R.string.follow_request_authorized else R.string.follow_request_rejected, whoRef.decoded_display_name ) } } } } } fun ActMain.followRequestDelete( pos: Int, accessInfo: SavedAccount, whoRef: TootAccountRef, bConfirmed: Boolean = false, callback: () -> Unit = {}, ) { if (!accessInfo.isMisskey) { follow( pos, accessInfo, whoRef, bFollow = false, bConfirmed = bConfirmed, callback = callback ) return } val who = whoRef.get() if (accessInfo.isMe(who)) { showToast(false, R.string.it_is_you) return } if (!bConfirmed) { DlgConfirm.openSimple( this, getString( R.string.confirm_cancel_follow_request_who_from, whoRef.decoded_display_name, AcctColor.getNickname(accessInfo) ) ) { followRequestDelete( pos, accessInfo, whoRef, bConfirmed = true, // CHANGED callback = callback ) } return } launchMain { var resultRelation: UserRelation? = null runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> if (!accessInfo.isMisskey) { TootApiResult("Mastodon has no API to cancel follow request") } else { val parser = TootParser(this, accessInfo) var userId: EntityId = who.id // リモートユーザの同期 if (who.isRemote) { val (result, ar) = client.syncAccountByAcct(accessInfo, who.acct) val user = ar?.get() ?: return@runApiTask result userId = user.id } client.request( "/api/following/requests/cancel", accessInfo.putMisskeyApiToken().apply { put("userId", userId) } .toPostRequestBuilder() )?.also { result -> parser.account(result.jsonObject)?.let { // parserに残ってるRelationをDBに保存する resultRelation = accessInfo.saveUserRelationMisskey(it.id, parser) } } } }?.let { result -> when (resultRelation) { null -> showToast(false, result.error) else -> { // ローカル操作成功、もしくはリモートフォロー成功 callback() showColumnMatchAccount(accessInfo) } } } } }