package jp.juggler.subwaytooter.api import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.util.* import jp.juggler.util.data.* import jp.juggler.util.log.LogCategory import okhttp3.Response import okhttp3.WebSocket open class TootApiResult( @Suppress("unused") val dummy: Int = 0, var error: String? = null, var response: Response? = null, var caption: String = "?", var bodyString: String? = null, ) { companion object { private val log = LogCategory("TootApiResult") val reWhiteSpace = """\s+""".asciiPattern() private val reLinkURL = """<([^>]+)>;\s*rel="([^"]+)"""".asciiPattern() fun makeWithCaption(apiHost: Host?) = makeWithCaption(apiHost?.pretty) fun makeWithCaption(caption: String?) = TootApiResult().apply { when (caption) { null, "" -> { log.e("makeWithCaption: missing caption!") error = "missing instance name" } else -> this.caption = caption } } } var requestInfo = "" var tokenInfo: JsonObject? = null var data: Any? = null set(value) { if (value is JsonArray) { parseLinkHeader(response, value) } field = value } val jsonObject: JsonObject? get() = data as? JsonObject val jsonArray: JsonArray? get() = data as? JsonArray val string: String? get() = data as? String var linkOlder: String? = null // より古いデータへのリンク var linkNewer: String? = null // より新しいデータへの constructor() : this(0) constructor(error: String) : this(0, error = error) constructor(socket: WebSocket) : this(0) { this.data = socket } constructor(response: Response, error: String) : this(0, error, response) constructor(response: Response, bodyString: String, data: Any?) : this( 0, response = response, bodyString = bodyString ) { this.data = data } // return result.setError(...) と書きたい fun setError(error: String) = also { it.error = error } private fun parseLinkHeader(response: Response?, array: JsonArray) { response ?: return log.d("array size=${array.size}") // https://handon.club/@highemerly/109755355021758238 // https://mastodon.juggler.jp/@tateisu/109756228563804507 // リンクヘッダが複数ある場合がある val linkHeaders = response.headers("Link") if (linkHeaders.isEmpty()) { log.d("missing Link header") } else { for (sv in linkHeaders) { // Link: ; rel="next", // ; rel="prev" val m = reLinkURL.matcher(sv) while (m.find()) { val url = m.groupEx(1) val rel = m.groupEx(2) log.d("Link: $rel $url") when (rel) { "next" -> linkOlder = url "prev" -> linkNewer = url } } } } } // アカウント作成APIのdetailsを読むため、エラー応答のjsonオブジェクトを保持する private var errorJson: JsonObject? = null internal fun simplifyErrorHtml( sv: String, jsonErrorParser: (json: JsonObject) -> String? = TootApiClient.DEFAULT_JSON_ERROR_PARSER, ): String { val response = this.response!! // JsonObjectとして解釈できるならエラーメッセージを検出する try { val json = sv.decodeJsonObject() this.errorJson = json jsonErrorParser(json)?.notEmpty()?.let { return it } } catch (_: Throwable) { } // HTMLならタグの除去を試みる val ct = response.body?.contentType() if (ct?.subtype == "html") { val decoded = DecodeOptions().decodeHTML(sv).toString() return reWhiteSpace.matcher(decoded).replaceAll(" ").trim() } // XXX: Amazon S3 が403を返した場合にcontent-typeが?/xmlでserverがAmazonならXMLをパースしてエラーを整形することもできるが、多分必要ない return reWhiteSpace.matcher(sv).replaceAll(" ").trim() } fun parseErrorResponse( bodyString: String? = null, jsonErrorParser: (json: JsonObject) -> String? = TootApiClient.DEFAULT_JSON_ERROR_PARSER, ) { val response = this.response!! val sb = StringBuilder() try { // body は既に読み終わっているか、そうでなければこれから読む if (bodyString != null) { sb.append(simplifyErrorHtml(bodyString, jsonErrorParser)) } else { try { val string = response.body?.string() if (string != null) { sb.append(simplifyErrorHtml(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(response.code.toString()) val message = response.message if (message.isNotEmpty()) sb.append(' ').append(message) sb.append(")") if (caption.isNotEmpty()) { sb.append(' ').append(caption) } } catch (ex: Throwable) { log.e(ex, "parseErrorResponse failed.") } this.error = sb.toString().replace("\n+".toRegex(), "\n") } }