2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.api
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import android.content.SharedPreferences
|
2018-08-19 06:19:33 +02:00
|
|
|
|
import jp.juggler.subwaytooter.*
|
2018-11-27 17:53:27 +01:00
|
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
2018-10-07 19:57:19 +02:00
|
|
|
|
import jp.juggler.subwaytooter.table.ClientInfo
|
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.util.*
|
2018-12-01 00:02:18 +01:00
|
|
|
|
import jp.juggler.util.*
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import okhttp3.*
|
2018-08-28 14:25:45 +02:00
|
|
|
|
import java.util.*
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import java.util.regex.Pattern
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
class TootApiClient(
|
2018-01-12 10:01:25 +01:00
|
|
|
|
internal val context : Context,
|
2018-03-18 06:42:44 +01:00
|
|
|
|
internal val httpClient : SimpleHttpClient = SimpleHttpClientImpl(
|
|
|
|
|
context,
|
2018-09-01 02:49:57 +02:00
|
|
|
|
App1.ok_http_client
|
2018-03-18 06:42:44 +01:00
|
|
|
|
),
|
2018-01-12 10:01:25 +01:00
|
|
|
|
internal val callback : TootApiCallback
|
2018-01-04 19:52:25 +01:00
|
|
|
|
) {
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// 認証に関する設定を保存する
|
|
|
|
|
internal val pref : SharedPreferences
|
|
|
|
|
|
|
|
|
|
// インスタンスのホスト名
|
|
|
|
|
var instance : String? = null
|
|
|
|
|
|
|
|
|
|
// アカウントがある場合に使用する
|
|
|
|
|
var account : SavedAccount? = null
|
|
|
|
|
set(value) {
|
2020-02-01 19:28:16 +01:00
|
|
|
|
instance = value?.hostAscii
|
2018-01-12 10:01:25 +01:00
|
|
|
|
field = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var currentCallCallback : CurrentCallCallback?
|
|
|
|
|
get() = httpClient.currentCallCallback
|
|
|
|
|
set(value) {
|
|
|
|
|
httpClient.currentCallCallback = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
pref = Pref.pref(context)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
companion object {
|
|
|
|
|
private val log = LogCategory("TootApiClient")
|
|
|
|
|
|
|
|
|
|
private const val DEFAULT_CLIENT_NAME = "SubwayTooter"
|
2018-12-25 12:54:41 +01:00
|
|
|
|
private const val REDIRECT_URL = "subwaytooter://oauth/"
|
|
|
|
|
|
|
|
|
|
// 20181225 3=>4 client credentialの取得時にもscopeの取得が必要になった
|
2019-03-13 18:34:56 +01:00
|
|
|
|
// 20190147 4=>5 client id とユーザIDが同じだと同じアクセストークンが返ってくるので複数端末の利用で困る。
|
|
|
|
|
// AUTH_VERSIONが古いclient情報は使わない。また、インポートの対象にしない。
|
|
|
|
|
private const val AUTH_VERSION = 5
|
2018-12-25 12:54:41 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
internal const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential"
|
2018-05-11 15:42:54 +02:00
|
|
|
|
internal const val KEY_CLIENT_SCOPE = "SubwayTooterClientScope"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
private const val KEY_AUTH_VERSION = "SubwayTooterAuthVersion"
|
2019-04-25 04:06:41 +02:00
|
|
|
|
const val KEY_IS_MISSKEY = "isMisskey" // for ClientInfo
|
|
|
|
|
const val KEY_MISSKEY_VERSION = "isMisskey" // for tokenInfo,TootInstance
|
2018-08-20 19:37:42 +02:00
|
|
|
|
const val KEY_MISSKEY_APP_SECRET = "secret"
|
2018-08-19 06:19:33 +02:00
|
|
|
|
const val KEY_API_KEY_MISSKEY = "apiKeyMisskey"
|
2018-11-03 15:21:42 +01:00
|
|
|
|
const val KEY_USER_ID = "userId"
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
private const val NO_INFORMATION = "(no information)"
|
|
|
|
|
|
2020-01-08 03:57:10 +01:00
|
|
|
|
private val reStartJsonArray = Pattern.compile("""\A\s*\[""")
|
|
|
|
|
private val reStartJsonObject = Pattern.compile("""\A\s*\{""")
|
|
|
|
|
private val reWhiteSpace = Pattern.compile("""\s+""")
|
2019-10-03 18:47:15 +02:00
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
private const val mspTokenUrl = "http://mastodonsearch.jp/api/v1.0.1/utoken"
|
|
|
|
|
private const val mspSearchUrl = "http://mastodonsearch.jp/api/v1.0.1/cross"
|
|
|
|
|
private const val mspApiKey = "e53de7f66130208f62d1808672bf6320523dcd0873dc69bc"
|
2018-01-17 18:39:16 +01:00
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
fun getMspMaxId(array : JsonArray, old : String?) : String? {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// max_id の更新
|
2020-01-07 09:03:32 +01:00
|
|
|
|
val size = array.size
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(size > 0) {
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val sv = array[size - 1].cast<JsonObject>()?.string("msp_id")?.notEmpty()
|
2020-01-07 09:03:32 +01:00
|
|
|
|
if(sv != null) return sv
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
2018-08-18 12:58:14 +02:00
|
|
|
|
// MSPでは終端は分からず、何度もリトライする
|
|
|
|
|
return old
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
fun getTootsearchHits(root : JsonObject) : JsonArray? {
|
|
|
|
|
return root["hits"].cast<JsonObject>()?.get("hits")?.cast()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// returns the number for "from" parameter of next page.
|
2018-08-18 12:58:14 +02:00
|
|
|
|
// returns null if no more next page.
|
2020-01-07 09:03:32 +01:00
|
|
|
|
fun getTootsearchMaxId(root : JsonObject, old : Long?) : Long? {
|
|
|
|
|
val size = getTootsearchHits(root)?.size ?: 0
|
2018-08-18 12:58:14 +02:00
|
|
|
|
return when {
|
|
|
|
|
size <= 0 -> null
|
|
|
|
|
else -> (old ?: 0L) + size.toLong()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
val DEFAULT_JSON_ERROR_PARSER = { json : JsonObject ->
|
|
|
|
|
json["error"]?.toString()
|
2018-11-27 17:53:27 +01:00
|
|
|
|
}
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
internal fun simplifyErrorHtml(
|
|
|
|
|
response : Response,
|
|
|
|
|
sv : String,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) : String {
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
// JsonObjectとして解釈できるならエラーメッセージを検出する
|
2018-01-12 10:01:25 +01:00
|
|
|
|
try {
|
2020-01-08 03:57:10 +01:00
|
|
|
|
val json = sv.decodeJsonObject()
|
|
|
|
|
val error_message =
|
|
|
|
|
jsonErrorParser(json)?.notEmpty()
|
|
|
|
|
if(error_message != null) {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
return error_message
|
|
|
|
|
}
|
2020-01-08 03:57:10 +01:00
|
|
|
|
}catch(_:Throwable) {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HTMLならタグの除去を試みる
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val ct = response.body?.contentType()
|
|
|
|
|
if(ct?.subtype == "html") {
|
2018-01-21 17:47:13 +01:00
|
|
|
|
val decoded = DecodeOptions().decodeHTML(sv).toString()
|
2018-01-17 18:39:16 +01:00
|
|
|
|
return reWhiteSpace.matcher(decoded).replaceAll(" ").trim()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
2020-01-08 03:57:10 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// XXX: Amazon S3 が403を返した場合にcontent-typeが?/xmlでserverがAmazonならXMLをパースしてエラーを整形することもできるが、多分必要ない
|
|
|
|
|
|
2018-01-17 18:39:16 +01:00
|
|
|
|
return reWhiteSpace.matcher(sv).replaceAll(" ").trim()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun formatResponse(
|
|
|
|
|
response : Response,
|
|
|
|
|
caption : String,
|
|
|
|
|
bodyString : String? = null,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) : String {
|
|
|
|
|
val sb = StringBuilder()
|
|
|
|
|
try {
|
|
|
|
|
// body は既に読み終わっているか、そうでなければこれから読む
|
|
|
|
|
if(bodyString != null) {
|
|
|
|
|
sb.append(simplifyErrorHtml(response, bodyString, jsonErrorParser))
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val string = response.body?.string()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(string != null) {
|
|
|
|
|
sb.append(simplifyErrorHtml(response, string, jsonErrorParser))
|
|
|
|
|
}
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.e(ex, "missing response body.")
|
|
|
|
|
sb.append("(missing response body)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(sb.isNotEmpty()) sb.append(' ')
|
2019-08-24 05:35:22 +02:00
|
|
|
|
sb.append("(HTTP ").append(response.code.toString())
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val message = response.message
|
2019-01-16 13:33:07 +01:00
|
|
|
|
if(message.isNotEmpty()) sb.append(' ').append(message)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
sb.append(")")
|
|
|
|
|
|
|
|
|
|
if(caption.isNotEmpty()) {
|
|
|
|
|
sb.append(' ').append(caption)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sb.toString().replace("\n+".toRegex(), "\n")
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
fun getScopeString(ti : TootInstance) = when {
|
2018-12-25 12:54:41 +01:00
|
|
|
|
// ti.versionGE(TootInstance.VERSION_2_7_0_rc1) -> "read+write+follow+push+create"
|
2018-05-14 16:41:13 +02:00
|
|
|
|
ti.versionGE(TootInstance.VERSION_2_4_0_rc1) -> "read+write+follow+push"
|
2018-05-11 15:42:54 +02:00
|
|
|
|
else -> "read+write+follow"
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
fun getScopeArrayMisskey(@Suppress("UNUSED_PARAMETER") ti : TootInstance) =
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonArray().apply {
|
2019-04-15 19:54:36 +02:00
|
|
|
|
if(ti.versionGE(TootInstance.MISSKEY_VERSION_11)) {
|
2019-04-15 19:57:59 +02:00
|
|
|
|
// https://github.com/syuilo/misskey/blob/master/src/server/api/kinds.ts
|
2019-04-15 19:54:36 +02:00
|
|
|
|
arrayOf(
|
|
|
|
|
"read:account",
|
|
|
|
|
"write:account",
|
|
|
|
|
"read:blocks",
|
|
|
|
|
"write:blocks",
|
|
|
|
|
"read:drive",
|
|
|
|
|
"write:drive",
|
|
|
|
|
"read:favorites",
|
|
|
|
|
"write:favorites",
|
|
|
|
|
"read:following",
|
|
|
|
|
"write:following",
|
|
|
|
|
"read:messaging",
|
|
|
|
|
"write:messaging",
|
|
|
|
|
"read:mutes",
|
|
|
|
|
"write:mutes",
|
|
|
|
|
"write:notes",
|
|
|
|
|
"read:notifications",
|
|
|
|
|
"write:notifications",
|
|
|
|
|
"read:reactions",
|
|
|
|
|
"write:reactions",
|
|
|
|
|
"write:votes"
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
// https://github.com/syuilo/misskey/issues/2341
|
|
|
|
|
arrayOf(
|
|
|
|
|
"account-read",
|
|
|
|
|
"account-write",
|
|
|
|
|
"account/read",
|
|
|
|
|
"account/write",
|
|
|
|
|
"drive-read",
|
|
|
|
|
"drive-write",
|
|
|
|
|
"favorite-read",
|
|
|
|
|
"favorite-write",
|
|
|
|
|
"favorites-read",
|
|
|
|
|
"following-read",
|
|
|
|
|
"following-write",
|
|
|
|
|
"messaging-read",
|
|
|
|
|
"messaging-write",
|
|
|
|
|
"note-read",
|
|
|
|
|
"note-write",
|
|
|
|
|
"notification-read",
|
|
|
|
|
"notification-write",
|
|
|
|
|
"reaction-read",
|
|
|
|
|
"reaction-write",
|
|
|
|
|
"vote-read",
|
|
|
|
|
"vote-write"
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
}
|
2019-02-06 02:43:55 +01:00
|
|
|
|
// APIのエラーを回避するため、重複を排除する
|
|
|
|
|
.toMutableSet()
|
2020-01-07 09:03:32 +01:00
|
|
|
|
.forEach { add(it) }
|
2018-08-19 06:19:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
private fun encodeScopeArray(scope_array : JsonArray?) : String? {
|
2018-08-30 15:06:23 +02:00
|
|
|
|
scope_array ?: return null
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val list = scope_array.stringArrayList()
|
2018-08-30 15:06:23 +02:00
|
|
|
|
list.sort()
|
|
|
|
|
return list.joinToString(",")
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
private fun compareScopeArray(a : JsonArray, b : JsonArray?) : Boolean {
|
2018-08-30 15:06:23 +02:00
|
|
|
|
return encodeScopeArray(a) == encodeScopeArray(b)
|
|
|
|
|
}
|
2019-04-25 04:06:41 +02:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Suppress("unused")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
internal val isApiCancelled : Boolean
|
2018-01-04 19:52:25 +01:00
|
|
|
|
get() = callback.isApiCancelled
|
|
|
|
|
|
|
|
|
|
fun publishApiProgress(s : String) {
|
|
|
|
|
callback.publishApiProgress(s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun publishApiProgressRatio(value : Int, max : Int) {
|
|
|
|
|
callback.publishApiProgressRatio(value, max)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
// ユーティリティ
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// リクエストをokHttpに渡してレスポンスを取得する
|
|
|
|
|
internal inline fun sendRequest(
|
|
|
|
|
result : TootApiResult,
|
|
|
|
|
progressPath : String? = null,
|
2018-09-01 02:49:57 +02:00
|
|
|
|
tmpOkhttpClient : OkHttpClient? = null,
|
2018-01-12 10:01:25 +01:00
|
|
|
|
block : () -> Request
|
|
|
|
|
) : Boolean {
|
|
|
|
|
return try {
|
|
|
|
|
result.response = null
|
|
|
|
|
result.bodyString = null
|
|
|
|
|
result.data = null
|
|
|
|
|
|
|
|
|
|
val request = block()
|
|
|
|
|
|
2019-09-27 04:30:48 +02:00
|
|
|
|
result.requestInfo = "${request.method} ${progressPath ?: request.url.encodedPath}"
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
callback.publishApiProgress(
|
|
|
|
|
context.getString(
|
|
|
|
|
R.string.request_api
|
2019-08-24 05:35:22 +02:00
|
|
|
|
, request.method
|
|
|
|
|
, progressPath ?: request.url.encodedPath
|
2018-01-12 10:01:25 +01:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2018-12-06 22:38:49 +01:00
|
|
|
|
val response = httpClient.getResponse(request, tmpOkhttpClient = tmpOkhttpClient)
|
|
|
|
|
result.response = response
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
null == result.error
|
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
result.setError(
|
|
|
|
|
"${result.caption}: ${ex.withCaption(
|
|
|
|
|
context.resources,
|
|
|
|
|
R.string.network_error
|
|
|
|
|
)}"
|
|
|
|
|
)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
false
|
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// レスポンスがエラーかボディがカラならエラー状態を設定する
|
|
|
|
|
// 例外を出すかも
|
|
|
|
|
internal fun readBodyString(
|
|
|
|
|
result : TootApiResult,
|
|
|
|
|
progressPath : String? = null,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) : String? {
|
|
|
|
|
|
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
val response = result.response !!
|
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val request = response.request
|
2019-01-16 13:33:07 +01:00
|
|
|
|
publishApiProgress(
|
|
|
|
|
context.getString(
|
|
|
|
|
R.string.reading_api,
|
2019-08-24 05:35:22 +02:00
|
|
|
|
request.method,
|
2019-01-16 13:33:07 +01:00
|
|
|
|
progressPath ?: result.caption
|
2018-01-21 13:46:36 +01:00
|
|
|
|
)
|
2019-01-16 13:33:07 +01:00
|
|
|
|
)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val bodyString = response.body?.string()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
// Misskey の /api/notes/favorites/create は 204(no content)を返す。ボディはカラになる。
|
2019-08-24 05:35:22 +02:00
|
|
|
|
if(bodyString?.isEmpty() != false && response.code in 200 until 300) {
|
2018-08-20 19:37:42 +02:00
|
|
|
|
result.bodyString = ""
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(! response.isSuccessful || bodyString?.isEmpty() != false) {
|
|
|
|
|
|
2019-04-25 04:06:41 +02:00
|
|
|
|
result.error = formatResponse(
|
2018-01-12 10:01:25 +01:00
|
|
|
|
response,
|
|
|
|
|
result.caption,
|
|
|
|
|
if(bodyString?.isNotEmpty() == true) bodyString else NO_INFORMATION,
|
|
|
|
|
jsonErrorParser
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return if(result.error != null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
publishApiProgress(context.getString(R.string.parsing_response))
|
|
|
|
|
result.bodyString = bodyString
|
|
|
|
|
bodyString
|
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-10 00:12:40 +01:00
|
|
|
|
// レスポンスがエラーかボディがカラならエラー状態を設定する
|
|
|
|
|
// 例外を出すかも
|
2018-05-11 15:42:54 +02:00
|
|
|
|
private fun readBodyBytes(
|
2018-03-10 00:12:40 +01:00
|
|
|
|
result : TootApiResult,
|
|
|
|
|
progressPath : String? = null,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-03-10 00:12:40 +01:00
|
|
|
|
) : ByteArray? {
|
|
|
|
|
|
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
val response = result.response !!
|
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val request = response.request
|
2019-01-16 14:46:56 +01:00
|
|
|
|
publishApiProgress(
|
|
|
|
|
context.getString(
|
|
|
|
|
R.string.reading_api,
|
2019-08-24 05:35:22 +02:00
|
|
|
|
request.method,
|
2019-01-16 14:46:56 +01:00
|
|
|
|
progressPath ?: result.caption
|
2018-03-10 00:12:40 +01:00
|
|
|
|
)
|
2019-01-16 14:46:56 +01:00
|
|
|
|
)
|
2018-03-10 00:12:40 +01:00
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val bodyBytes = response.body?.bytes()
|
2018-03-10 00:12:40 +01:00
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
if(! response.isSuccessful || bodyBytes?.isEmpty() != false) {
|
|
|
|
|
|
2019-04-25 04:06:41 +02:00
|
|
|
|
result.error = formatResponse(
|
2018-03-10 00:12:40 +01:00
|
|
|
|
response,
|
|
|
|
|
result.caption,
|
|
|
|
|
if(bodyBytes?.isNotEmpty() == true) bodyBytes.decodeUTF8() else NO_INFORMATION,
|
|
|
|
|
jsonErrorParser
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return if(result.error != null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
result.bodyString = "(binary data)"
|
|
|
|
|
result.data = bodyBytes
|
|
|
|
|
bodyBytes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
private fun parseBytes(
|
2018-03-10 00:12:40 +01:00
|
|
|
|
result : TootApiResult,
|
|
|
|
|
progressPath : String? = null,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-03-10 00:12:40 +01:00
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
|
|
|
|
|
val response = result.response !! // nullにならないはず
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
readBodyBytes(result, progressPath, jsonErrorParser)
|
|
|
|
|
?: return if(isApiCancelled) null else result
|
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
result.error =
|
|
|
|
|
formatResponse(response, result.caption, result.bodyString ?: NO_INFORMATION)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
internal fun parseString(
|
|
|
|
|
result : TootApiResult,
|
|
|
|
|
progressPath : String? = null,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val response = result.response !! // nullにならないはず
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val bodyString = readBodyString(result, progressPath, jsonErrorParser)
|
|
|
|
|
?: return if(isApiCancelled) null else result
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
result.data = bodyString
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
2018-01-21 13:46:36 +01:00
|
|
|
|
result.error =
|
|
|
|
|
formatResponse(response, result.caption, result.bodyString ?: NO_INFORMATION)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
2018-01-12 10:01:25 +01:00
|
|
|
|
return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// レスポンスからJSONデータを読む
|
|
|
|
|
internal fun parseJson(
|
|
|
|
|
result : TootApiResult,
|
|
|
|
|
progressPath : String? = null,
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonErrorParser : (json : JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) : TootApiResult? // 引数に指定したresultそのものか、キャンセルされたらnull
|
|
|
|
|
{
|
|
|
|
|
val response = result.response !! // nullにならないはず
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
try {
|
2018-08-21 12:19:02 +02:00
|
|
|
|
var bodyString = readBodyString(result, progressPath, jsonErrorParser)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
?: return if(isApiCancelled) null else result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-08-30 14:33:22 +02:00
|
|
|
|
if(bodyString.isEmpty()) {
|
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
// 204 no content は 空オブジェクトと解釈する
|
2020-01-07 09:03:32 +01:00
|
|
|
|
result.data = JsonObject()
|
2018-08-30 14:33:22 +02:00
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
} else if(reStartJsonArray.matcher(bodyString).find()) {
|
2020-01-08 03:57:10 +01:00
|
|
|
|
result.data = bodyString.decodeJsonArray()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
} else if(reStartJsonObject.matcher(bodyString).find()) {
|
2020-01-08 03:57:10 +01:00
|
|
|
|
val json = bodyString.decodeJsonObject()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val error_message = jsonErrorParser(json)
|
|
|
|
|
if(error_message != null) {
|
|
|
|
|
result.error = error_message
|
|
|
|
|
} else {
|
|
|
|
|
result.data = json
|
|
|
|
|
}
|
2018-08-30 14:33:22 +02:00
|
|
|
|
} else {
|
2018-08-21 12:19:02 +02:00
|
|
|
|
// HTMLならタグを除去する
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val ct = response.body?.contentType()
|
|
|
|
|
if(ct?.subtype == "html") {
|
2019-09-27 04:30:48 +02:00
|
|
|
|
val decoded = DecodeOptions().decodeHTML(bodyString).toString()
|
|
|
|
|
.replace("""[\s ]+""".toRegex(), " ")
|
2018-08-30 14:33:22 +02:00
|
|
|
|
bodyString = decoded
|
2018-08-21 12:19:02 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val sb = StringBuilder()
|
|
|
|
|
.append(context.getString(R.string.response_not_json))
|
|
|
|
|
.append(' ')
|
|
|
|
|
.append(bodyString)
|
2018-08-30 14:33:22 +02:00
|
|
|
|
|
2018-08-21 12:19:02 +02:00
|
|
|
|
if(sb.isNotEmpty()) sb.append(' ')
|
2019-08-24 05:35:22 +02:00
|
|
|
|
sb.append("(HTTP ").append(response.code.toString())
|
2018-08-21 12:19:02 +02:00
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val message = response.message
|
2019-01-16 13:33:07 +01:00
|
|
|
|
if(message.isNotEmpty()) sb.append(' ').append(message)
|
2019-01-16 14:46:56 +01:00
|
|
|
|
|
2018-08-21 12:19:02 +02:00
|
|
|
|
sb.append(")")
|
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val url = response.request.url.toString()
|
2019-01-16 13:33:07 +01:00
|
|
|
|
if(url.isNotEmpty()) sb.append(' ').append(url)
|
2018-08-21 12:19:02 +02:00
|
|
|
|
|
|
|
|
|
result.error = sb.toString()
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
2018-01-21 13:46:36 +01:00
|
|
|
|
result.error =
|
|
|
|
|
formatResponse(response, result.caption, result.bodyString ?: NO_INFORMATION)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
fun request(
|
|
|
|
|
path : String,
|
|
|
|
|
request_builder : Request.Builder = Request.Builder()
|
|
|
|
|
) : TootApiResult? {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
2018-11-27 17:53:27 +01:00
|
|
|
|
val account = this.account // may null
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if(! sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
|
|
|
|
log.d("request: $path")
|
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
request_builder.url("https://$instance$path")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
2018-11-27 17:53:27 +01:00
|
|
|
|
val access_token = account?.getAccessToken()
|
2018-01-21 13:46:36 +01:00
|
|
|
|
if(access_token?.isNotEmpty() == true) {
|
2018-05-11 15:42:54 +02:00
|
|
|
|
request_builder.header("Authorization", "Bearer $access_token")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request_builder.build()
|
|
|
|
|
|
|
|
|
|
}) return result
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
return parseJson(result)
|
|
|
|
|
} finally {
|
|
|
|
|
val error = result.error
|
|
|
|
|
if(error != null) log.d("error: $error")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
// misskey authentication
|
2019-10-03 18:47:15 +02:00
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
private fun getAppInfoMisskey(appId : String?) : TootApiResult? {
|
|
|
|
|
appId ?: return TootApiResult("missing app id")
|
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
if(sendRequest(result) {
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonObject().apply {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
put("appId", appId)
|
|
|
|
|
}
|
|
|
|
|
.toPostRequestBuilder()
|
|
|
|
|
.url("https://$instance/api/app/show")
|
|
|
|
|
.build()
|
|
|
|
|
}) {
|
|
|
|
|
parseJson(result) ?: return null
|
|
|
|
|
result.jsonObject?.put(KEY_IS_MISSKEY, true)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-30 14:33:22 +02:00
|
|
|
|
private fun prepareBrowserUrlMisskey(appSecret : String) : String? {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
|
|
|
|
|
if(result.error != null) {
|
|
|
|
|
showToast(context, false, result.error)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
|
2018-08-16 21:58:30 +02:00
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
if(! sendRequest(result) {
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonObject().apply {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
put("appSecret", appSecret)
|
|
|
|
|
}
|
|
|
|
|
.toPostRequestBuilder()
|
|
|
|
|
.url("https://$instance/api/auth/session/generate")
|
|
|
|
|
.build()
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
val error = result.error
|
|
|
|
|
if(error != null) {
|
|
|
|
|
showToast(context, false, error)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parseJson(result) ?: return null
|
|
|
|
|
|
|
|
|
|
val jsonObject = result.jsonObject
|
|
|
|
|
if(jsonObject == null) {
|
|
|
|
|
showToast(context, false, result.error)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
// {"token":"0ba88e2d-4b7d-4599-8d90-dc341a005637","url":"https://misskey.xyz/auth/0ba88e2d-4b7d-4599-8d90-dc341a005637"}
|
|
|
|
|
|
|
|
|
|
// ブラウザで開くURL
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val url = jsonObject.string("url")
|
2018-08-19 06:19:33 +02:00
|
|
|
|
if(url?.isEmpty() != false) {
|
|
|
|
|
showToast(context, false, "missing 'url' in auth session response.")
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val e = PrefDevice.prefDevice(context)
|
|
|
|
|
.edit()
|
|
|
|
|
.putString(PrefDevice.LAST_AUTH_INSTANCE, instance)
|
|
|
|
|
.putString(PrefDevice.LAST_AUTH_SECRET, appSecret)
|
|
|
|
|
|
|
|
|
|
val account = this.account
|
|
|
|
|
if(account != null) {
|
|
|
|
|
e.putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
|
|
|
|
|
} else {
|
|
|
|
|
e.remove(PrefDevice.LAST_AUTH_DB_ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.apply()
|
|
|
|
|
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun registerClientMisskey(
|
2020-01-07 09:03:32 +01:00
|
|
|
|
scope_array : JsonArray,
|
2018-08-19 06:19:33 +02:00
|
|
|
|
client_name : String
|
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
2018-08-16 21:58:30 +02:00
|
|
|
|
if(sendRequest(result) {
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonObject().apply {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
put("nameId", "SubwayTooter")
|
|
|
|
|
put("name", client_name)
|
|
|
|
|
put("description", "Android app for federated SNS")
|
|
|
|
|
put("callbackUrl", "subwaytooter://misskey/auth_callback")
|
|
|
|
|
put("permission", scope_array)
|
|
|
|
|
}
|
|
|
|
|
.toPostRequestBuilder()
|
|
|
|
|
.url("https://$instance/api/app/create")
|
|
|
|
|
.build()
|
|
|
|
|
}) {
|
|
|
|
|
parseJson(result) ?: return null
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun authentication1Misskey(clientNameArg : String, ti : TootInstance) : TootApiResult? {
|
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
val instance = result.caption // same to instance
|
|
|
|
|
|
|
|
|
|
// クライアントIDがアプリ上に保存されているか?
|
2019-01-21 00:00:59 +01:00
|
|
|
|
val client_name = clientNameArg.notEmpty() ?: DEFAULT_CLIENT_NAME
|
2018-08-19 06:19:33 +02:00
|
|
|
|
val client_info = ClientInfo.load(instance, client_name)
|
|
|
|
|
|
|
|
|
|
// スコープ一覧を取得する
|
|
|
|
|
val scope_array = getScopeArrayMisskey(ti)
|
|
|
|
|
|
2018-12-25 12:54:41 +01:00
|
|
|
|
if(client_info != null
|
2020-01-08 04:23:45 +01:00
|
|
|
|
&& AUTH_VERSION == client_info.int(KEY_AUTH_VERSION)
|
|
|
|
|
&& client_info.boolean(KEY_IS_MISSKEY) == true
|
2018-12-25 12:54:41 +01:00
|
|
|
|
) {
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val appSecret = client_info.string(KEY_MISSKEY_APP_SECRET)
|
2018-08-20 19:37:42 +02:00
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val r2 = getAppInfoMisskey(client_info.string("id"))
|
2018-08-19 06:19:33 +02:00
|
|
|
|
val tmpClientInfo = r2?.jsonObject
|
|
|
|
|
// tmpClientInfo はsecretを含まないので保存してはいけない
|
2019-08-24 05:35:22 +02:00
|
|
|
|
when {
|
|
|
|
|
// アプリが登録済みで
|
|
|
|
|
// クライアント名が一致してて
|
|
|
|
|
// パーミッションが同じ
|
|
|
|
|
tmpClientInfo != null
|
2020-01-08 04:23:45 +01:00
|
|
|
|
&& client_name == tmpClientInfo.string("name")
|
2020-01-07 09:03:32 +01:00
|
|
|
|
&& compareScopeArray(scope_array, tmpClientInfo["permission"].cast())
|
2019-08-24 05:35:22 +02:00
|
|
|
|
&& appSecret?.isNotEmpty() == true -> {
|
|
|
|
|
// クライアント情報を再利用する
|
|
|
|
|
result.data = prepareBrowserUrlMisskey(appSecret)
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
// XXX appSecretを使ってクライアント情報を削除できるようにするべきだが、該当するAPIが存在しない
|
|
|
|
|
}
|
2018-08-19 06:19:33 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val r2 = registerClientMisskey(scope_array, client_name)
|
|
|
|
|
val jsonObject = r2?.jsonObject ?: return r2
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val appSecret = jsonObject.string(KEY_MISSKEY_APP_SECRET)
|
2018-08-30 14:33:22 +02:00
|
|
|
|
if(appSecret?.isEmpty() != false) {
|
|
|
|
|
showToast(context, true, context.getString(R.string.cant_get_misskey_app_secret))
|
2018-08-20 19:37:42 +02:00
|
|
|
|
return null
|
|
|
|
|
}
|
2018-08-19 06:19:33 +02:00
|
|
|
|
// {
|
|
|
|
|
// "createdAt": "2018-08-19T00:43:10.105Z",
|
|
|
|
|
// "userId": null,
|
|
|
|
|
// "name": "Via芸",
|
|
|
|
|
// "nameId": "test1",
|
|
|
|
|
// "description": "test1",
|
|
|
|
|
// "permission": [
|
|
|
|
|
// "account-read",
|
|
|
|
|
// "account-write",
|
|
|
|
|
// "note-write",
|
|
|
|
|
// "reaction-write",
|
|
|
|
|
// "following-write",
|
|
|
|
|
// "drive-read",
|
|
|
|
|
// "drive-write",
|
|
|
|
|
// "notification-read",
|
|
|
|
|
// "notification-write"
|
|
|
|
|
// ],
|
|
|
|
|
// "callbackUrl": "test1://test1/auth_callback",
|
|
|
|
|
// "id": "5b78bd1ea0db0527f25815c3",
|
|
|
|
|
// "iconUrl": "https://misskey.xyz/files/app-default.jpg"
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// 2018/8/19現在、/api/app/create のレスポンスにsecretが含まれないので認証に使えない
|
|
|
|
|
// https://github.com/syuilo/misskey/issues/2343
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
jsonObject[KEY_IS_MISSKEY] = true
|
|
|
|
|
jsonObject[KEY_AUTH_VERSION] = AUTH_VERSION
|
2018-08-19 06:19:33 +02:00
|
|
|
|
ClientInfo.save(instance, client_name, jsonObject.toString())
|
2018-08-20 19:37:42 +02:00
|
|
|
|
result.data = prepareBrowserUrlMisskey(appSecret)
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// oAuth2認証の続きを行う
|
2019-08-24 05:35:22 +02:00
|
|
|
|
fun authentication2Misskey(
|
|
|
|
|
clientNameArg : String,
|
|
|
|
|
token : String,
|
|
|
|
|
misskeyVersion : Int
|
|
|
|
|
) : TootApiResult? {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
val instance = result.caption // same to instance
|
2019-01-21 00:00:59 +01:00
|
|
|
|
val client_name = clientNameArg.notEmpty() ?: DEFAULT_CLIENT_NAME
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
|
|
|
|
@Suppress("UNUSED_VARIABLE")
|
|
|
|
|
val client_info = ClientInfo.load(instance, client_name)
|
|
|
|
|
?: return result.setError("missing client id")
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val appSecret = client_info.string(KEY_MISSKEY_APP_SECRET)
|
2018-08-30 14:33:22 +02:00
|
|
|
|
if(appSecret?.isEmpty() != false) {
|
2018-08-20 19:37:42 +02:00
|
|
|
|
return result.setError(context.getString(R.string.cant_get_misskey_app_secret))
|
|
|
|
|
}
|
2018-08-30 14:33:22 +02:00
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
if(! sendRequest(result) {
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonObject().apply {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
put("appSecret", appSecret)
|
|
|
|
|
put("token", token)
|
|
|
|
|
}
|
|
|
|
|
.toPostRequestBuilder()
|
|
|
|
|
.url("https://$instance/api/auth/session/userkey")
|
|
|
|
|
.build()
|
2018-08-16 21:58:30 +02:00
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
parseJson(result) ?: return null
|
|
|
|
|
|
|
|
|
|
val token_info = result.jsonObject ?: return result
|
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
// {"accessToken":"...","user":{…}}
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val access_token = token_info.string("accessToken")
|
2018-08-19 06:19:33 +02:00
|
|
|
|
if(access_token?.isEmpty() != false) {
|
|
|
|
|
return result.setError("missing accessToken in the response.")
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 09:03:32 +01:00
|
|
|
|
val user = token_info["user"].cast<JsonObject>()
|
2018-11-03 15:21:42 +01:00
|
|
|
|
?: return result.setError("missing user in the response.")
|
2018-11-27 17:53:27 +01:00
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
token_info.remove("user")
|
|
|
|
|
|
|
|
|
|
val apiKey = "$access_token$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
|
|
|
|
|
|
|
|
|
|
// ユーザ情報を読めたならtokenInfoを保存する
|
2020-01-08 04:23:45 +01:00
|
|
|
|
EntityId.mayNull(user.string("id"))?.putTo(token_info, KEY_USER_ID)
|
2020-01-07 09:03:32 +01:00
|
|
|
|
token_info[KEY_MISSKEY_VERSION] = misskeyVersion
|
|
|
|
|
token_info[KEY_AUTH_VERSION] = AUTH_VERSION
|
|
|
|
|
token_info[KEY_API_KEY_MISSKEY] = apiKey
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
|
|
|
|
// tokenInfoとユーザ情報の入ったresultを返す
|
|
|
|
|
result.tokenInfo = token_info
|
|
|
|
|
result.data = user
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// クライアントをタンスに登録
|
2018-08-30 05:23:13 +02:00
|
|
|
|
fun registerClient(scope_string : String, clientName : String) : TootApiResult? {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
val instance = result.caption // same to instance
|
|
|
|
|
// OAuth2 クライアント登録
|
|
|
|
|
if(! sendRequest(result) {
|
2018-12-02 11:25:00 +01:00
|
|
|
|
("client_name=" + clientName.encodePercent()
|
|
|
|
|
+ "&redirect_uris=" + REDIRECT_URL.encodePercent()
|
|
|
|
|
+ "&scopes=$scope_string"
|
2019-08-24 05:35:22 +02:00
|
|
|
|
).toFormRequestBody().toPost()
|
2018-01-21 13:46:36 +01:00
|
|
|
|
.url("https://$instance/api/v1/apps")
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
return parseJson(result)
|
|
|
|
|
}
|
2018-01-13 07:15:52 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
// クライアントアプリの登録を確認するためのトークンを生成する
|
|
|
|
|
// oAuth2 Client Credentials の取得
|
|
|
|
|
// https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow
|
|
|
|
|
// このトークンはAPIを呼び出すたびに新しく生成される…
|
2020-01-07 09:03:32 +01:00
|
|
|
|
internal fun getClientCredential(client_info : JsonObject) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
2018-01-10 16:47:35 +01:00
|
|
|
|
if(result.error != null) return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(! sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val client_id = client_info.string("client_id")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
?: return result.setError("missing client_id")
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val client_secret = client_info.string("client_secret")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
?: return result.setError("missing client_secret")
|
|
|
|
|
|
2018-12-25 12:54:41 +01:00
|
|
|
|
"grant_type=client_credentials&scope=read+write&client_id=${client_id.encodePercent()}&client_secret=${client_secret.encodePercent()}"
|
2019-08-24 05:35:22 +02:00
|
|
|
|
.toFormRequestBody().toPost()
|
2018-12-02 11:25:00 +01:00
|
|
|
|
.url("https://$instance/oauth/token")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val r2 = parseJson(result)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val jsonObject = r2?.jsonObject ?: return r2
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2018-12-25 12:54:41 +01:00
|
|
|
|
log.d("getClientCredential: ${jsonObject}")
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val sv = jsonObject.string("access_token")?.notEmpty()
|
2020-01-07 09:03:32 +01:00
|
|
|
|
if(sv != null ) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
result.data = sv
|
|
|
|
|
} else {
|
|
|
|
|
result.data = null
|
2018-01-12 10:01:25 +01:00
|
|
|
|
result.error = "missing client credential."
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// client_credentialがまだ有効か調べる
|
2018-01-12 10:01:25 +01:00
|
|
|
|
internal fun verifyClientCredential(client_credential : String) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(! sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
Request.Builder()
|
|
|
|
|
.url("https://$instance/api/v1/apps/verify_credentials")
|
|
|
|
|
.header("Authorization", "Bearer $client_credential")
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
return parseJson(result)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
// client_credentialを無効にする
|
|
|
|
|
private fun revokeClientCredential(
|
2020-01-07 09:03:32 +01:00
|
|
|
|
client_info : JsonObject,
|
2018-05-11 15:42:54 +02:00
|
|
|
|
client_credential : String
|
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val client_id = client_info.string("client_id")
|
2018-05-11 15:42:54 +02:00
|
|
|
|
?: return result.setError("missing client_id")
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val client_secret = client_info.string("client_secret")
|
2018-05-11 15:42:54 +02:00
|
|
|
|
?: return result.setError("missing client_secret")
|
|
|
|
|
|
|
|
|
|
if(! sendRequest(result) {
|
2018-12-02 11:25:00 +01:00
|
|
|
|
("token=" + client_credential.encodePercent()
|
|
|
|
|
+ "&client_id=" + client_id.encodePercent()
|
|
|
|
|
+ "&client_secret=" + client_secret.encodePercent()
|
2019-08-24 05:35:22 +02:00
|
|
|
|
).toFormRequestBody().toPost()
|
2018-05-11 15:42:54 +02:00
|
|
|
|
.url("https://$instance/oauth/revoke")
|
|
|
|
|
.build()
|
2018-12-02 11:25:00 +01:00
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
}) return result
|
|
|
|
|
|
|
|
|
|
return parseJson(result)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
// 認証ページURLを作る
|
2020-01-07 09:03:32 +01:00
|
|
|
|
internal fun prepareBrowserUrl(scope_string : String, client_info : JsonObject) : String? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val account = this.account
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val client_id = client_info.string("client_id") ?: return null
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
val state = StringBuilder()
|
|
|
|
|
.append((if(account != null) "db:${account.db_id}" else "host:$instance"))
|
|
|
|
|
.append(',')
|
|
|
|
|
.append("random:${System.currentTimeMillis()}")
|
|
|
|
|
.toString()
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
return ("https://" + instance + "/oauth/authorize"
|
2018-01-21 13:46:36 +01:00
|
|
|
|
+ "?client_id=" + client_id.encodePercent()
|
2018-01-04 19:52:25 +01:00
|
|
|
|
+ "&response_type=code"
|
2018-01-21 13:46:36 +01:00
|
|
|
|
+ "&redirect_uri=" + REDIRECT_URL.encodePercent()
|
2018-05-11 15:42:54 +02:00
|
|
|
|
+ "&scope=$scope_string"
|
|
|
|
|
+ "&scopes=$scope_string"
|
2019-03-13 18:34:56 +01:00
|
|
|
|
+ "&state=" + state.encodePercent()
|
2018-01-04 19:52:25 +01:00
|
|
|
|
+ "&grant_type=authorization_code"
|
|
|
|
|
+ "&approval_prompt=force"
|
2018-10-30 22:47:02 +01:00
|
|
|
|
+ "&force_login=true"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
// +"&access_type=offline"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
private fun prepareClientMastodon(
|
|
|
|
|
clientNameArg : String,
|
|
|
|
|
ti : TootInstance,
|
|
|
|
|
forceUpdateClient : Boolean = false
|
|
|
|
|
) : TootApiResult? {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
// 前準備
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val instance = result.caption // same to instance
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// クライアントIDがアプリ上に保存されているか?
|
2019-01-21 00:00:59 +01:00
|
|
|
|
val client_name = clientNameArg.notEmpty() ?: DEFAULT_CLIENT_NAME
|
2018-12-25 12:54:41 +01:00
|
|
|
|
var client_info = ClientInfo.load(instance, client_name)
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
|
|
|
|
// スコープ一覧を取得する
|
|
|
|
|
val scope_string = getScopeString(ti)
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
when {
|
2020-01-08 04:23:45 +01:00
|
|
|
|
AUTH_VERSION != client_info?.int(KEY_AUTH_VERSION) -> {
|
2019-03-13 18:34:56 +01:00
|
|
|
|
// 古いクライアント情報は使わない。削除もしない。
|
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
client_info.boolean(KEY_IS_MISSKEY) == true -> {
|
2019-03-13 18:34:56 +01:00
|
|
|
|
// Misskeyにはclient情報をまだ利用できるかどうか調べる手段がないので、再利用しない
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
else -> {
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val old_scope = client_info.string(KEY_CLIENT_SCOPE)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
|
|
|
|
|
// client_credential をまだ取得していないなら取得する
|
2020-01-08 04:23:45 +01:00
|
|
|
|
var client_credential = client_info.string(KEY_CLIENT_CREDENTIAL)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
if(client_credential?.isEmpty() != false) {
|
|
|
|
|
val resultSub = getClientCredential(client_info)
|
|
|
|
|
client_credential = resultSub?.string
|
|
|
|
|
if(client_credential?.isNotEmpty() == true) {
|
|
|
|
|
try {
|
2020-01-07 09:03:32 +01:00
|
|
|
|
client_info[KEY_CLIENT_CREDENTIAL] = client_credential
|
2019-03-13 18:34:56 +01:00
|
|
|
|
ClientInfo.save(instance, client_name, client_info.toString())
|
2020-01-07 09:03:32 +01:00
|
|
|
|
} catch(ignored : JsonException) {
|
2019-03-13 18:34:56 +01:00
|
|
|
|
}
|
2018-12-25 12:54:41 +01:00
|
|
|
|
}
|
2019-03-13 18:34:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// client_credential があるならcredentialがまだ使えるか確認する
|
|
|
|
|
if(client_credential?.isNotEmpty() == true) {
|
|
|
|
|
val resultSub = verifyClientCredential(client_credential)
|
|
|
|
|
val currentCC = resultSub?.jsonObject
|
|
|
|
|
if(currentCC != null) {
|
|
|
|
|
if(old_scope != scope_string || forceUpdateClient) {
|
|
|
|
|
// マストドン2.4でスコープが追加された
|
|
|
|
|
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
|
|
|
|
|
ClientInfo.delete(instance, client_name)
|
|
|
|
|
|
|
|
|
|
// client credential をタンスから消去する
|
|
|
|
|
revokeClientCredential(client_info, client_credential)
|
|
|
|
|
|
|
|
|
|
// XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
|
|
|
|
|
} else {
|
|
|
|
|
// クライアント情報を再利用する
|
|
|
|
|
result.data = client_info
|
|
|
|
|
return result
|
|
|
|
|
}
|
2018-05-11 15:42:54 +02:00
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
val r2 = registerClient(scope_string, client_name)
|
2018-12-25 12:54:41 +01:00
|
|
|
|
client_info = r2?.jsonObject ?: return r2
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
// {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"}
|
2020-01-07 09:03:32 +01:00
|
|
|
|
client_info[KEY_AUTH_VERSION] = AUTH_VERSION
|
|
|
|
|
client_info[KEY_CLIENT_SCOPE] = scope_string
|
2018-12-25 12:54:41 +01:00
|
|
|
|
|
|
|
|
|
// client_credential をまだ取得していないなら取得する
|
2020-01-08 04:23:45 +01:00
|
|
|
|
var client_credential = client_info.string(KEY_CLIENT_CREDENTIAL)
|
2018-12-25 12:54:41 +01:00
|
|
|
|
if(client_credential?.isEmpty() != false) {
|
|
|
|
|
val resultSub = getClientCredential(client_info)
|
|
|
|
|
client_credential = resultSub?.string
|
2019-11-15 05:10:44 +01:00
|
|
|
|
if(client_credential?.isEmpty() != false) return resultSub
|
2020-01-07 09:03:32 +01:00
|
|
|
|
|
|
|
|
|
client_info[KEY_CLIENT_CREDENTIAL] = client_credential
|
2018-12-25 12:54:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 05:10:44 +01:00
|
|
|
|
try {
|
|
|
|
|
ClientInfo.save(instance, client_name, client_info.toString())
|
2020-01-07 09:03:32 +01:00
|
|
|
|
} catch(ignored : JsonException) {
|
2019-11-15 05:10:44 +01:00
|
|
|
|
}
|
2018-12-25 12:54:41 +01:00
|
|
|
|
result.data = client_info
|
2018-01-04 19:52:25 +01:00
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-25 12:54:41 +01:00
|
|
|
|
private fun authentication1Mastodon(
|
|
|
|
|
clientNameArg : String,
|
2019-03-13 18:34:56 +01:00
|
|
|
|
ti : TootInstance,
|
|
|
|
|
forceUpdateClient : Boolean = false
|
2019-10-02 11:37:41 +02:00
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
|
|
|
|
|
if(ti.instanceType == TootInstance.InstanceType.Pixelfed) {
|
|
|
|
|
return TootApiResult("currently Pixelfed instance is not supported.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return prepareClientMastodon(clientNameArg, ti, forceUpdateClient)?.also { result ->
|
2018-12-25 12:54:41 +01:00
|
|
|
|
val client_info = result.jsonObject
|
|
|
|
|
if(client_info != null) {
|
|
|
|
|
result.data = prepareBrowserUrl(getScopeString(ti), client_info)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-20 19:37:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-19 06:19:33 +02:00
|
|
|
|
// クライアントを登録してブラウザで開くURLを生成する
|
2019-03-13 18:34:56 +01:00
|
|
|
|
fun authentication1(
|
|
|
|
|
clientNameArg : String,
|
|
|
|
|
forceUpdateClient : Boolean = false
|
|
|
|
|
) : TootApiResult? {
|
2018-08-19 06:19:33 +02:00
|
|
|
|
|
2019-10-06 13:23:33 +02:00
|
|
|
|
val (ti, ri) = TootInstance.get(this)
|
|
|
|
|
ti ?: return ri
|
|
|
|
|
return when {
|
|
|
|
|
ti.misskeyVersion > 0 -> authentication1Misskey(clientNameArg, ti)
|
|
|
|
|
else -> authentication1Mastodon(clientNameArg, ti, forceUpdateClient)
|
2018-08-19 06:19:33 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
// oAuth2認証の続きを行う
|
2018-01-13 07:15:52 +01:00
|
|
|
|
fun authentication2(clientNameArg : String, code : String) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val instance = result.caption // same to instance
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val client_info =
|
|
|
|
|
ClientInfo.load(instance, client_name) ?: return result.setError("missing client id")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(! sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val scope_string = client_info.string(KEY_CLIENT_SCOPE)
|
|
|
|
|
val client_id = client_info.string("client_id")
|
|
|
|
|
val client_secret = client_info.string("client_secret")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
if(client_id == null) return result.setError("missing client_id ")
|
|
|
|
|
if(client_secret == null) return result.setError("missing client_secret")
|
|
|
|
|
|
|
|
|
|
val post_content = ("grant_type=authorization_code"
|
|
|
|
|
+ "&code=" + code.encodePercent()
|
|
|
|
|
+ "&client_id=" + client_id.encodePercent()
|
|
|
|
|
+ "&redirect_uri=" + REDIRECT_URL.encodePercent()
|
|
|
|
|
+ "&client_secret=" + client_secret.encodePercent()
|
2018-05-11 15:42:54 +02:00
|
|
|
|
+ "&scope=$scope_string"
|
|
|
|
|
+ "&scopes=$scope_string")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
2019-08-24 05:35:22 +02:00
|
|
|
|
post_content.toFormRequestBody().toPost()
|
2018-01-21 13:46:36 +01:00
|
|
|
|
.url("https://$instance/oauth/token")
|
|
|
|
|
.build()
|
|
|
|
|
|
|
|
|
|
}) return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val r2 = parseJson(result)
|
|
|
|
|
val token_info = r2?.jsonObject ?: return r2
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// {"access_token":"******","token_type":"bearer","scope":"read","created_at":1492334641}
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val access_token = token_info.string("access_token")
|
2018-01-13 07:15:52 +01:00
|
|
|
|
if(access_token?.isEmpty() != false) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
return result.setError("missing access_token in the response.")
|
|
|
|
|
}
|
2018-01-13 07:15:52 +01:00
|
|
|
|
return getUserCredential(access_token, token_info)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-29 19:34:19 +02:00
|
|
|
|
// アクセストークン手動入力でアカウントを更新する場合、アカウントの情報を取得する
|
2018-01-13 07:15:52 +01:00
|
|
|
|
fun getUserCredential(
|
2018-08-20 02:07:55 +02:00
|
|
|
|
access_token : String
|
2020-01-07 09:03:32 +01:00
|
|
|
|
, tokenInfo : JsonObject = JsonObject()
|
2019-04-25 04:06:41 +02:00
|
|
|
|
, misskeyVersion : Int = 0
|
2018-01-13 07:15:52 +01:00
|
|
|
|
) : TootApiResult? {
|
2019-04-25 04:06:41 +02:00
|
|
|
|
if(misskeyVersion > 0) {
|
2018-08-20 02:07:55 +02:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
// 認証されたアカウントのユーザ情報を取得する
|
|
|
|
|
if(! sendRequest(result) {
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonObject().apply{
|
|
|
|
|
put("i", access_token)
|
|
|
|
|
}
|
2018-08-20 02:07:55 +02:00
|
|
|
|
.toPostRequestBuilder()
|
|
|
|
|
.url("https://$instance/api/i")
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
|
|
|
|
|
|
|
|
|
val r2 = parseJson(result)
|
|
|
|
|
if(r2?.jsonObject != null) {
|
|
|
|
|
// ユーザ情報を読めたならtokenInfoを保存する
|
2020-01-07 09:03:32 +01:00
|
|
|
|
tokenInfo[KEY_AUTH_VERSION] = AUTH_VERSION
|
|
|
|
|
tokenInfo[KEY_API_KEY_MISSKEY] = access_token
|
|
|
|
|
tokenInfo[KEY_MISSKEY_VERSION] = misskeyVersion
|
2018-08-20 02:07:55 +02:00
|
|
|
|
result.tokenInfo = tokenInfo
|
|
|
|
|
}
|
|
|
|
|
return r2
|
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
} else {
|
2018-08-20 02:07:55 +02:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
// 認証されたアカウントのユーザ情報を取得する
|
|
|
|
|
if(! sendRequest(result) {
|
|
|
|
|
Request.Builder()
|
|
|
|
|
.url("https://$instance/api/v1/accounts/verify_credentials")
|
|
|
|
|
.header("Authorization", "Bearer $access_token")
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
|
|
|
|
|
|
|
|
|
val r2 = parseJson(result)
|
|
|
|
|
if(r2?.jsonObject != null) {
|
|
|
|
|
// ユーザ情報を読めたならtokenInfoを保存する
|
2020-01-07 09:03:32 +01:00
|
|
|
|
tokenInfo[KEY_AUTH_VERSION] = AUTH_VERSION
|
|
|
|
|
tokenInfo["access_token"] = access_token
|
2018-08-20 02:07:55 +02:00
|
|
|
|
result.tokenInfo = tokenInfo
|
|
|
|
|
}
|
|
|
|
|
return r2
|
|
|
|
|
|
2018-01-13 07:15:52 +01:00
|
|
|
|
}
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-25 12:54:41 +01:00
|
|
|
|
fun createUser1(clientNameArg : String) : TootApiResult? {
|
2019-03-13 18:34:56 +01:00
|
|
|
|
|
2019-10-06 13:23:33 +02:00
|
|
|
|
val (ti, ri) = TootInstance.get(this)
|
|
|
|
|
ti ?: return ri
|
|
|
|
|
|
2019-10-02 11:37:41 +02:00
|
|
|
|
return when(ti.instanceType) {
|
|
|
|
|
TootInstance.InstanceType.Misskey ->
|
|
|
|
|
TootApiResult("Misskey has no API to create new account")
|
|
|
|
|
TootInstance.InstanceType.Pleroma ->
|
|
|
|
|
TootApiResult("Pleroma has no API to create new account")
|
|
|
|
|
TootInstance.InstanceType.Pixelfed ->
|
|
|
|
|
TootApiResult("Pixelfed has no API to create new account")
|
|
|
|
|
else ->
|
|
|
|
|
prepareClientMastodon(clientNameArg, ti)
|
2020-01-07 09:03:32 +01:00
|
|
|
|
// result.JsonObject に credentialつきのclient_info を格納して返す
|
2018-12-25 12:54:41 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ユーザ名入力の後に呼ばれる
|
|
|
|
|
fun createUser2Mastodon(
|
2020-01-07 09:03:32 +01:00
|
|
|
|
client_info : JsonObject,
|
2018-12-25 12:54:41 +01:00
|
|
|
|
username : String,
|
|
|
|
|
email : String,
|
|
|
|
|
password : String,
|
2019-10-03 18:47:15 +02:00
|
|
|
|
agreement : Boolean,
|
|
|
|
|
reason : String?
|
2018-12-25 12:54:41 +01:00
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
|
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
log.d("createUser2Mastodon: client is : ${client_info}")
|
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val client_credential = client_info.string(KEY_CLIENT_CREDENTIAL)
|
2019-11-15 05:10:44 +01:00
|
|
|
|
?: return result.setError("createUser2Mastodon(): missing client credential")
|
2019-10-03 18:47:15 +02:00
|
|
|
|
|
2018-12-25 12:54:41 +01:00
|
|
|
|
if(! sendRequest(result) {
|
2019-10-06 13:23:33 +02:00
|
|
|
|
|
|
|
|
|
val params = ArrayList<String>().apply {
|
2019-10-03 18:47:15 +02:00
|
|
|
|
add("username=${username.encodePercent()}")
|
|
|
|
|
add("email=${email.encodePercent()}")
|
|
|
|
|
add("password=${password.encodePercent()}")
|
|
|
|
|
add("agreement=${agreement}")
|
2019-10-06 13:23:33 +02:00
|
|
|
|
if(reason?.isNotEmpty() == true) add("reason=${reason.encodePercent()}")
|
2019-10-03 18:47:15 +02:00
|
|
|
|
}
|
2019-10-06 13:23:33 +02:00
|
|
|
|
|
2019-10-03 18:47:15 +02:00
|
|
|
|
params
|
|
|
|
|
.joinToString("&").toFormRequestBody().toPost()
|
2018-12-25 12:54:41 +01:00
|
|
|
|
.url("https://$instance/api/v1/accounts")
|
|
|
|
|
.header("Authorization", "Bearer ${client_credential}")
|
|
|
|
|
.build()
|
|
|
|
|
|
|
|
|
|
}) return result
|
|
|
|
|
|
|
|
|
|
return parseJson(result)
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
|
fun searchMsp(query : String, max_id : String?) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// ユーザトークンを読む
|
2018-01-21 13:46:36 +01:00
|
|
|
|
var user_token : String? = Pref.spMspUserToken(pref)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
for(nTry in 0 until 3) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
if(callback.isApiCancelled) return null
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
// ユーザトークンがなければ取得する
|
2018-01-21 13:46:36 +01:00
|
|
|
|
if(user_token == null || user_token.isEmpty()) {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
callback.publishApiProgress("get MSP user token...")
|
|
|
|
|
|
|
|
|
|
val result : TootApiResult = TootApiResult.makeWithCaption("Mastodon Search Portal")
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
if(! sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
Request.Builder()
|
|
|
|
|
.url(mspTokenUrl + "?apikey=" + mspApiKey.encodePercent())
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
val r2 = parseJson(result) { json ->
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val error = json.string("error")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(error == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val type = json.string("type")
|
2018-01-13 07:15:52 +01:00
|
|
|
|
"error: $type $error"
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val jsonObject = r2?.jsonObject ?: return r2
|
2020-01-08 04:23:45 +01:00
|
|
|
|
user_token = jsonObject.jsonObject("result")?.string("token")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(user_token?.isEmpty() != false) {
|
|
|
|
|
return result.setError("Can't get MSP user token. response=${result.bodyString}")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
} else {
|
|
|
|
|
pref.edit().put(Pref.spMspUserToken, user_token).apply()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// ユーザトークンを使って検索APIを呼び出す
|
|
|
|
|
val result : TootApiResult = TootApiResult.makeWithCaption("Mastodon Search Portal")
|
|
|
|
|
if(result.error != null) return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(! sendRequest(result) {
|
2018-08-18 12:58:14 +02:00
|
|
|
|
val url = StringBuilder()
|
|
|
|
|
.append(mspSearchUrl)
|
|
|
|
|
.append("?apikey=").append(mspApiKey.encodePercent())
|
|
|
|
|
.append("&utoken=").append(user_token.encodePercent())
|
|
|
|
|
.append("&q=").append(query.encodePercent())
|
2018-08-19 06:19:33 +02:00
|
|
|
|
.append("&max=").append(max_id?.encodePercent() ?: "")
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
2018-08-18 12:58:14 +02:00
|
|
|
|
Request.Builder().url(url.toString()).build()
|
2018-01-21 13:46:36 +01:00
|
|
|
|
}) return result
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
var isUserTokenError = false
|
|
|
|
|
val r2 = parseJson(result) { json ->
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val error = json.string("error")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(error == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
// ユーザトークンがダメなら生成しなおす
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val detail = json.string("detail")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if("utoken" == detail) {
|
|
|
|
|
isUserTokenError = true
|
|
|
|
|
}
|
2020-01-07 09:03:32 +01:00
|
|
|
|
|
2020-01-08 04:23:45 +01:00
|
|
|
|
val type = json.string("type")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
"API returns error: $type $error"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(r2 == null || ! isUserTokenError) return r2
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
2018-01-12 10:01:25 +01:00
|
|
|
|
return TootApiResult("MSP user token retry exceeded.")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
fun searchTootsearch(
|
|
|
|
|
query : String,
|
2018-08-18 12:58:14 +02:00
|
|
|
|
from : Long?
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
|
|
|
|
|
val result = TootApiResult.makeWithCaption("Tootsearch")
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
if(! sendRequest(result) {
|
2018-08-18 12:58:14 +02:00
|
|
|
|
val sb = StringBuilder()
|
|
|
|
|
.append("https://tootsearch.chotto.moe/api/v1/search?sort=")
|
|
|
|
|
.append("created_at:desc".encodePercent())
|
|
|
|
|
.append("&q=").append(query.encodePercent())
|
|
|
|
|
if(from != null) {
|
|
|
|
|
sb.append("&from=").append(from.toString().encodePercent())
|
|
|
|
|
}
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
|
|
|
|
Request.Builder()
|
2018-08-18 12:58:14 +02:00
|
|
|
|
.url(sb.toString())
|
2018-01-21 13:46:36 +01:00
|
|
|
|
.build()
|
|
|
|
|
|
|
|
|
|
}) return result
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
return parseJson(result)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
|
// JSONデータ以外を扱うリクエスト
|
2018-01-11 10:31:25 +01:00
|
|
|
|
|
2018-08-16 21:58:30 +02:00
|
|
|
|
fun http(req : Request) : TootApiResult? {
|
2019-08-24 05:35:22 +02:00
|
|
|
|
val result = TootApiResult.makeWithCaption(req.url.host)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
2018-05-12 10:17:12 +02:00
|
|
|
|
sendRequest(result, progressPath = null) { req }
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-20 19:37:42 +02:00
|
|
|
|
// fun requestJson(req : Request) : TootApiResult? {
|
|
|
|
|
// val result = TootApiResult.makeWithCaption(req.url().host())
|
|
|
|
|
// if(result.error != null) return result
|
|
|
|
|
// if(sendRequest(result, progressPath = null) { req }) {
|
2020-01-08 03:57:10 +01:00
|
|
|
|
// decodeJsonValue(result)
|
2018-08-20 19:37:42 +02:00
|
|
|
|
// }
|
|
|
|
|
// return result
|
|
|
|
|
// }
|
2018-05-12 10:17:12 +02:00
|
|
|
|
|
|
|
|
|
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
|
2018-08-16 21:58:30 +02:00
|
|
|
|
fun getHttp(url : String) : TootApiResult? {
|
2018-05-12 10:17:12 +02:00
|
|
|
|
val result = http(Request.Builder().url(url).build())
|
2018-08-16 21:58:30 +02:00
|
|
|
|
if(result != null && result.error == null) {
|
2018-05-12 10:17:12 +02:00
|
|
|
|
parseString(result)
|
|
|
|
|
}
|
|
|
|
|
return result
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
fun getHttpBytes(url : String) : Pair<TootApiResult?, ByteArray?> {
|
2018-03-10 00:12:40 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(url)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
if(result.error != null) return Pair(result, null)
|
2018-03-10 00:12:40 +01:00
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
if(! sendRequest(result, progressPath = url) {
|
2018-03-10 00:12:40 +01:00
|
|
|
|
Request.Builder().url(url).build()
|
2019-01-16 16:27:37 +01:00
|
|
|
|
}) {
|
|
|
|
|
return Pair(result, null)
|
|
|
|
|
}
|
|
|
|
|
val r2 = parseBytes(result)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
return Pair(r2, r2?.data as? ByteArray)
|
2018-03-10 00:12:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
fun webSocket(
|
|
|
|
|
path : String,
|
|
|
|
|
ws_listener : WebSocketListener
|
|
|
|
|
) : Pair<TootApiResult?, WebSocket?> {
|
|
|
|
|
var ws : WebSocket? = null
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
if(result.error != null) return Pair(result, null)
|
|
|
|
|
val account = this.account ?: return Pair(TootApiResult("account is null"), null)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
try {
|
|
|
|
|
var url = "wss://$instance$path"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-13 07:15:52 +01:00
|
|
|
|
val request_builder = Request.Builder()
|
2018-01-21 13:46:36 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val access_token = account.getAccessToken()
|
|
|
|
|
if(access_token?.isNotEmpty() == true) {
|
|
|
|
|
val delm = if(- 1 != url.indexOf('?')) '&' else '?'
|
2018-01-21 13:46:36 +01:00
|
|
|
|
url = url + delm + "access_token=" + access_token.encodePercent()
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-13 07:15:52 +01:00
|
|
|
|
val request = request_builder.url(url).build()
|
2019-08-24 05:35:22 +02:00
|
|
|
|
publishApiProgress(context.getString(R.string.request_api, request.method, path))
|
2019-01-16 16:27:37 +01:00
|
|
|
|
ws = httpClient.getWebSocket(request, ws_listener)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(isApiCancelled) {
|
|
|
|
|
ws.cancel()
|
2019-03-13 18:34:56 +01:00
|
|
|
|
return Pair(null, null)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
result.error =
|
|
|
|
|
"${result.caption}: ${ex.withCaption(context.resources, R.string.network_error)}"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
2019-03-13 18:34:56 +01:00
|
|
|
|
return Pair(result, ws)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
}
|
2018-08-20 19:37:42 +02:00
|
|
|
|
|
2018-08-30 14:33:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-13 17:49:19 +02:00
|
|
|
|
// query: query_string after ? ( ? itself is excluded )
|
2019-09-27 04:30:48 +02:00
|
|
|
|
fun TootApiClient.requestMastodonSearch(
|
|
|
|
|
parser : TootParser,
|
|
|
|
|
query : String
|
|
|
|
|
) : Pair<TootApiResult?, TootResults?> {
|
2019-09-13 17:49:19 +02:00
|
|
|
|
|
|
|
|
|
var searchApiVersion = 2
|
|
|
|
|
var apiResult = request("/api/v2/search?$query")
|
|
|
|
|
?: return Pair(null, null)
|
2019-09-27 04:30:48 +02:00
|
|
|
|
|
|
|
|
|
if((apiResult.response?.code ?: 0) in 400 until 500) {
|
2019-09-13 17:49:19 +02:00
|
|
|
|
searchApiVersion = 1
|
|
|
|
|
apiResult = request("/api/v1/search?$query")
|
|
|
|
|
?: return Pair(null, null)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-27 04:30:48 +02:00
|
|
|
|
val searchResult = parser.results(apiResult.jsonObject)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
searchResult?.searchApiVersion = searchApiVersion
|
|
|
|
|
|
2019-09-27 04:30:48 +02:00
|
|
|
|
return Pair(apiResult, searchResult)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-29 17:38:52 +01:00
|
|
|
|
// result.data に TootAccountRefを格納して返す。もしくはエラーかキャンセル
|
2019-01-16 14:46:56 +01:00
|
|
|
|
fun TootApiClient.syncAccountByUrl(
|
|
|
|
|
accessInfo : SavedAccount,
|
|
|
|
|
who_url : String
|
|
|
|
|
) : Pair<TootApiResult?, TootAccountRef?> {
|
2018-10-07 19:57:19 +02:00
|
|
|
|
|
2018-09-20 18:41:04 +02:00
|
|
|
|
// misskey由来のアカウントURLは https://host/@user@instance などがある
|
|
|
|
|
val m = TootAccount.reAccountUrl.matcher(who_url)
|
2018-10-07 19:57:19 +02:00
|
|
|
|
if(m.find()) {
|
2018-09-20 18:41:04 +02:00
|
|
|
|
// val host = m.group(1)
|
2019-09-12 16:05:18 +02:00
|
|
|
|
val user = m.groupEx(2) !!.decodePercent()
|
|
|
|
|
val instance = m.groupEx(3)?.decodePercent()
|
2018-10-07 19:57:19 +02:00
|
|
|
|
if(instance?.isNotEmpty() == true) {
|
|
|
|
|
return this.syncAccountByUrl(accessInfo, "https://$instance/@$user")
|
2018-09-20 18:41:04 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-18 04:57:10 +01:00
|
|
|
|
val parser = TootParser(context, accessInfo)
|
|
|
|
|
|
2019-09-13 17:49:19 +02:00
|
|
|
|
return if(accessInfo.isMisskey) {
|
2018-12-18 04:57:10 +01:00
|
|
|
|
|
2018-08-28 14:25:45 +02:00
|
|
|
|
val acct = TootAccount.getAcctFromUrl(who_url)
|
2019-09-27 04:30:48 +02:00
|
|
|
|
?: return Pair(
|
|
|
|
|
TootApiResult(context.getString(R.string.user_id_conversion_failed)),
|
|
|
|
|
null
|
|
|
|
|
)
|
2018-08-28 14:25:45 +02:00
|
|
|
|
|
2019-09-13 17:49:19 +02:00
|
|
|
|
var ar : TootAccountRef? = null
|
|
|
|
|
val result = request(
|
2018-12-18 04:57:10 +01:00
|
|
|
|
"/api/users/show",
|
2020-01-07 09:03:32 +01:00
|
|
|
|
accessInfo.putMisskeyApiToken().apply {
|
2018-12-18 04:57:10 +01:00
|
|
|
|
when(val delm = acct.indexOf('@')) {
|
|
|
|
|
- 1 -> put("username", acct)
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
put("username", acct.substring(0, delm))
|
|
|
|
|
put("host", acct.substring(delm + 1))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.toPostRequestBuilder()
|
|
|
|
|
)
|
|
|
|
|
?.apply {
|
2019-01-16 14:46:56 +01:00
|
|
|
|
ar = TootAccountRef.mayNull(parser, parser.account(jsonObject))
|
|
|
|
|
if(ar == null && error == null) {
|
2018-12-18 04:57:10 +01:00
|
|
|
|
setError(context.getString(R.string.user_id_conversion_failed))
|
|
|
|
|
}
|
2018-08-28 14:25:45 +02:00
|
|
|
|
}
|
2019-09-13 17:49:19 +02:00
|
|
|
|
Pair(result, ar)
|
2018-08-30 14:33:22 +02:00
|
|
|
|
} else {
|
2019-09-27 04:30:48 +02:00
|
|
|
|
val (apiResult, searchResult) = requestMastodonSearch(
|
|
|
|
|
parser,
|
|
|
|
|
"q=${who_url.encodePercent()}&resolve=true"
|
|
|
|
|
)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
val ar = searchResult?.accounts?.firstOrNull()
|
|
|
|
|
if(apiResult != null && apiResult.error == null && ar == null) {
|
|
|
|
|
apiResult.setError(context.getString(R.string.user_id_conversion_failed))
|
|
|
|
|
}
|
|
|
|
|
Pair(apiResult, ar)
|
2018-08-28 14:25:45 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-20 19:37:42 +02:00
|
|
|
|
|
2019-01-16 14:46:56 +01:00
|
|
|
|
fun TootApiClient.syncAccountByAcct(
|
|
|
|
|
accessInfo : SavedAccount,
|
|
|
|
|
acct : String
|
|
|
|
|
) : Pair<TootApiResult?, TootAccountRef?> {
|
2018-12-18 04:57:10 +01:00
|
|
|
|
|
|
|
|
|
val parser = TootParser(context, accessInfo)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
return if(accessInfo.isMisskey) {
|
|
|
|
|
var ar : TootAccountRef? = null
|
|
|
|
|
val result = request(
|
2018-12-18 04:57:10 +01:00
|
|
|
|
"/api/users/show",
|
|
|
|
|
accessInfo.putMisskeyApiToken()
|
|
|
|
|
.apply {
|
|
|
|
|
when(val delm = acct.indexOf('@')) {
|
|
|
|
|
- 1 -> put("username", acct)
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
put("username", acct.substring(0, delm))
|
|
|
|
|
put("host", acct.substring(delm + 1))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.toPostRequestBuilder()
|
|
|
|
|
)
|
|
|
|
|
?.apply {
|
2019-01-16 14:46:56 +01:00
|
|
|
|
ar = TootAccountRef.mayNull(parser, parser.account(jsonObject))
|
|
|
|
|
if(ar == null && error == null) {
|
2018-12-18 04:57:10 +01:00
|
|
|
|
setError(context.getString(R.string.user_id_conversion_failed))
|
|
|
|
|
}
|
2018-08-28 14:25:45 +02:00
|
|
|
|
}
|
2019-09-13 17:49:19 +02:00
|
|
|
|
Pair(result, ar)
|
2018-08-30 14:33:22 +02:00
|
|
|
|
} else {
|
2019-09-27 04:30:48 +02:00
|
|
|
|
val (apiResult, searchResult) = requestMastodonSearch(
|
|
|
|
|
parser,
|
|
|
|
|
"q=${acct.encodePercent()}&resolve=true"
|
|
|
|
|
)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
val ar = searchResult?.accounts?.firstOrNull()
|
|
|
|
|
if(apiResult != null && apiResult.error == null && ar == null) {
|
|
|
|
|
apiResult.setError(context.getString(R.string.user_id_conversion_failed))
|
|
|
|
|
}
|
|
|
|
|
Pair(apiResult, ar)
|
2018-08-28 14:25:45 +02:00
|
|
|
|
}
|
2019-09-13 17:49:19 +02:00
|
|
|
|
|
2018-08-30 14:33:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
fun TootApiClient.syncStatus(
|
|
|
|
|
accessInfo : SavedAccount,
|
|
|
|
|
urlArg : String
|
|
|
|
|
) : Pair<TootApiResult?, TootStatus?> {
|
2018-11-27 17:53:27 +01:00
|
|
|
|
|
|
|
|
|
var url = urlArg
|
|
|
|
|
|
|
|
|
|
// misskey の投稿URLは外部タンスの投稿を複製したものの可能性がある
|
|
|
|
|
// これを投稿元タンスのURLに変換しないと、投稿の同期には使えない
|
|
|
|
|
val m = TootStatus.reStatusPageMisskey.matcher(urlArg)
|
|
|
|
|
if(m.find()) {
|
2019-09-12 16:05:18 +02:00
|
|
|
|
val host = m.groupEx(1)
|
|
|
|
|
val noteId = m.groupEx(2)
|
2018-11-27 18:42:55 +01:00
|
|
|
|
|
2018-12-08 07:26:19 +01:00
|
|
|
|
TootApiClient(context, callback = callback)
|
2018-11-27 18:42:55 +01:00
|
|
|
|
.apply { instance = host }
|
|
|
|
|
.request(
|
|
|
|
|
"/api/notes/show",
|
2020-01-07 09:03:32 +01:00
|
|
|
|
JsonObject().apply{
|
|
|
|
|
put("noteId", noteId)
|
|
|
|
|
}
|
2018-11-27 18:42:55 +01:00
|
|
|
|
.toPostRequestBuilder()
|
2018-12-08 07:26:19 +01:00
|
|
|
|
)
|
|
|
|
|
?.also { result ->
|
|
|
|
|
TootParser(
|
|
|
|
|
context,
|
2019-04-25 04:06:41 +02:00
|
|
|
|
LinkHelper.newLinkHelper(host, misskeyVersion = 10),
|
2018-12-08 07:26:19 +01:00
|
|
|
|
serviceType = ServiceType.MISSKEY
|
|
|
|
|
)
|
|
|
|
|
.status(result.jsonObject)
|
|
|
|
|
?.apply {
|
2020-02-01 19:28:16 +01:00
|
|
|
|
if(host.equals(accessInfo.hostAscii, ignoreCase = true)) {
|
2019-03-13 18:34:56 +01:00
|
|
|
|
return Pair(result, this)
|
2018-12-08 07:26:19 +01:00
|
|
|
|
}
|
|
|
|
|
uri.letNotEmpty { url = it }
|
|
|
|
|
}
|
2019-01-16 16:27:37 +01:00
|
|
|
|
|
2018-11-27 17:53:27 +01:00
|
|
|
|
}
|
2019-03-13 18:34:56 +01:00
|
|
|
|
?: return Pair(null, null) // cancelled.
|
2018-11-27 17:53:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-12-08 07:26:19 +01:00
|
|
|
|
// 使いたいタンス上の投稿IDを取得する
|
|
|
|
|
val parser = TootParser(context, accessInfo)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
return if(accessInfo.isMisskey) {
|
|
|
|
|
var targetStatus : TootStatus? = null
|
|
|
|
|
val result = request(
|
2018-12-08 07:26:19 +01:00
|
|
|
|
"/api/ap/show",
|
2020-01-07 09:03:32 +01:00
|
|
|
|
accessInfo.putMisskeyApiToken().apply{
|
|
|
|
|
put("uri", url)
|
|
|
|
|
}
|
2018-12-08 07:26:19 +01:00
|
|
|
|
.toPostRequestBuilder()
|
2018-12-18 04:57:10 +01:00
|
|
|
|
)
|
|
|
|
|
?.apply {
|
2019-01-16 16:27:37 +01:00
|
|
|
|
targetStatus = parser.parseMisskeyApShow(jsonObject) as? TootStatus
|
|
|
|
|
if(targetStatus == null && error == null) {
|
2018-12-18 04:57:10 +01:00
|
|
|
|
setError(context.getString(R.string.cant_sync_toot))
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-09-13 17:49:19 +02:00
|
|
|
|
Pair(result, targetStatus)
|
2018-12-18 04:57:10 +01:00
|
|
|
|
} else {
|
2019-09-27 04:30:48 +02:00
|
|
|
|
val (apiResult, searchResult) = requestMastodonSearch(
|
|
|
|
|
parser,
|
|
|
|
|
"q=${url.encodePercent()}&resolve=true"
|
|
|
|
|
)
|
2019-09-13 17:49:19 +02:00
|
|
|
|
val targetStatus = searchResult?.statuses?.firstOrNull()
|
2019-09-27 04:30:48 +02:00
|
|
|
|
if(apiResult != null && apiResult.error == null && targetStatus == null) {
|
2019-09-13 17:49:19 +02:00
|
|
|
|
apiResult.setError(context.getString(R.string.cant_sync_toot))
|
|
|
|
|
}
|
2019-09-27 04:30:48 +02:00
|
|
|
|
Pair(apiResult, targetStatus)
|
2018-10-07 19:57:19 +02:00
|
|
|
|
}
|
2019-09-13 17:49:19 +02:00
|
|
|
|
|
2018-11-27 17:53:27 +01:00
|
|
|
|
}
|
2018-10-07 19:57:19 +02:00
|
|
|
|
|
2018-08-30 14:33:22 +02:00
|
|
|
|
fun TootApiClient.syncStatus(
|
|
|
|
|
accessInfo : SavedAccount,
|
|
|
|
|
statusRemote : TootStatus
|
2019-03-13 18:34:56 +01:00
|
|
|
|
) : Pair<TootApiResult?, TootStatus?> {
|
2018-08-30 15:06:23 +02:00
|
|
|
|
|
2018-10-07 19:57:19 +02:00
|
|
|
|
// URL->URIの順に試す
|
2018-08-30 15:06:23 +02:00
|
|
|
|
|
2018-10-07 19:57:19 +02:00
|
|
|
|
val uriList = ArrayList<String>(2)
|
2018-10-31 03:53:35 +01:00
|
|
|
|
|
2018-12-08 07:26:19 +01:00
|
|
|
|
statusRemote.url.letNotEmpty {
|
2019-08-24 05:35:22 +02:00
|
|
|
|
when {
|
|
|
|
|
it.contains("/notes/") -> {
|
|
|
|
|
// Misskeyタンスから読んだマストドンの投稿はurlがmisskeyタンス上のものになる
|
|
|
|
|
// ActivityPub object id としては不適切なので使わない
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> uriList.add(it)
|
2018-08-30 14:33:22 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-10-31 03:53:35 +01:00
|
|
|
|
|
2018-12-08 07:26:19 +01:00
|
|
|
|
statusRemote.uri.letNotEmpty {
|
2018-10-07 19:57:19 +02:00
|
|
|
|
// uri の方は↑の問題はない
|
|
|
|
|
uriList.add(it)
|
|
|
|
|
}
|
2018-08-30 14:33:22 +02:00
|
|
|
|
|
2018-12-08 07:26:19 +01:00
|
|
|
|
if(accessInfo.isMisskey && uriList.firstOrNull()?.contains("@") == true) {
|
2018-10-07 19:57:19 +02:00
|
|
|
|
// https://github.com/syuilo/misskey/pull/2832
|
|
|
|
|
// @user を含むuri はMisskeyだと少し効率が悪いそうなので順序を入れ替える
|
|
|
|
|
uriList.reverse()
|
2018-08-30 14:33:22 +02:00
|
|
|
|
}
|
2018-10-07 19:57:19 +02:00
|
|
|
|
|
2018-10-31 03:53:35 +01:00
|
|
|
|
for(uri in uriList) {
|
2019-01-16 16:27:37 +01:00
|
|
|
|
val pair = syncStatus(accessInfo, uri)
|
2019-03-13 18:34:56 +01:00
|
|
|
|
if(pair.second != null || pair.first == null) {
|
2019-01-16 16:27:37 +01:00
|
|
|
|
return pair
|
2018-10-07 19:57:19 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-13 18:34:56 +01:00
|
|
|
|
return Pair(TootApiResult("can't resolve status URL/URI."), null)
|
2018-12-02 10:35:04 +01:00
|
|
|
|
}
|