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-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
import org.json.JSONException
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
|
|
|
|
|
import jp.juggler.subwaytooter.App1
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.Pref
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.table.ClientInfo
|
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
|
|
|
|
import jp.juggler.subwaytooter.R
|
2018-05-11 15:42:54 +02:00
|
|
|
|
import jp.juggler.subwaytooter.api.entity.TootInstance
|
2018-01-17 02:16:26 +01:00
|
|
|
|
import jp.juggler.subwaytooter.put
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.util.*
|
|
|
|
|
import okhttp3.*
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import org.json.JSONArray
|
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,
|
|
|
|
|
App1.ok_http_client,
|
|
|
|
|
App1.ok_http_client2
|
|
|
|
|
),
|
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) {
|
|
|
|
|
instance = value?.host
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
val MEDIA_TYPE_FORM_URL_ENCODED = MediaType.parse("application/x-www-form-urlencoded")
|
|
|
|
|
val MEDIA_TYPE_JSON = MediaType.parse("application/json;charset=UTF-8")
|
|
|
|
|
|
|
|
|
|
private const val DEFAULT_CLIENT_NAME = "SubwayTooter"
|
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"
|
2018-05-11 15:42:54 +02:00
|
|
|
|
private const val AUTH_VERSION = 3
|
2018-01-04 19:52:25 +01:00
|
|
|
|
private const val REDIRECT_URL = "subwaytooter://oauth/"
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
private const val NO_INFORMATION = "(no information)"
|
|
|
|
|
|
|
|
|
|
private val reStartJsonArray = Pattern.compile("\\A\\s*\\[")
|
|
|
|
|
private val reStartJsonObject = Pattern.compile("\\A\\s*\\{")
|
2018-01-17 18:39:16 +01:00
|
|
|
|
private val reWhiteSpace = Pattern.compile("\\s+")
|
2018-01-12 10:01:25 +01: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
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
fun getMspMaxId(array : JSONArray, max_id : String) : String {
|
|
|
|
|
// max_id の更新
|
|
|
|
|
val size = array.length()
|
|
|
|
|
if(size > 0) {
|
|
|
|
|
val item = array.optJSONObject(size - 1)
|
|
|
|
|
if(item != null) {
|
|
|
|
|
val sv = item.optString("msp_id")
|
|
|
|
|
if(sv?.isNotEmpty() == true) return sv
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return max_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getTootsearchHits(root : JSONObject) : JSONArray? {
|
|
|
|
|
val hits = root.optJSONObject("hits")
|
|
|
|
|
return hits?.optJSONArray("hits")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// returns the number for "from" parameter of next page.
|
|
|
|
|
// returns "" if no more next page.
|
|
|
|
|
fun getTootsearchMaxId(root : JSONObject, old : String) : String {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val old_from = old.optInt() ?: 0
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val hits2 = getTootsearchHits(root)
|
|
|
|
|
if(hits2 != null) {
|
|
|
|
|
val size = hits2.length()
|
|
|
|
|
return if(size == 0) "" else Integer.toString(old_from + hits2.length())
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val DEFAULT_JSON_ERROR_PARSER = { json : JSONObject -> json.parseString("error") }
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
internal fun simplifyErrorHtml(
|
|
|
|
|
response : Response,
|
|
|
|
|
sv : String,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : String {
|
|
|
|
|
|
|
|
|
|
// JSONObjectとして解釈できるならエラーメッセージを検出する
|
|
|
|
|
try {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val error_message = jsonErrorParser(sv.toJsonObject())
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(error_message?.isNotEmpty() == true) {
|
|
|
|
|
return error_message
|
|
|
|
|
}
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.e(ex, "response body is not JSON or missing 'error' attribute.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HTMLならタグの除去を試みる
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : String {
|
|
|
|
|
val sb = StringBuilder()
|
|
|
|
|
try {
|
|
|
|
|
// body は既に読み終わっているか、そうでなければこれから読む
|
|
|
|
|
if(bodyString != null) {
|
|
|
|
|
sb.append(simplifyErrorHtml(response, bodyString, jsonErrorParser))
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
val string = response.body()?.string()
|
|
|
|
|
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(' ')
|
|
|
|
|
sb.append("(HTTP ").append(Integer.toString(response.code()))
|
|
|
|
|
|
|
|
|
|
val message = response.message()
|
|
|
|
|
if(message != null && message.isNotEmpty()) {
|
|
|
|
|
sb.append(' ').append(message)
|
|
|
|
|
}
|
|
|
|
|
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-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-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-03-18 06:42:44 +01:00
|
|
|
|
cached : Boolean = false,
|
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()
|
|
|
|
|
|
|
|
|
|
callback.publishApiProgress(
|
|
|
|
|
context.getString(
|
|
|
|
|
R.string.request_api
|
|
|
|
|
, request.method()
|
|
|
|
|
, progressPath ?: request.url().encodedPath()
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2018-04-30 16:01:00 +02:00
|
|
|
|
result.response = httpClient.getResponse(request, cached = cached)
|
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,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : String? {
|
|
|
|
|
|
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
val response = result.response !!
|
|
|
|
|
|
|
|
|
|
val request = response.request()
|
|
|
|
|
if(request != null) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
publishApiProgress(
|
|
|
|
|
context.getString(
|
|
|
|
|
R.string.reading_api,
|
|
|
|
|
request.method(),
|
|
|
|
|
progressPath ?: result.caption
|
|
|
|
|
)
|
|
|
|
|
)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val bodyString = response.body()?.string()
|
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
if(! response.isSuccessful || bodyString?.isEmpty() != false) {
|
|
|
|
|
|
|
|
|
|
result.error = TootApiClient.formatResponse(
|
|
|
|
|
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,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : ByteArray? {
|
|
|
|
|
|
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
val response = result.response !!
|
|
|
|
|
|
|
|
|
|
val request = response.request()
|
|
|
|
|
if(request != null) {
|
|
|
|
|
publishApiProgress(
|
|
|
|
|
context.getString(
|
|
|
|
|
R.string.reading_api,
|
|
|
|
|
request.method(),
|
|
|
|
|
progressPath ?: result.caption
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val bodyBytes = response.body()?.bytes()
|
|
|
|
|
if(isApiCancelled) return null
|
|
|
|
|
|
|
|
|
|
if(! response.isSuccessful || bodyBytes?.isEmpty() != false) {
|
|
|
|
|
|
|
|
|
|
result.error = TootApiClient.formatResponse(
|
|
|
|
|
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,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : 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,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : 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,
|
|
|
|
|
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
|
|
|
|
) : TootApiResult? // 引数に指定したresultそのものか、キャンセルされたらnull
|
|
|
|
|
{
|
|
|
|
|
val response = result.response !! // nullにならないはず
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
try {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val bodyString = readBodyString(result, progressPath, jsonErrorParser)
|
|
|
|
|
?: return if(isApiCancelled) null else result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(reStartJsonArray.matcher(bodyString).find()) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
result.data = bodyString.toJsonArray()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
|
|
|
|
} else if(reStartJsonObject.matcher(bodyString).find()) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val json = bodyString.toJsonObject()
|
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
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
result.error = context.getString(R.string.response_not_json) + "\n" + 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
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
val account = this.account ?: return result.setError("account is null")
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
val access_token = account.getAccessToken()
|
|
|
|
|
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-01-04 19:52:25 +01:00
|
|
|
|
// 疑似アカウントの追加時に、インスタンスの検証を行う
|
2018-01-13 07:15:52 +01:00
|
|
|
|
fun getInstanceInformation() : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
2018-01-10 16:47:35 +01:00
|
|
|
|
if(result.error != null) return result
|
2018-08-16 21:58:30 +02:00
|
|
|
|
|
|
|
|
|
if(sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
Request.Builder().url("https://$instance/api/v1/instance").build()
|
2018-08-16 21:58:30 +02:00
|
|
|
|
}
|
|
|
|
|
&& parseJson(result) != null
|
|
|
|
|
&& result.jsonObject != null
|
|
|
|
|
) {
|
|
|
|
|
// インスタンス情報のjsonを読めたらマストドンのインスタンス
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// misskeyか試してみる
|
|
|
|
|
val r2 = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(sendRequest(r2) {
|
|
|
|
|
Request.Builder().post(RequestBody.create(MEDIA_TYPE_JSON,JSONObject().apply{
|
|
|
|
|
put("dummy",1)
|
|
|
|
|
}.toString()))
|
|
|
|
|
.url("https://$instance/api/notes/local-timeline").build()
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
if(parseJson(r2) != null && r2.jsonArray != null) {
|
|
|
|
|
r2.data = JSONObject().apply{
|
|
|
|
|
put("isMisskey", true)
|
|
|
|
|
}
|
|
|
|
|
return r2
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// misskeyの事は忘れて本来のエラー情報を返す
|
|
|
|
|
return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
// インスタンス情報を取得する
|
|
|
|
|
internal fun getInstanceInformation2() : TootApiResult? {
|
|
|
|
|
val r = getInstanceInformation()
|
|
|
|
|
if(r != null) {
|
|
|
|
|
val json = r.jsonObject
|
|
|
|
|
if(json != null) {
|
|
|
|
|
val parser = TootParser(context, object : LinkHelper {
|
|
|
|
|
override val host : String?
|
|
|
|
|
get() = instance
|
|
|
|
|
})
|
|
|
|
|
val ti = parser.instance(json)
|
|
|
|
|
if(ti != null) {
|
|
|
|
|
r.data = ti
|
|
|
|
|
} else {
|
|
|
|
|
r.setError("can't parse data in /api/v1/instance")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
// クライアントをタンスに登録
|
2018-05-11 15:42:54 +02:00
|
|
|
|
internal 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-01-21 13:46:36 +01:00
|
|
|
|
Request.Builder()
|
|
|
|
|
.url("https://$instance/api/v1/apps")
|
|
|
|
|
.post(
|
|
|
|
|
RequestBody.create(
|
|
|
|
|
MEDIA_TYPE_FORM_URL_ENCODED,
|
|
|
|
|
"client_name=" + clientName.encodePercent()
|
|
|
|
|
+ "&redirect_uris=" + REDIRECT_URL.encodePercent()
|
2018-05-11 15:42:54 +02:00
|
|
|
|
+ "&scopes=$scope_string"
|
2018-01-21 13:46:36 +01:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.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を呼び出すたびに新しく生成される…
|
2018-01-12 10:01:25 +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
|
|
|
|
|
|
|
|
|
val client_id = client_info.parseString("client_id")
|
|
|
|
|
?: return result.setError("missing client_id")
|
|
|
|
|
|
|
|
|
|
val client_secret = client_info.parseString("client_secret")
|
|
|
|
|
?: return result.setError("missing client_secret")
|
|
|
|
|
|
|
|
|
|
Request.Builder()
|
|
|
|
|
.url("https://$instance/oauth/token")
|
|
|
|
|
.post(
|
|
|
|
|
RequestBody.create(
|
|
|
|
|
MEDIA_TYPE_FORM_URL_ENCODED,
|
|
|
|
|
"grant_type=client_credentials"
|
|
|
|
|
+ "&client_id=" + client_id.encodePercent()
|
|
|
|
|
+ "&client_secret=" + client_secret.encodePercent()
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.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-01-21 13:46:36 +01:00
|
|
|
|
val sv = jsonObject.parseString("access_token")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
if(sv?.isNotEmpty() == true) {
|
|
|
|
|
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-05-11 15:42:54 +02:00
|
|
|
|
// // client_credentialを無効にする
|
|
|
|
|
internal fun revokeClientCredential(
|
|
|
|
|
client_info : JSONObject,
|
|
|
|
|
client_credential : String
|
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
val result = TootApiResult.makeWithCaption(this.instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
val client_id = client_info.parseString("client_id")
|
|
|
|
|
?: return result.setError("missing client_id")
|
|
|
|
|
|
|
|
|
|
val client_secret = client_info.parseString("client_secret")
|
|
|
|
|
?: return result.setError("missing client_secret")
|
|
|
|
|
|
|
|
|
|
if(! sendRequest(result) {
|
|
|
|
|
Request.Builder()
|
|
|
|
|
.url("https://$instance/oauth/revoke")
|
|
|
|
|
.post(
|
|
|
|
|
RequestBody.create(
|
|
|
|
|
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED,
|
|
|
|
|
"token=" + client_credential.encodePercent()
|
|
|
|
|
+ "&client_id=" + client_id.encodePercent()
|
|
|
|
|
+ "&client_secret=" + client_secret.encodePercent()
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
|
|
|
|
|
|
|
|
|
return parseJson(result)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
// 認証ページURLを作る
|
2018-05-11 15:42:54 +02:00
|
|
|
|
internal fun prepareBrowserUrl(scope_string : String, client_info : JSONObject) : String? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val account = this.account
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val client_id = client_info.parseString("client_id") ?: return null
|
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"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
+ "&state=" + (if(account != null) "db:" + account.db_id else "host:" + instance)
|
|
|
|
|
+ "&grant_type=authorization_code"
|
|
|
|
|
+ "&approval_prompt=force"
|
|
|
|
|
// +"&access_type=offline"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// クライアントを登録してブラウザで開くURLを生成する
|
2018-01-13 07:15:52 +01:00
|
|
|
|
fun authentication1(clientNameArg : String) : TootApiResult? {
|
2018-05-11 15:42:54 +02:00
|
|
|
|
|
|
|
|
|
// インスタンス情報の取得
|
|
|
|
|
val ri = getInstanceInformation2()
|
|
|
|
|
val ti = ri?.data as? TootInstance ?: return ri
|
|
|
|
|
val scope_string = getScopeString(ti)
|
|
|
|
|
|
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がアプリ上に保存されているか?
|
|
|
|
|
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
|
|
|
|
|
val client_info = ClientInfo.load(instance, client_name)
|
|
|
|
|
if(client_info != null) {
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL)
|
2018-05-11 15:42:54 +02:00
|
|
|
|
val old_scope = client_info.parseString(KEY_CLIENT_SCOPE)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// client_credential をまだ取得していないなら取得する
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(client_credential?.isEmpty() != false) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val resultSub = getClientCredential(client_info)
|
|
|
|
|
client_credential = resultSub?.string
|
|
|
|
|
if(client_credential?.isNotEmpty() == true) {
|
|
|
|
|
try {
|
|
|
|
|
client_info.put(KEY_CLIENT_CREDENTIAL, client_credential)
|
|
|
|
|
ClientInfo.save(instance, client_name, client_info.toString())
|
|
|
|
|
} catch(ignored : JSONException) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// client_credential があるならcredentialがまだ使えるか確認する
|
|
|
|
|
if(client_credential?.isNotEmpty() == true) {
|
|
|
|
|
val resultSub = verifyClientCredential(client_credential)
|
2018-01-10 16:47:35 +01:00
|
|
|
|
if(resultSub?.jsonObject != null) {
|
2018-05-11 15:42:54 +02:00
|
|
|
|
|
|
|
|
|
if(old_scope != scope_string) {
|
|
|
|
|
// マストドン2.4でスコープが追加された
|
|
|
|
|
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
|
|
|
|
|
ClientInfo.delete(instance, client_name)
|
|
|
|
|
|
|
|
|
|
// client credential をタンスから消去する
|
|
|
|
|
revokeClientCredential(client_info, client_credential)
|
|
|
|
|
|
|
|
|
|
// FIXME クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
|
|
|
|
|
} else {
|
|
|
|
|
// クライアント情報を再利用する
|
|
|
|
|
result.data = prepareBrowserUrl(scope_string, client_info)
|
|
|
|
|
return result
|
|
|
|
|
}
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
val r2 = registerClient(scope_string, client_name)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val jsonObject = 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":"******"}
|
|
|
|
|
jsonObject.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
2018-05-11 15:42:54 +02:00
|
|
|
|
jsonObject.put(KEY_CLIENT_SCOPE, scope_string)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
ClientInfo.save(instance, client_name, jsonObject.toString())
|
2018-05-11 15:42:54 +02:00
|
|
|
|
result.data = prepareBrowserUrl(scope_string, jsonObject)
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2018-05-11 15:42:54 +02:00
|
|
|
|
val scope_string = client_info.optString(KEY_CLIENT_SCOPE)
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val client_id = client_info.parseString("client_id")
|
|
|
|
|
val client_secret = client_info.parseString("client_secret")
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
Request.Builder()
|
|
|
|
|
.url("https://$instance/oauth/token")
|
|
|
|
|
.post(RequestBody.create(MEDIA_TYPE_FORM_URL_ENCODED, post_content))
|
|
|
|
|
.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}
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val access_token = token_info.parseString("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
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// アクセストークン手動入力でアカウントを更新する場合
|
|
|
|
|
// verify_credentialsを呼び出す
|
2018-01-13 07:15:52 +01:00
|
|
|
|
fun getUserCredential(
|
|
|
|
|
access_token : String,
|
|
|
|
|
tokenInfo : JSONObject = JSONObject()
|
|
|
|
|
) : TootApiResult? {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
2018-01-13 07:15:52 +01:00
|
|
|
|
// 認証されたアカウントのユーザ情報を取得する
|
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/accounts/verify_credentials")
|
|
|
|
|
.header("Authorization", "Bearer $access_token")
|
|
|
|
|
.build()
|
|
|
|
|
}) return result
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val r2 = parseJson(result)
|
2018-01-13 07:15:52 +01:00
|
|
|
|
if(r2?.jsonObject != null) {
|
|
|
|
|
// ユーザ情報を読めたならtokenInfoを保存する
|
|
|
|
|
tokenInfo.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
|
|
|
|
tokenInfo.put("access_token", access_token)
|
|
|
|
|
result.tokenInfo = tokenInfo
|
|
|
|
|
}
|
|
|
|
|
return r2
|
2018-01-12 10:01:25 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01: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 ->
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val error = json.parseString("error")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(error == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val type = json.parseString("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
|
|
|
|
|
user_token = jsonObject.optJSONObject("result")?.optString("token")
|
|
|
|
|
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-01-21 13:46:36 +01:00
|
|
|
|
val url = (mspSearchUrl
|
|
|
|
|
+ "?apikey=" + mspApiKey.encodePercent()
|
|
|
|
|
+ "&utoken=" + user_token.encodePercent()
|
|
|
|
|
+ "&q=" + query.encodePercent()
|
|
|
|
|
+ "&max=" + max_id.encodePercent())
|
|
|
|
|
|
|
|
|
|
Request.Builder().url(url).build()
|
|
|
|
|
}) 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 ->
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val error = json.parseString("error")
|
2018-01-12 10:01:25 +01:00
|
|
|
|
if(error == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
// ユーザトークンがダメなら生成しなおす
|
|
|
|
|
val detail = json.optString("detail")
|
|
|
|
|
if("utoken" == detail) {
|
|
|
|
|
isUserTokenError = true
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val type = json.parseString("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,
|
|
|
|
|
max_id : String // 空文字列、もしくはfromに指定するパラメータ
|
|
|
|
|
) : TootApiResult? {
|
|
|
|
|
|
|
|
|
|
val result = TootApiResult.makeWithCaption("Tootsearch")
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
if(! sendRequest(result) {
|
2018-01-21 13:46:36 +01:00
|
|
|
|
val url = ("https://tootsearch.chotto.moe/api/v1/search"
|
|
|
|
|
+ "?sort=" + "created_at:desc".encodePercent()
|
|
|
|
|
+ "&from=" + max_id.encodePercent()
|
|
|
|
|
+ "&q=" + query.encodePercent())
|
|
|
|
|
|
|
|
|
|
Request.Builder()
|
|
|
|
|
.url(url)
|
|
|
|
|
.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? {
|
2018-05-12 10:17:12 +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-16 21:58:30 +02:00
|
|
|
|
fun requestJson(req : Request) : TootApiResult? {
|
2018-05-12 10:17:12 +02:00
|
|
|
|
val result = TootApiResult.makeWithCaption(req.url().host())
|
|
|
|
|
if(result.error != null) return result
|
2018-08-16 21:58:30 +02:00
|
|
|
|
if(sendRequest(result, progressPath = null) { req }) {
|
2018-05-12 10:17:12 +02:00
|
|
|
|
parseJson(result)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 疑似アカウントでステータス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
|
|
|
|
}
|
|
|
|
|
|
2018-03-10 00:12:40 +01:00
|
|
|
|
fun getHttpBytes(url : String) : TootApiResult? {
|
|
|
|
|
val result = TootApiResult.makeWithCaption(url)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
|
|
|
|
|
if(! sendRequest(result, progressPath = url) {
|
|
|
|
|
Request.Builder().url(url).build()
|
|
|
|
|
}) return result
|
|
|
|
|
return parseBytes(result)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 13:46:36 +01:00
|
|
|
|
fun webSocket(path : String, ws_listener : WebSocketListener) : TootApiResult? {
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val result = TootApiResult.makeWithCaption(instance)
|
|
|
|
|
if(result.error != null) return result
|
|
|
|
|
val account = this.account ?: return TootApiResult("account is null")
|
|
|
|
|
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()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
publishApiProgress(context.getString(R.string.request_api, request.method(), path))
|
|
|
|
|
val ws = httpClient.getWebSocket(request, ws_listener)
|
|
|
|
|
if(isApiCancelled) {
|
|
|
|
|
ws.cancel()
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
result.data = ws
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
2018-01-21 13:46:36 +01:00
|
|
|
|
result.error =
|
|
|
|
|
"${result.caption}: ${ex.withCaption(context.resources, R.string.network_error)}"
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-12 10:01:25 +01:00
|
|
|
|
}
|