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-01-04 19:52:25 +01:00
import org.json.JSONException
import org.json.JSONObject
import jp.juggler.subwaytooter.table.ClientInfo
import jp.juggler.subwaytooter.table.SavedAccount
2018-05-11 15:42:54 +02:00
import jp.juggler.subwaytooter.api.entity.TootInstance
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-08-19 06:19:33 +02:00
const val KEY _IS _MISSKEY = " isMisskey "
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-08-20 19:37:42 +02:00
// // APIからsecretを得られないバグがあるので定数を渡す
// const val appSecretError =
// "This Misskey instance Currently Misskey does not allow client registration from API, please tell me notify instance name that you want login via Subway Tooter."
// val testAppSecretMap = mapOf(
// Pair("misskey.xyz", "NGiWNZFP37WiAee3SGcVe8eSiDyLbbWf")
// , Pair("misskey.jp", "GO45N7JgeEWtlNUS4xRcOFY56JMjUTZk")
// , Pair("msky.cafe", "lvU12i7CXAB5xiqkABwzyJRzdAqhf0k3")
// , Pair("misskey.m544.net", "SLcaqff0Puymh4Fl30JCc09i6uumwJ4t")
// )
2018-08-19 06:19:33 +02:00
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-08-18 12:58:14 +02:00
fun getMspMaxId ( array : JSONArray , old : String ? ) : String ? {
2018-01-12 10:01:25 +01:00
// 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
}
}
2018-08-18 12:58:14 +02:00
// MSPでは終端は分からず、何度もリトライする
return old
2018-01-12 10:01:25 +01:00
}
fun getTootsearchHits ( root : JSONObject ) : JSONArray ? {
val hits = root . optJSONObject ( " hits " )
return hits ?. optJSONArray ( " hits " )
}
// returns the number for "from" parameter of next page.
2018-08-18 12:58:14 +02:00
// returns null if no more next page.
fun getTootsearchMaxId ( root : JSONObject , old : Long ? ) : Long ? {
val size = getTootsearchHits ( root ) ?. length ( ) ?: 0
return when {
size <= 0 -> null
else -> ( old ?: 0L ) + size . toLong ( )
2018-01-12 10:01:25 +01:00
}
}
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 {
2018-08-20 02:07:55 +02:00
val url = response . request ( ) ?. url ( )
2018-08-20 19:37:42 +02:00
if ( url ?. toString ( ) ?. contains ( " misskey " ) == true ) {
2018-08-20 02:07:55 +02:00
log . d ( " Misskey response error: url= $url " )
}
2018-01-12 10:01:25 +01:00
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-08-19 06:19:33 +02:00
fun getScopeArrayMisskey ( @Suppress ( " UNUSED_PARAMETER " ) ti : TootInstance ) =
JSONArray ( ) . apply {
2018-08-24 18:24:11 +02:00
2018-08-19 06:19:33 +02:00
put ( " account-read " )
put ( " account-write " )
2018-08-24 18:24:11 +02:00
put ( " note-read " )
2018-08-19 06:19:33 +02:00
put ( " note-write " )
2018-08-24 18:24:11 +02:00
put ( " reaction-read " )
2018-08-19 06:19:33 +02:00
put ( " reaction-write " )
2018-08-24 18:24:11 +02:00
2018-08-23 07:45:12 +02:00
put ( " following-read " ) // フォロリク申請一覧で使われていた
2018-08-24 18:24:11 +02:00
put ( " following-write " )
2018-08-19 06:19:33 +02:00
put ( " drive-read " )
put ( " drive-write " )
2018-08-24 18:24:11 +02:00
2018-08-19 06:19:33 +02:00
put ( " notification-read " )
put ( " notification-write " )
2018-08-24 18:24:11 +02:00
put ( " favorite-read " )
2018-08-20 19:37:42 +02:00
put ( " favorite-write " )
2018-08-24 18:24:11 +02:00
2018-08-20 19:37:42 +02:00
put ( " account/read " )
put ( " account/write " )
2018-08-23 07:45:12 +02:00
2018-08-24 18:24:11 +02:00
put ( " messaging-read " )
put ( " messaging-write " )
put ( " vote-read " )
put ( " vote-write " )
2018-08-20 19:37:42 +02:00
// https://github.com/syuilo/misskey/issues/2341
2018-08-19 06:19:33 +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-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
2018-08-20 19:37:42 +02:00
// Misskey の /api/notes/favorites/create は 204(no content)を返す。ボディはカラになる。
if ( bodyString ?. isEmpty ( ) != false && response . code ( ) in 200 until 300 ) {
result . bodyString = " "
return " "
}
2018-01-12 10:01:25 +01:00
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-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-20 19:37:42 +02:00
if ( bodyString . isEmpty ( ) ) {
// 204 no content は 空オブジェクトと解釈する
result . data = JSONObject ( )
} else 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
}
2018-08-20 19:37:42 +02:00
} else {
2018-08-21 12:19:02 +02:00
// HTMLならタグを除去する
val ct = response . body ( ) ?. contentType ( )
if ( ct ?. subtype ( ) == " html " ) {
val decoded = DecodeOptions ( ) . decodeHTML ( bodyString ) . toString ( )
bodyString = decoded
}
val sb = StringBuilder ( )
. append ( context . getString ( R . string . response _not _json ) )
. append ( ' ' )
. append ( bodyString )
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 ( " ) " )
val url = response . request ( ) ?. url ( ) ?. toString ( )
if ( url ?. isNotEmpty ( ) == true ) {
sb . append ( ' ' ) . append ( url )
}
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
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-08-19 06:19:33 +02:00
//////////////////////////////////////////////////////////////////////
// misskey authentication
2018-01-04 19:52:25 +01:00
// 疑似アカウントの追加時に、インスタンスの検証を行う
2018-08-19 06:19:33 +02:00
private fun getInstanceInformationMisskey ( ) : 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-19 06:19:33 +02:00
if ( sendRequest ( result ) {
JSONObject ( ) . apply {
put ( " dummy " , 1 )
}
. toPostRequestBuilder ( )
. url ( " https:// $instance /api/meta " )
. build ( )
} ) {
parseJson ( result ) ?: return null
result . jsonObject ?. put ( KEY _IS _MISSKEY , true )
}
return result
}
// インスタンス情報を取得する
2018-08-20 19:37:42 +02:00
internal fun parseInstanceInformation ( result : TootApiResult ? ) : TootApiResult ? {
if ( result != null ) {
val json = result . jsonObject
2018-08-19 06:19:33 +02:00
if ( json != null ) {
val parser = TootParser (
context ,
2018-08-20 19:37:42 +02:00
LinkHelper . newLinkHelper ( instance , isMisskey = json . optBoolean ( KEY _IS _MISSKEY ) )
2018-08-19 06:19:33 +02:00
)
val ti = parser . instance ( json )
if ( ti != null ) {
2018-08-20 19:37:42 +02:00
result . data = ti
2018-08-19 06:19:33 +02:00
} else {
2018-08-20 19:37:42 +02:00
result . setError ( " can't parse data in instance information. " )
2018-08-19 06:19:33 +02:00
}
}
}
2018-08-20 19:37:42 +02:00
return result
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 ) {
JSONObject ( ) . apply {
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-20 19:37:42 +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 ) {
JSONObject ( ) . apply {
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
val url = jsonObject . parseString ( " url " )
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 (
scope _array : JSONArray ,
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 ) {
2018-08-19 06:19:33 +02:00
JSONObject ( ) . apply {
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がアプリ上に保存されているか?
val client _name = if ( clientNameArg . isNotEmpty ( ) ) clientNameArg else DEFAULT _CLIENT _NAME
val client _info = ClientInfo . load ( instance , client _name )
// スコープ一覧を取得する
val scope _array = getScopeArrayMisskey ( ti )
if ( client _info != null && client _info . optBoolean ( KEY _IS _MISSKEY ) ) {
2018-08-20 19:37:42 +02:00
val appSecret = client _info . parseString ( KEY _MISSKEY _APP _SECRET )
2018-08-19 06:19:33 +02:00
val r2 = getAppInfoMisskey ( client _info . parseString ( " id " ) )
val tmpClientInfo = r2 ?. jsonObject
// tmpClientInfo はsecretを含まないので保存してはいけない
if ( tmpClientInfo != null // アプリが登録済みで
&& client _name == tmpClientInfo . parseString ( " name " ) // クライアント名が一致してて
&& tmpClientInfo . optJSONArray ( " permission " ) ?. length ( ) == scope _array . length ( ) // パーミッションが同じ
2018-08-20 19:37:42 +02:00
&& appSecret ?. isNotEmpty ( ) == true
2018-08-19 06:19:33 +02:00
) {
// クライアント情報を再利用する
2018-08-20 19:37:42 +02:00
result . data = prepareBrowserUrlMisskey ( appSecret )
2018-08-19 06:19:33 +02:00
return result
} else {
// XXX appSecretを使ってクライアント情報を削除できるようにするべきだが、該当するAPIが存在しない
}
}
val r2 = registerClientMisskey ( scope _array , client _name )
val jsonObject = r2 ?. jsonObject ?: return r2
2018-08-20 19:37:42 +02:00
val appSecret = jsonObject . parseString ( KEY _MISSKEY _APP _SECRET )
if ( appSecret ?. isEmpty ( ) != false ) {
showToast ( context , true , context . getString ( R . string . cant _get _misskey _app _secret ) )
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
jsonObject . put ( KEY _IS _MISSKEY , true )
jsonObject . put ( KEY _AUTH _VERSION , AUTH _VERSION )
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認証の続きを行う
fun authentication2Misskey ( clientNameArg : String , token : String ) : TootApiResult ? {
val result = TootApiResult . makeWithCaption ( instance )
if ( result . error != null ) return result
val instance = result . caption // same to instance
val client _name = if ( clientNameArg . isNotEmpty ( ) ) clientNameArg else DEFAULT _CLIENT _NAME
@Suppress ( " UNUSED_VARIABLE " )
val client _info = ClientInfo . load ( instance , client _name )
?: return result . setError ( " missing client id " )
2018-08-20 19:37:42 +02:00
val appSecret = client _info . parseString ( KEY _MISSKEY _APP _SECRET )
if ( appSecret ?. isEmpty ( ) != false ) {
return result . setError ( context . getString ( R . string . cant _get _misskey _app _secret ) )
}
2018-08-19 06:19:33 +02:00
if ( ! sendRequest ( result ) {
JSONObject ( ) . apply {
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
val access _token = token _info . parseString ( " accessToken " )
if ( access _token ?. isEmpty ( ) != false ) {
return result . setError ( " missing accessToken in the response. " )
}
val user = token _info . optJSONObject ( " user " )
?: result . setError ( " missing user in the response. " )
token _info . remove ( " user " )
val apiKey = " $access _token $appSecret " . encodeUTF8 ( ) . digestSHA256 ( ) . encodeHexLower ( )
// ユーザ情報を読めたならtokenInfoを保存する
token _info . put ( KEY _IS _MISSKEY , true )
token _info . put ( KEY _AUTH _VERSION , AUTH _VERSION )
token _info . put ( KEY _API _KEY _MISSKEY , apiKey )
// tokenInfoとユーザ情報の入ったresultを返す
result . tokenInfo = token _info
result . data = user
return result
}
//////////////////////////////////////////////////////////////////////
// 疑似アカウントの追加時に、インスタンスの検証を行う
private fun getInstanceInformationMastodon ( ) : TootApiResult ? {
val result = TootApiResult . makeWithCaption ( instance )
if ( result . error != null ) return result
if ( sendRequest ( result ) {
Request . Builder ( ) . url ( " https:// $instance /api/v1/instance " ) . build ( )
2018-08-16 21:58:30 +02:00
}
) {
2018-08-19 06:19:33 +02:00
parseJson ( result ) ?: return null
2018-08-16 21:58:30 +02:00
}
// misskeyの事は忘れて本来のエラー情報を返す
return result
2018-01-04 19:52:25 +01:00
}
2018-08-20 19:37:42 +02:00
2018-01-12 10:01:25 +01:00
// クライアントをタンスに登録
2018-08-19 06:19:33 +02:00
private 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-08-19 06:19:33 +02:00
// client_credentialを無効にする
private fun revokeClientCredential (
2018-05-11 15:42:54 +02:00
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-08-19 06:19:33 +02:00
+ " &state= " + ( if ( account != null ) " db: ${account.db_id} " else " host: $instance " )
2018-01-04 19:52:25 +01:00
+ " &grant_type=authorization_code "
+ " &approval_prompt=force "
// +"&access_type=offline"
)
}
2018-08-19 06:19:33 +02:00
private fun authentication1Mastodon (
clientNameArg : String ,
ti : TootInstance
) : TootApiResult ? {
2018-05-11 15:42:54 +02:00
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がアプリ上に保存されているか?
val client _name = if ( clientNameArg . isNotEmpty ( ) ) clientNameArg else DEFAULT _CLIENT _NAME
val client _info = ClientInfo . load ( instance , client _name )
2018-08-19 06:19:33 +02:00
// スコープ一覧を取得する
val scope _string = getScopeString ( ti )
if ( client _info != null && ! client _info . optBoolean ( KEY _IS _MISSKEY ) ) {
2018-01-04 19:52:25 +01:00
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 )
2018-08-19 06:19:33 +02:00
// XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
2018-05-11 15:42:54 +02:00
} 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
}
2018-08-20 19:37:42 +02:00
// 疑似アカウントの追加時に、インスタンスの検証を行う
fun getInstanceInformation ( ) : TootApiResult ? {
// マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス
val r1 = getInstanceInformationMastodon ( ) ?: return null
if ( r1 . jsonObject != null ) return r1
// misskeyのインスタンス情報を読めたら、それはmisskeyのインスタンス
val r2 = getInstanceInformationMisskey ( ) ?: return null
if ( r2 . jsonObject != null ) return r2
return r1 // 通信エラーの表示ならr1でもr2でも構わないはず
}
2018-08-19 06:19:33 +02:00
// クライアントを登録してブラウザで開くURLを生成する
fun authentication1 ( clientNameArg : String ) : TootApiResult ? {
// マストドンのインスタンス情報
2018-08-20 19:37:42 +02:00
var ri = parseInstanceInformation ( getInstanceInformationMastodon ( ) )
2018-08-19 06:19:33 +02:00
var ti = ri ?. data as ? TootInstance
if ( ti != null && ( ri ?. response ?. code ( ) ?: 0 ) in 200 until 300 ) {
return authentication1Mastodon ( clientNameArg , ti )
}
// misskeyのインスタンス情報
2018-08-20 19:37:42 +02:00
ri = parseInstanceInformation ( getInstanceInformationMisskey ( ) )
2018-08-19 06:19:33 +02:00
ti = ri ?. data as ? TootInstance
if ( ti != null && ( ri ?. response ?. code ( ) ?: 0 ) in 200 until 300 ) {
return authentication1Misskey ( clientNameArg , ti )
}
return ri
}
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
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 (
2018-08-20 02:07:55 +02:00
access _token : String
2018-08-20 19:37:42 +02:00
, tokenInfo : JSONObject = JSONObject ( )
, isMisskey : Boolean = false
2018-01-13 07:15:52 +01:00
) : TootApiResult ? {
2018-08-20 19:37:42 +02:00
if ( isMisskey ) {
2018-08-20 02:07:55 +02:00
val result = TootApiResult . makeWithCaption ( instance )
if ( result . error != null ) return result
// 認証されたアカウントのユーザ情報を取得する
if ( ! sendRequest ( result ) {
JSONObject ( )
2018-08-20 19:37:42 +02:00
. 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を保存する
tokenInfo . put ( KEY _AUTH _VERSION , AUTH _VERSION )
2018-08-20 19:37:42 +02:00
tokenInfo . put ( KEY _API _KEY _MISSKEY , access _token )
tokenInfo . put ( KEY _IS _MISSKEY , true )
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を保存する
tokenInfo . put ( KEY _AUTH _VERSION , AUTH _VERSION )
tokenInfo . put ( " access_token " , access _token )
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-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 ->
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-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 ->
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 ,
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 ? {
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-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 }) {
// parseJson(result)
// }
// 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
}
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
}
2018-08-20 19:37:42 +02:00