mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-01-27 09:11:23 +01:00
update
This commit is contained in:
parent
f558a1a42d
commit
0ef6186f64
@ -79,6 +79,7 @@ dependencies {
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
testCompile 'junit:junit:4.12' // しばらくはjunitと併用
|
||||
|
||||
|
||||
compile 'uk.co.chrisjenx:calligraphy:2.3.0'
|
||||
compile 'com.github.woxthebox:draglistview:1.5.1'
|
||||
compile 'com.github.omadahealth:swipy:1.2.3@aar'
|
||||
@ -86,6 +87,8 @@ dependencies {
|
||||
compile 'com.github.kenglxn.QRGen:android:2.3.0'
|
||||
|
||||
compile 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.9.1'
|
||||
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.9.1'
|
||||
|
||||
compile 'commons-io:commons-io:2.6'
|
||||
|
||||
|
@ -0,0 +1,234 @@
|
||||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
import android.test.mock.MockContext
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import org.json.JSONObject
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TestDuplicateMap {
|
||||
|
||||
private val parser = TootParser(
|
||||
MockContext(),
|
||||
SavedAccount(
|
||||
db_id = 1,
|
||||
acct = "user1@host1",
|
||||
hostArg = null
|
||||
)
|
||||
)
|
||||
|
||||
private val generatedItems = ArrayList<Any>()
|
||||
|
||||
private fun genStatus(
|
||||
parser : TootParser,
|
||||
accountJson : JSONObject,
|
||||
statusId : Long,
|
||||
uri : String?,
|
||||
url : String?
|
||||
):TootStatus{
|
||||
val itemJson = JSONObject()
|
||||
|
||||
itemJson.apply {
|
||||
put("account", accountJson)
|
||||
put("id", statusId)
|
||||
if(uri != null) put("uri", uri)
|
||||
if(url != null) put("url", url)
|
||||
}
|
||||
|
||||
return TootStatus(
|
||||
parser,
|
||||
itemJson,
|
||||
serviceType = ServiceType.MASTODON
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkStatus(
|
||||
map : DuplicateMap,
|
||||
parser : TootParser,
|
||||
accountJson : JSONObject,
|
||||
statusId : Long,
|
||||
uri : String?,
|
||||
url : String?
|
||||
) {
|
||||
val item = genStatus(parser,accountJson,statusId,uri,url)
|
||||
assertNotNull(item)
|
||||
generatedItems.add(item)
|
||||
assertEquals(false, map.isDuplicate(item))
|
||||
assertEquals(true, map.isDuplicate(item))
|
||||
}
|
||||
|
||||
private fun testDuplicateStatus() {
|
||||
|
||||
val account1Json = JSONObject()
|
||||
account1Json.apply {
|
||||
put("username", "user1")
|
||||
put("acct", "user1")
|
||||
put("id", 1L)
|
||||
put("url", "http://${parser.accessInfo.host}/@user1")
|
||||
}
|
||||
|
||||
val account1 = TootAccount(
|
||||
parser.context,
|
||||
parser.accessInfo,
|
||||
src = account1Json,
|
||||
serviceType = ServiceType.MASTODON
|
||||
)
|
||||
assertNotNull(account1)
|
||||
|
||||
val map = DuplicateMap()
|
||||
|
||||
// 普通のステータス
|
||||
checkStatus(
|
||||
map,
|
||||
parser,
|
||||
account1Json,
|
||||
1L,
|
||||
"http://${parser.accessInfo.host}/@${account1.username}/1",
|
||||
"http://${parser.accessInfo.host}/@${account1.username}/1"
|
||||
)
|
||||
// 別のステータス
|
||||
checkStatus(
|
||||
map,
|
||||
parser,
|
||||
account1Json,
|
||||
2L,
|
||||
"http://${parser.accessInfo.host}/@${account1.username}/2",
|
||||
"http://${parser.accessInfo.host}/@${account1.username}/2"
|
||||
)
|
||||
// 今度はuriがない
|
||||
checkStatus(
|
||||
map,
|
||||
parser,
|
||||
account1Json,
|
||||
3L,
|
||||
null, // "http://${parser.accessInfo.host}/@${account1.username}/3",
|
||||
"http://${parser.accessInfo.host}/@${account1.username}/3"
|
||||
)
|
||||
// 今度はuriとURLがない
|
||||
checkStatus(
|
||||
map,
|
||||
parser,
|
||||
account1Json,
|
||||
4L,
|
||||
null, // "http://${parser.accessInfo.host}/@${account1.username}/4",
|
||||
null //"http://${parser.accessInfo.host}/@${account1.username}/4"
|
||||
)
|
||||
// 今度はIDがおかしい
|
||||
checkStatus(
|
||||
map,
|
||||
parser,
|
||||
account1Json,
|
||||
TootStatus.INVALID_ID,
|
||||
null, // "http://${parser.accessInfo.host}/@${account1.username}/4",
|
||||
null //"http://${parser.accessInfo.host}/@${account1.username}/4"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun checkNotification(
|
||||
map : DuplicateMap,
|
||||
parser : TootParser,
|
||||
id : Long
|
||||
) {
|
||||
val itemJson = JSONObject()
|
||||
|
||||
itemJson.apply {
|
||||
put("type", TootNotification.TYPE_MENTION)
|
||||
put("id", id)
|
||||
}
|
||||
|
||||
val item = TootNotification( parser,itemJson )
|
||||
assertNotNull(item)
|
||||
generatedItems.add(item)
|
||||
assertEquals(false, map.isDuplicate(item))
|
||||
assertEquals(true, map.isDuplicate(item))
|
||||
}
|
||||
|
||||
private fun testDuplicateNotification() {
|
||||
val map = DuplicateMap()
|
||||
checkNotification(map,parser,0L)
|
||||
checkNotification(map,parser,1L)
|
||||
checkNotification(map,parser,2L)
|
||||
checkNotification(map,parser,3L)
|
||||
}
|
||||
|
||||
private fun checkReport(
|
||||
map : DuplicateMap,
|
||||
id : Long
|
||||
) {
|
||||
val item = TootReport( id,"eat" )
|
||||
|
||||
assertNotNull(item)
|
||||
generatedItems.add(item)
|
||||
assertEquals(false, map.isDuplicate(item))
|
||||
assertEquals(true, map.isDuplicate(item))
|
||||
}
|
||||
|
||||
|
||||
private fun testDuplicateReport() {
|
||||
val map = DuplicateMap()
|
||||
checkReport(map,0L)
|
||||
checkReport(map,1L)
|
||||
checkReport(map,2L)
|
||||
checkReport(map,3L)
|
||||
}
|
||||
|
||||
private fun checkAccount(
|
||||
map : DuplicateMap,
|
||||
parser : TootParser,
|
||||
id : Long
|
||||
) {
|
||||
|
||||
val itemJson = JSONObject()
|
||||
itemJson.apply {
|
||||
put("username", "user$id")
|
||||
put("acct", "user$id")
|
||||
put("id", id)
|
||||
put("url", "http://${parser.accessInfo.host}/@user$id")
|
||||
}
|
||||
|
||||
val item = TootAccount(
|
||||
parser.context,
|
||||
parser.accessInfo,
|
||||
src = itemJson,
|
||||
serviceType = ServiceType.MASTODON
|
||||
)
|
||||
assertNotNull(item)
|
||||
generatedItems.add(item)
|
||||
assertEquals(false, map.isDuplicate(item))
|
||||
assertEquals(true, map.isDuplicate(item))
|
||||
}
|
||||
|
||||
|
||||
private fun testDuplicateAccount() {
|
||||
val map = DuplicateMap()
|
||||
checkAccount(map,parser,0L)
|
||||
checkAccount(map,parser,1L)
|
||||
checkAccount(map,parser,2L)
|
||||
checkAccount(map,parser,3L)
|
||||
}
|
||||
|
||||
@Test fun testFilterList(){
|
||||
generatedItems.clear()
|
||||
testDuplicateStatus()
|
||||
testDuplicateNotification()
|
||||
testDuplicateReport()
|
||||
testDuplicateAccount()
|
||||
|
||||
val map = DuplicateMap()
|
||||
|
||||
val dst = map.filterDuplicate( generatedItems)
|
||||
assertEquals( generatedItems.size,dst.size)
|
||||
|
||||
val dst2 = map.filterDuplicate( generatedItems)
|
||||
assertEquals( 0,dst2.size)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,96 @@
|
||||
package jp.juggler.subwaytooter.mock
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class MockSharedPreferences(
|
||||
val map : HashMap<String, Any> = HashMap()
|
||||
) : SharedPreferences {
|
||||
|
||||
override fun contains(key : String?) = map.contains(key)
|
||||
|
||||
override fun getBoolean(key : String?, defValue : Boolean)
|
||||
= map.get(key) as? Boolean ?: defValue
|
||||
|
||||
override fun getInt(key : String?, defValue : Int)
|
||||
= map.get(key) as? Int ?: defValue
|
||||
|
||||
override fun getLong(key : String?, defValue : Long)
|
||||
= map.get(key) as? Long ?: defValue
|
||||
|
||||
override fun getFloat(key : String?, defValue : Float)
|
||||
= map.get(key) as? Float ?: defValue
|
||||
|
||||
override fun getString(key : String?, defValue : String?)
|
||||
= map.get(key) as? String ?: defValue
|
||||
|
||||
override fun getStringSet(key : String?, defValues : MutableSet<String>?)
|
||||
= map.get(key) as? MutableSet<String> ?: defValues
|
||||
|
||||
|
||||
override fun edit() : SharedPreferences.Editor {
|
||||
return Editor(this)
|
||||
}
|
||||
|
||||
override fun getAll() : MutableMap<String, *> {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun registerOnSharedPreferenceChangeListener(
|
||||
listener : SharedPreferences.OnSharedPreferenceChangeListener?
|
||||
) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
override fun unregisterOnSharedPreferenceChangeListener(
|
||||
listener : SharedPreferences.OnSharedPreferenceChangeListener?
|
||||
) {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val REMOVED_OBJECT = Any()
|
||||
}
|
||||
|
||||
class Editor(private val pref : MockSharedPreferences) : SharedPreferences.Editor {
|
||||
private val changeSet = HashMap<String, Any>()
|
||||
|
||||
override fun commit() : Boolean {
|
||||
for((k, v) in changeSet) {
|
||||
if(v === REMOVED_OBJECT) {
|
||||
pref.map.remove(k)
|
||||
} else {
|
||||
pref.map.put(k, v)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun apply() {
|
||||
commit()
|
||||
}
|
||||
|
||||
override fun clear() : SharedPreferences.Editor {
|
||||
changeSet.clear()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun remove(key : String) : SharedPreferences.Editor {
|
||||
changeSet.put(key, REMOVED_OBJECT)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun putAny(k : String, v : Any?) : SharedPreferences.Editor {
|
||||
changeSet.put(k, v ?: REMOVED_OBJECT)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun putLong(key : String, value : Long) = putAny(key, value)
|
||||
override fun putInt(key : String, value : Int) = putAny(key, value)
|
||||
override fun putBoolean(key : String, value : Boolean) = putAny(key, value)
|
||||
override fun putFloat(key : String, value : Float) = putAny(key, value)
|
||||
override fun putString(key : String, value : String?) = putAny(key, value)
|
||||
override fun putStringSet(key : String, value : MutableSet<String>?) = putAny(key, value)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -12,6 +12,7 @@ import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
|
||||
import org.hjson.JsonValue
|
||||
|
||||
@ -185,28 +186,21 @@ class ActCustomStreamListener : AppCompatActivity(), View.OnClickListener, TextW
|
||||
var call = App1.ok_http_client.newCall(builder.build())
|
||||
|
||||
val response = call.execute()
|
||||
if(! response.isSuccessful) {
|
||||
addLog(Utils.formatResponse(response, "Can't get configuration from URL."))
|
||||
break
|
||||
}
|
||||
|
||||
val bodyString : String?
|
||||
try {
|
||||
bodyString = response.body()?.string()
|
||||
val bodyString : String? = try {
|
||||
response.body()?.string()
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
addLog("Can't get content body")
|
||||
null
|
||||
}
|
||||
|
||||
if(! response.isSuccessful || bodyString?.isEmpty() != false ){
|
||||
addLog(TootApiClient.formatResponse(response, "Can't get configuration from URL.",bodyString))
|
||||
break
|
||||
}
|
||||
|
||||
if(bodyString == null) {
|
||||
addLog("content body is null")
|
||||
break
|
||||
}
|
||||
|
||||
val jv : JsonValue
|
||||
try {
|
||||
jv = JsonValue.readHjson(bodyString)
|
||||
val jv : JsonValue = try {
|
||||
JsonValue.readHjson(bodyString)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
addLog(Utils.formatError(ex, "Can't parse configuration data."))
|
||||
|
@ -1394,7 +1394,7 @@ class ActMain : AppCompatActivity()
|
||||
val sa = SavedAccount.loadAccount(this@ActMain, dataId)
|
||||
?: return TootApiResult("missing account db_id=" + dataId)
|
||||
this.sa = sa
|
||||
client.setAccount(sa)
|
||||
client.account = sa
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(Utils.formatError(ex, "invalid state"))
|
||||
@ -1402,7 +1402,7 @@ class ActMain : AppCompatActivity()
|
||||
|
||||
} else if(sv.startsWith("host:")) {
|
||||
val host = sv.substring(5)
|
||||
client.setInstance(host)
|
||||
client.instance =host
|
||||
}
|
||||
|
||||
if(client.instance == null) {
|
||||
|
@ -376,7 +376,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
|
||||
}
|
||||
|
||||
if(! response.isSuccessful) {
|
||||
return TootApiResult(Utils.formatResponse(response, "response error"))
|
||||
return TootApiResult(TootApiClient.formatResponse(response, "response error"))
|
||||
}
|
||||
|
||||
return try {
|
||||
|
@ -187,7 +187,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
|
||||
if(response.isSuccessful) {
|
||||
return true
|
||||
}
|
||||
log.e(Utils.formatResponse(response, "check_exist failed."))
|
||||
log.e(TootApiClient.formatResponse(response, "check_exist failed."))
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
@ -1739,7 +1739,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
|
||||
this.account = account
|
||||
|
||||
// アカウントがあるなら基本的にはすべての情報を復元できるはずだが、いくつか確認が必要だ
|
||||
val api_client = TootApiClient(this@ActPost, object : TootApiCallback {
|
||||
val api_client = TootApiClient(this@ActPost, callback=object : TootApiCallback {
|
||||
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled
|
||||
@ -1749,7 +1749,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
|
||||
}
|
||||
})
|
||||
|
||||
api_client.setAccount(account)
|
||||
api_client.account = account
|
||||
|
||||
if(in_reply_to_id != - 1L) {
|
||||
val result = api_client.request("/api/v1/statuses/" + in_reply_to_id)
|
||||
|
@ -21,6 +21,7 @@ import com.bumptech.glide.load.engine.executor.GlideExecutor
|
||||
import com.bumptech.glide.load.engine.executor.GlideExecutor.newDiskCacheExecutor
|
||||
import com.bumptech.glide.load.engine.executor.GlideExecutor.newSourceExecutor
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
@ -451,7 +452,7 @@ class App1 : Application() {
|
||||
}
|
||||
|
||||
if(! response.isSuccessful) {
|
||||
log.e(Utils.formatResponse(response, "getHttp response error."))
|
||||
log.e(TootApiClient.formatResponse(response, "getHttp response error."))
|
||||
return null
|
||||
}
|
||||
|
||||
@ -480,7 +481,7 @@ class App1 : Application() {
|
||||
}
|
||||
|
||||
if(! response.isSuccessful) {
|
||||
log.e(Utils.formatResponse(response, "getHttp response error."))
|
||||
log.e(TootApiClient.formatResponse(response, "getHttp response error."))
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ class Column(
|
||||
val context : Context,
|
||||
val access_info : SavedAccount,
|
||||
val column_type : Int
|
||||
) : StreamReader.Callback {
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("Column")
|
||||
@ -1227,7 +1227,12 @@ class Column(
|
||||
internal var list_tmp : ArrayList<Any>? = null
|
||||
|
||||
internal fun getInstanceInformation(client : TootApiClient, instance_name : String?) : TootApiResult? {
|
||||
if(instance_name != null) client.setInstance(instance_name)
|
||||
if( instance_name != null ){
|
||||
// 「インスタンス情報」カラムをNAアカウントで開く場合
|
||||
client.instance = instance_name
|
||||
}else{
|
||||
// カラムに紐付けられたアカウントのタンスのインスタンス情報
|
||||
}
|
||||
val result = client.request("/api/v1/instance")
|
||||
val jsonObject = result?.jsonObject
|
||||
if(jsonObject != null) {
|
||||
@ -1267,7 +1272,7 @@ class Column(
|
||||
//
|
||||
val delimiter = if(- 1 != path_base.indexOf('?')) '&' else '?'
|
||||
while(true) {
|
||||
if(client.isCancelled) {
|
||||
if(client.isApiCancelled) {
|
||||
log.d("loading-statuses: cancelled.")
|
||||
break
|
||||
}
|
||||
@ -1367,7 +1372,7 @@ class Column(
|
||||
//
|
||||
val delimiter = if(- 1 != path_base.indexOf('?')) '&' else '?'
|
||||
while(true) {
|
||||
if(client.isCancelled) {
|
||||
if(client.isApiCancelled) {
|
||||
log.d("loading-notifications: cancelled.")
|
||||
break
|
||||
}
|
||||
@ -1413,7 +1418,7 @@ class Column(
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params : Void) : TootApiResult? {
|
||||
val client = TootApiClient(context, object : TootApiCallback {
|
||||
val client = TootApiClient(context, callback=object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
@ -1426,7 +1431,7 @@ class Column(
|
||||
}
|
||||
})
|
||||
|
||||
client.setAccount(access_info)
|
||||
client.account =access_info
|
||||
|
||||
try {
|
||||
var result : TootApiResult?
|
||||
@ -1454,7 +1459,9 @@ class Column(
|
||||
TAB_STATUS -> {
|
||||
|
||||
var instance = access_info.instance
|
||||
if(access_info.isPseudo || instance == null) {
|
||||
// まだ取得してない
|
||||
// 疑似アカウントの場合は過去のデータが別タンスかもしれない?
|
||||
if( instance == null || access_info.isPseudo ) {
|
||||
val r2 = getInstanceInformation(client, null)
|
||||
if(instance_tmp != null) {
|
||||
instance = instance_tmp
|
||||
@ -1587,23 +1594,11 @@ class Column(
|
||||
list_tmp = ArrayList()
|
||||
result = TootApiResult()
|
||||
} else {
|
||||
result = MSPClient.search(context, search_query, max_id, object : TootApiCallback {
|
||||
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
override fun publishApiProgress(s : String) {
|
||||
Utils.runOnMainThread {
|
||||
if(isCancelled) return@runOnMainThread
|
||||
task_progress = s
|
||||
fireShowContent()
|
||||
}
|
||||
}
|
||||
})
|
||||
result = client.searchMsp( search_query, max_id )
|
||||
val jsonArray = result?.jsonArray
|
||||
if(jsonArray != null) {
|
||||
// max_id の更新
|
||||
max_id = MSPClient.getMaxId(jsonArray, max_id)
|
||||
max_id = TootApiClient.getMspMaxId(jsonArray, max_id)
|
||||
// リストデータの用意
|
||||
val search_result = TootStatus.parseList(parser, jsonArray, serviceType = ServiceType.MSP)
|
||||
list_tmp = addWithFilterStatus(null, search_result)
|
||||
@ -1619,22 +1614,11 @@ class Column(
|
||||
list_tmp = ArrayList()
|
||||
result = TootApiResult()
|
||||
} else {
|
||||
result = TootsearchClient.search(context, search_query, max_id, object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
override fun publishApiProgress(s : String) {
|
||||
Utils.runOnMainThread {
|
||||
if(isCancelled) return@runOnMainThread
|
||||
task_progress = s
|
||||
fireShowContent()
|
||||
}
|
||||
}
|
||||
})
|
||||
result = client.searchTootsearch( search_query, max_id)
|
||||
val jsonObject = result?.jsonObject
|
||||
if(jsonObject != null) {
|
||||
// max_id の更新
|
||||
max_id = TootsearchClient.getMaxId(jsonObject, max_id)
|
||||
max_id = TootApiClient.getTootsearchMaxId(jsonObject, max_id)
|
||||
// リストデータの用意
|
||||
val search_result = TootStatus.parseListTootsearch(parser, jsonObject)
|
||||
this.list_tmp = addWithFilterStatus(null, search_result)
|
||||
@ -2256,7 +2240,7 @@ class Column(
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params : Void) : TootApiResult? {
|
||||
val client = TootApiClient(context, object : TootApiCallback {
|
||||
val client = TootApiClient(context, callback=object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
@ -2269,7 +2253,7 @@ class Column(
|
||||
}
|
||||
})
|
||||
|
||||
client.setAccount(access_info)
|
||||
client.account =access_info
|
||||
try {
|
||||
|
||||
return when(column_type) {
|
||||
@ -2342,22 +2326,11 @@ class Column(
|
||||
list_tmp = ArrayList()
|
||||
result = TootApiResult(context.getString(R.string.end_of_list))
|
||||
} else {
|
||||
result = MSPClient.search(context, search_query, max_id, object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
override fun publishApiProgress(s : String) {
|
||||
Utils.runOnMainThread {
|
||||
if(isCancelled) return@runOnMainThread
|
||||
task_progress = s
|
||||
fireShowContent()
|
||||
}
|
||||
}
|
||||
})
|
||||
result = client.searchMsp( search_query, max_id)
|
||||
val jsonArray = result?.jsonArray
|
||||
if(jsonArray != null) {
|
||||
// max_id の更新
|
||||
max_id = MSPClient.getMaxId(jsonArray, max_id)
|
||||
max_id = TootApiClient.getMspMaxId(jsonArray, max_id)
|
||||
// リストデータの用意
|
||||
val search_result = TootStatus.parseList(parser, jsonArray, serviceType = ServiceType.MSP)
|
||||
list_tmp = addWithFilterStatus(list_tmp, search_result)
|
||||
@ -2375,22 +2348,11 @@ class Column(
|
||||
list_tmp = ArrayList()
|
||||
result = TootApiResult(context.getString(R.string.end_of_list))
|
||||
} else {
|
||||
result = TootsearchClient.search(context, search_query, max_id, object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
override fun publishApiProgress(s : String) {
|
||||
Utils.runOnMainThread {
|
||||
if(isCancelled) return@runOnMainThread
|
||||
task_progress = s
|
||||
fireShowContent()
|
||||
}
|
||||
}
|
||||
})
|
||||
result = client.searchTootsearch( search_query, max_id)
|
||||
val jsonObject = result?.jsonObject
|
||||
if(jsonObject != null) {
|
||||
// max_id の更新
|
||||
max_id = TootsearchClient.getMaxId(jsonObject, max_id)
|
||||
max_id = TootApiClient.getTootsearchMaxId(jsonObject, max_id)
|
||||
// リストデータの用意
|
||||
val search_result = TootStatus.parseListTootsearch(parser, jsonObject)
|
||||
list_tmp = addWithFilterStatus(list_tmp, search_result)
|
||||
@ -2746,7 +2708,7 @@ class Column(
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params : Void) : TootApiResult? {
|
||||
val client = TootApiClient(context, object : TootApiCallback {
|
||||
val client = TootApiClient(context, callback=object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = isCancelled || is_dispose.get()
|
||||
|
||||
@ -2759,7 +2721,7 @@ class Column(
|
||||
}
|
||||
})
|
||||
|
||||
client.setAccount(access_info)
|
||||
client.account = access_info
|
||||
|
||||
try {
|
||||
return when(column_type) {
|
||||
@ -2948,31 +2910,6 @@ class Column(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStreamingMessage(event_type : String, item : Any?) {
|
||||
if(is_dispose.get()) return
|
||||
|
||||
if("delete" == event_type) {
|
||||
if(item is Long) {
|
||||
removeStatus(access_info, item)
|
||||
}
|
||||
} else {
|
||||
if(item is TootNotification) {
|
||||
if(column_type != TYPE_NOTIFICATIONS) return
|
||||
if(isFiltered(item)) return
|
||||
} else if(item is TootStatus) {
|
||||
if(column_type == TYPE_NOTIFICATIONS) return
|
||||
if(column_type == TYPE_LOCAL && item.account.acct.indexOf('@') != - 1) return
|
||||
if(isFiltered(item)) return
|
||||
|
||||
if(this.enable_speech) {
|
||||
App1.getAppState(context).addSpeech(item.reblog ?: item)
|
||||
}
|
||||
}
|
||||
stream_data_queue.addFirst(item)
|
||||
proc_stream_data.run()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onStart(callback : Callback) {
|
||||
this.callback_ref = WeakReference(callback)
|
||||
|
||||
@ -3118,7 +3055,7 @@ class Column(
|
||||
stream_data_queue.clear()
|
||||
|
||||
app_state.stream_reader.register(
|
||||
access_info, stream_path, highlight_trie, this
|
||||
access_info, stream_path, highlight_trie, onStreamingMessage
|
||||
)
|
||||
}
|
||||
|
||||
@ -3128,18 +3065,47 @@ class Column(
|
||||
val stream_path = streamPath
|
||||
if(stream_path != null) {
|
||||
app_state.stream_reader.unregister(
|
||||
access_info, stream_path, this
|
||||
access_info, stream_path, onStreamingMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val proc_stream_data = object : Runnable {
|
||||
private val onStreamingMessage = fun(event_type : String, item : Any?) {
|
||||
if(is_dispose.get()) return
|
||||
if("delete" == event_type) {
|
||||
if(item is Long) {
|
||||
removeStatus(access_info, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if(item is TootNotification) {
|
||||
if(column_type != TYPE_NOTIFICATIONS) return
|
||||
if(isFiltered(item)) return
|
||||
} else if(item is TootStatus) {
|
||||
if(column_type == TYPE_NOTIFICATIONS) return
|
||||
if(column_type == TYPE_LOCAL && item.account.acct.indexOf('@') != - 1) return
|
||||
if(isFiltered(item)) return
|
||||
|
||||
if(this.enable_speech) {
|
||||
App1.getAppState(context).addSpeech(item.reblog ?: item)
|
||||
}
|
||||
}
|
||||
|
||||
stream_data_queue.addFirst(item)
|
||||
mergeStreamingMessage.run()
|
||||
}
|
||||
|
||||
private val mergeStreamingMessage = object : Runnable {
|
||||
override fun run() {
|
||||
App1.getAppState(context).handler.removeCallbacks(this)
|
||||
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
|
||||
// 前回マージしてから暫くは待機する
|
||||
val remain = last_show_stream_data + 333L - now
|
||||
if(remain > 0) {
|
||||
App1.getAppState(context).handler.postDelayed(this, 333L)
|
||||
App1.getAppState(context).handler.postDelayed(this, remain)
|
||||
return
|
||||
}
|
||||
last_show_stream_data = now
|
||||
@ -3147,29 +3113,42 @@ class Column(
|
||||
val list_new = duplicate_map.filterDuplicate(stream_data_queue)
|
||||
stream_data_queue.clear()
|
||||
|
||||
if(list_new.isEmpty()) {
|
||||
return
|
||||
} else {
|
||||
if(column_type == TYPE_NOTIFICATIONS) {
|
||||
val list = ArrayList<TootNotification>()
|
||||
for(o in list_new) {
|
||||
if(o is TootNotification) {
|
||||
list.add(o)
|
||||
}
|
||||
}
|
||||
if(! list.isEmpty()) {
|
||||
PollingWorker.injectData(context, access_info.db_id, list)
|
||||
if(list_new.isEmpty()) return
|
||||
|
||||
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
|
||||
if(column_type == TYPE_NOTIFICATIONS) {
|
||||
val list = ArrayList<TootNotification>()
|
||||
for(o in list_new) {
|
||||
if(o is TootNotification) {
|
||||
list.add(o)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
since_id = getId(list_new[0]).toString()
|
||||
} catch(ex : Throwable) {
|
||||
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
||||
log.e(ex, "getId() failed. o=", list_new[0])
|
||||
if(! list.isEmpty()) {
|
||||
PollingWorker.injectData(context, access_info.db_id, list)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 最新のIDをsince_idとして覚える(ソートはしない)
|
||||
var new_id_max = Long.MIN_VALUE
|
||||
var new_id_min = Long.MAX_VALUE
|
||||
for(o in list_new) {
|
||||
try {
|
||||
val id = getId(o)
|
||||
if(id < 0) continue
|
||||
if(id > new_id_max) new_id_max = id
|
||||
if(id < new_id_min) new_id_min = id
|
||||
} catch(ex : Throwable) {
|
||||
// IDを取得できないタイプのオブジェクトだった
|
||||
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
if(new_id_max != Long.MAX_VALUE) {
|
||||
since_id = new_id_max.toString()
|
||||
// XXX: コレはリフレッシュ時に取得漏れを引き起こすのでは…?
|
||||
// しかしコレなしだとリフレッシュ時に大量に読むことになる…
|
||||
}
|
||||
|
||||
val holder = viewHolder
|
||||
|
||||
// 事前にスクロール位置を覚えておく
|
||||
@ -3191,14 +3170,14 @@ class Column(
|
||||
}
|
||||
}
|
||||
|
||||
// 画面復帰時の自動リフレッシュではギャップが残る可能性がある
|
||||
if(bPutGap) {
|
||||
bPutGap = false
|
||||
try {
|
||||
if(list_new.size > 0 && list_data.size > 0) {
|
||||
val max = getId(list_new[list_new.size - 1])
|
||||
if(list_data.size > 0 && new_id_min != Long.MAX_VALUE) {
|
||||
val since = getId(list_data[0])
|
||||
if(max > since) {
|
||||
val gap = TootGap(max, since)
|
||||
if(new_id_min > since) {
|
||||
val gap = TootGap(new_id_min, since)
|
||||
list_new.add(gap)
|
||||
}
|
||||
}
|
||||
|
@ -51,12 +51,7 @@ import jp.juggler.subwaytooter.table.MutedApp
|
||||
import jp.juggler.subwaytooter.table.MutedWord
|
||||
import jp.juggler.subwaytooter.table.NotificationTracking
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
import jp.juggler.subwaytooter.util.NotificationHelper
|
||||
import jp.juggler.subwaytooter.util.TaskList
|
||||
import jp.juggler.subwaytooter.util.Utils
|
||||
import jp.juggler.subwaytooter.util.WordTrieTree
|
||||
import jp.juggler.subwaytooter.util.WorkerBase
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
@ -755,7 +750,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
val body = response.body()?.string()
|
||||
|
||||
if(! response.isSuccessful || body?.isEmpty() != false ) {
|
||||
log.e(Utils.formatResponse(response, "getInstallId: get /counter failed."))
|
||||
log.e(TootApiClient.formatResponse(response, "getInstallId: get /counter failed."))
|
||||
return null
|
||||
}
|
||||
|
||||
@ -980,11 +975,11 @@ class PollingWorker private constructor(c : Context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AccountThread(val account : SavedAccount) : Thread(), TootApiClient.CurrentCallCallback {
|
||||
internal inner class AccountThread(val account : SavedAccount) : Thread(), CurrentCallCallback {
|
||||
|
||||
private var current_call : Call? = null
|
||||
|
||||
val client = TootApiClient(context, object : TootApiCallback {
|
||||
val client = TootApiClient(context, callback=object : TootApiCallback {
|
||||
override val isApiCancelled : Boolean
|
||||
get() = job.isJobCancelled
|
||||
})
|
||||
@ -996,7 +991,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
private var nid_last_show = - 1L
|
||||
|
||||
init {
|
||||
client.setCurrentCallCallback(this)
|
||||
client.currentCallCallback = this
|
||||
}
|
||||
|
||||
override fun onCallCreated(call : Call) {
|
||||
@ -1228,8 +1223,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
if(now - nr.last_load >= 60000L * 2) {
|
||||
nr.last_load = now
|
||||
|
||||
client.setAccount(account)
|
||||
|
||||
client.account = account
|
||||
|
||||
for(nTry in 0 .. 3) {
|
||||
if(job.isJobCancelled) return
|
||||
|
@ -42,10 +42,6 @@ internal class StreamReader(
|
||||
|
||||
private val reader_list = LinkedList<Reader>()
|
||||
|
||||
internal interface Callback {
|
||||
fun onStreamingMessage(event_type : String, item : Any?)
|
||||
}
|
||||
|
||||
private inner class Reader(
|
||||
internal val access_info : SavedAccount,
|
||||
internal val end_point : String,
|
||||
@ -55,7 +51,7 @@ internal class StreamReader(
|
||||
internal val bDisposed = AtomicBoolean()
|
||||
internal val bListening = AtomicBoolean()
|
||||
internal val socket = AtomicReference<WebSocket>(null)
|
||||
internal val callback_list = LinkedList<Callback>()
|
||||
internal val callback_list = LinkedList< (event_type : String, item : Any?)->Unit >()
|
||||
internal val parser : TootParser
|
||||
|
||||
init {
|
||||
@ -76,14 +72,14 @@ internal class StreamReader(
|
||||
this.parser.setHighlightTrie(highlight_trie)
|
||||
}
|
||||
|
||||
@Synchronized internal fun addCallback(stream_callback : Callback) {
|
||||
@Synchronized internal fun addCallback(stream_callback : (event_type : String, item : Any?)->Unit ) {
|
||||
for(c in callback_list) {
|
||||
if(c === stream_callback) return
|
||||
}
|
||||
callback_list.add(stream_callback)
|
||||
}
|
||||
|
||||
@Synchronized internal fun removeCallback(stream_callback : Callback) {
|
||||
@Synchronized internal fun removeCallback(stream_callback : (event_type : String, item : Any?)->Unit) {
|
||||
val it = callback_list.iterator()
|
||||
while(it.hasNext()) {
|
||||
val c = it.next()
|
||||
@ -120,15 +116,14 @@ internal class StreamReader(
|
||||
}
|
||||
|
||||
Utils.runOnMainThread {
|
||||
if(bDisposed.get()) return@runOnMainThread
|
||||
synchronized(this) {
|
||||
if(bDisposed.get()) return@runOnMainThread
|
||||
for(callback in callback_list) {
|
||||
try {
|
||||
callback.onStreamingMessage(event, payload)
|
||||
callback(event, payload)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -239,7 +234,7 @@ internal class StreamReader(
|
||||
accessInfo : SavedAccount,
|
||||
endPoint : String,
|
||||
highlightTrie : WordTrieTree?,
|
||||
streamCallback : Callback
|
||||
streamCallback : (event_type : String, item : Any?)->Unit
|
||||
) {
|
||||
|
||||
val reader = prepareReader(accessInfo, endPoint, highlightTrie)
|
||||
@ -255,7 +250,7 @@ internal class StreamReader(
|
||||
fun unregister(
|
||||
accessInfo : SavedAccount,
|
||||
endPoint : String,
|
||||
streamCallback : Callback
|
||||
streamCallback : (event_type : String, item : Any?)->Unit
|
||||
) {
|
||||
synchronized(reader_list) {
|
||||
val it = reader_list.iterator()
|
||||
|
@ -24,18 +24,27 @@ class DuplicateMap {
|
||||
set_status_uri.clear()
|
||||
}
|
||||
|
||||
private fun isDuplicate(o : Any) : Boolean {
|
||||
fun isDuplicate(o : Any) : Boolean {
|
||||
|
||||
when(o) {
|
||||
|
||||
is TootStatus ->{
|
||||
val uri = o.uri
|
||||
if( uri != null && uri.isNotEmpty() ){
|
||||
if(set_status_uri.contains(o.uri)) return true
|
||||
set_status_uri.add(o.uri)
|
||||
}else{
|
||||
if(set_status_id.contains(o.id)) return true
|
||||
set_status_id.add(o.id)
|
||||
val url = o.url
|
||||
when {
|
||||
uri?.isNotEmpty() == true -> {
|
||||
if(set_status_uri.contains(uri)) return true
|
||||
set_status_uri.add(uri)
|
||||
}
|
||||
url?.isNotEmpty() == true -> {
|
||||
// URIとURLで同じマップを使いまわすが、害はないと思う…
|
||||
if(set_status_uri.contains(url)) return true
|
||||
set_status_uri.add(url)
|
||||
}
|
||||
else -> {
|
||||
if(set_status_id.contains(o.id)) return true
|
||||
set_status_id.add(o.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,174 +0,0 @@
|
||||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
import jp.juggler.subwaytooter.util.Utils
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
object MSPClient {
|
||||
private val log = LogCategory("MSPClient")
|
||||
|
||||
private val url_token = "http://mastodonsearch.jp/api/v1.0.1/utoken"
|
||||
private val url_search = "http://mastodonsearch.jp/api/v1.0.1/cross"
|
||||
private val api_key = "e53de7f66130208f62d1808672bf6320523dcd0873dc69bc"
|
||||
|
||||
private val ok_http_client = App1.ok_http_client
|
||||
|
||||
fun search(
|
||||
context : Context,
|
||||
query : String,
|
||||
max_id : String,
|
||||
callback : TootApiCallback
|
||||
) : TootApiResult? {
|
||||
|
||||
// ユーザトークンを読む
|
||||
val pref = Pref.pref(context)
|
||||
var user_token = pref.getString(Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN, null)
|
||||
|
||||
var response : Response
|
||||
|
||||
for(nTry in 0 .. 9) {
|
||||
// ユーザトークンがなければ取得する
|
||||
if(user_token == null || user_token.isEmpty() ) {
|
||||
|
||||
callback.publishApiProgress("get MSP user token...")
|
||||
|
||||
val url = url_token + "?apikey=" + Uri.encode(api_key)
|
||||
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
val call = ok_http_client.newCall(request)
|
||||
response = call.execute()
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
|
||||
if(callback.isApiCancelled) return null
|
||||
val bodyString = response.body()?.string()
|
||||
if(callback.isApiCancelled) return null
|
||||
|
||||
if(! response.isSuccessful) {
|
||||
val result = TootApiResult( 0,response = response)
|
||||
val code = response.code()
|
||||
|
||||
if(response.code() < 400 || bodyString == null ) {
|
||||
result.error = Utils.formatResponse(response, "マストドン検索ポータル", bodyString ?: "(no information)")
|
||||
return result
|
||||
}
|
||||
|
||||
result.bodyString = bodyString
|
||||
|
||||
try {
|
||||
val obj = JSONObject(bodyString)
|
||||
val type = Utils.optStringX(obj,"type")
|
||||
val error = Utils.optStringX(obj,"error")
|
||||
if( error != null && error.isNotEmpty() ){
|
||||
result.error = "API returns error. $code, $type, $error"
|
||||
return result
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
result.error = Utils.formatResponse(response,"マストドン検索ポータル",bodyString)
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
user_token = JSONObject(bodyString).getJSONObject("result").getString("token")
|
||||
if( user_token == null || user_token.isEmpty() ) {
|
||||
return TootApiResult("Can't get MSP user token. response=$bodyString")
|
||||
}
|
||||
pref.edit().putString(Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN, user_token).apply()
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(Utils.formatError(ex, "API data error"))
|
||||
}
|
||||
}
|
||||
|
||||
// ユーザトークンを使って検索APIを呼び出す
|
||||
callback.publishApiProgress("waiting search result...")
|
||||
val url = (url_search
|
||||
+ "?apikey=" + Uri.encode(api_key)
|
||||
+ "&utoken=" + Uri.encode(user_token)
|
||||
+ "&q=" + Uri.encode(query)
|
||||
+ "&max=" + Uri.encode(max_id))
|
||||
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
val call = ok_http_client.newCall(request)
|
||||
response = call.execute()
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
|
||||
if(callback.isApiCancelled) return null
|
||||
val bodyString = response.body()?.string()
|
||||
if(callback.isApiCancelled) return null
|
||||
|
||||
if(! response.isSuccessful) {
|
||||
val result = TootApiResult( 0,response = response)
|
||||
val code = response.code()
|
||||
if(response.code() < 400 || bodyString == null ) {
|
||||
result.error = Utils.formatResponse(response, "マストドン検索ポータル", bodyString ?: "(no information)")
|
||||
return result
|
||||
}
|
||||
try {
|
||||
val error = JSONObject(bodyString).getJSONObject("error")
|
||||
val detail = error.optString("detail")
|
||||
val type = error.optString("type")
|
||||
// ユーザトークンがダメなら生成しなおす
|
||||
if("utoken" == detail ) {
|
||||
user_token = null
|
||||
continue
|
||||
}
|
||||
result.error = "API returns error. $code, $type, $detail"
|
||||
return result
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
result.error = Utils.formatResponse(response,"マストドン検索ポータル",bodyString)
|
||||
return result
|
||||
}
|
||||
|
||||
try{
|
||||
if( bodyString != null ) {
|
||||
val array = JSONArray(bodyString)
|
||||
return TootApiResult(response = response, bodyString = bodyString, data = array)
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
return TootApiResult( response, Utils.formatResponse(response,"マストドン検索ポータル",bodyString))
|
||||
}
|
||||
return TootApiResult("MSP user token retry exceeded.")
|
||||
}
|
||||
|
||||
fun getMaxId(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!= null && sv.isNotEmpty() ) return sv
|
||||
}
|
||||
}
|
||||
return max_id
|
||||
}
|
||||
|
||||
}
|
@ -1,64 +1,182 @@
|
||||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.table.ClientInfo
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.util.Utils
|
||||
import okhttp3.Call
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocketListener
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import okhttp3.*
|
||||
import org.json.JSONArray
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class TootApiClient(
|
||||
private val context : Context,
|
||||
private val callback : TootApiCallback
|
||||
internal val context : Context,
|
||||
internal val httpClient : SimpleHttpClient = SimpleHttpClientImpl(App1.ok_http_client),
|
||||
internal val callback : TootApiCallback
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("TootApiClient")
|
||||
|
||||
private val ok_http_client = App1.ok_http_client
|
||||
|
||||
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"
|
||||
private const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential"
|
||||
|
||||
private const val KEY_AUTH_VERSION = "SubwayTooterAuthVersion"
|
||||
private const val AUTH_VERSION = 1
|
||||
private const val REDIRECT_URL = "subwaytooter://oauth/"
|
||||
|
||||
}
|
||||
|
||||
interface CurrentCallCallback {
|
||||
fun onCallCreated(call : Call)
|
||||
}
|
||||
|
||||
private var call_callback : CurrentCallCallback? = null
|
||||
// 認証に関する設定を保存する
|
||||
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)
|
||||
}
|
||||
|
||||
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"
|
||||
internal const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential"
|
||||
|
||||
private const val KEY_AUTH_VERSION = "SubwayTooterAuthVersion"
|
||||
private const val AUTH_VERSION = 1
|
||||
private const val REDIRECT_URL = "subwaytooter://oauth/"
|
||||
|
||||
private const val NO_INFORMATION = "(no information)"
|
||||
|
||||
private val reStartJsonArray = Pattern.compile("\\A\\s*\\[")
|
||||
private val reStartJsonObject = Pattern.compile("\\A\\s*\\{")
|
||||
|
||||
private val mspTokenUrl = "http://mastodonsearch.jp/api/v1.0.1/utoken"
|
||||
private val mspSearchUrl = "http://mastodonsearch.jp/api/v1.0.1/cross"
|
||||
private val mspApiKey = "e53de7f66130208f62d1808672bf6320523dcd0873dc69bc"
|
||||
|
||||
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 {
|
||||
val old_from = Utils.parse_int(old, 0)
|
||||
val hits2 = getTootsearchHits(root)
|
||||
if(hits2 != null) {
|
||||
val size = hits2.length()
|
||||
return if(size == 0) "" else Integer.toString(old_from + hits2.length())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
val DEFAULT_JSON_ERROR_PARSER = { json : JSONObject ->
|
||||
Utils.optStringX(json, "error")
|
||||
}
|
||||
|
||||
internal fun simplifyErrorHtml(
|
||||
response : Response,
|
||||
sv : String,
|
||||
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
||||
) : String {
|
||||
|
||||
// JSONObjectとして解釈できるならエラーメッセージを検出する
|
||||
try {
|
||||
val data = JSONObject(sv)
|
||||
val error_message = jsonErrorParser(data)
|
||||
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") {
|
||||
return DecodeOptions().decodeHTML(null, null, sv).toString()
|
||||
}
|
||||
|
||||
// XXX: Amazon S3 が403を返した場合にcontent-typeが?/xmlでserverがAmazonならXMLをパースしてエラーを整形することもできるが、多分必要ない
|
||||
|
||||
return sv
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val isApiCancelled : Boolean
|
||||
get() = callback.isApiCancelled
|
||||
|
||||
val isCancelled : Boolean
|
||||
internal val isApiCancelled : Boolean
|
||||
get() = callback.isApiCancelled
|
||||
|
||||
fun publishApiProgress(s : String) {
|
||||
@ -69,179 +187,266 @@ class TootApiClient(
|
||||
callback.publishApiProgressRatio(value, max)
|
||||
}
|
||||
|
||||
fun setCurrentCallCallback(call_callback : CurrentCallCallback) {
|
||||
this.call_callback = call_callback
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// ユーティリティ
|
||||
|
||||
// アカウント追加時に使用する
|
||||
fun setInstance(instance : String?) : TootApiClient {
|
||||
this.instance = instance
|
||||
return this
|
||||
}
|
||||
|
||||
fun setAccount(account : SavedAccount) : TootApiClient {
|
||||
this.account = account
|
||||
this.instance = account.host
|
||||
return this
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun request(path : String, request_builder : Request.Builder = Request.Builder()) : TootApiResult? {
|
||||
log.d("request: $path")
|
||||
val result = request_sub(path, request_builder)
|
||||
val error = result?.error
|
||||
if(error != null) log.d("error: $error")
|
||||
return result
|
||||
}
|
||||
|
||||
private fun request_sub(path : String, request_builder : Request.Builder) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
val account = this.account ?: return result.setError("account is null")
|
||||
val access_token = account.getAccessToken()
|
||||
|
||||
val response = try {
|
||||
request_builder.url("https://" + instance + path)
|
||||
// リクエストをokHttpに渡してレスポンスを取得する
|
||||
internal inline fun sendRequest(
|
||||
result : TootApiResult,
|
||||
progressPath : String? = null,
|
||||
block : () -> Request
|
||||
) : Boolean {
|
||||
return try {
|
||||
result.response = null
|
||||
result.bodyString = null
|
||||
result.data = null
|
||||
|
||||
if(access_token != null && access_token.isNotEmpty()) {
|
||||
request_builder.header("Authorization", "Bearer " + access_token)
|
||||
}
|
||||
val request = block()
|
||||
|
||||
callback.publishApiProgress(
|
||||
context.getString(
|
||||
R.string.request_api
|
||||
, request.method()
|
||||
, progressPath ?: request.url().encodedPath()
|
||||
)
|
||||
)
|
||||
|
||||
result.response = httpClient.getResponse(request)
|
||||
|
||||
null == result.error
|
||||
|
||||
sendRequest(request_builder.build())
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
result.setError(result.caption + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
false
|
||||
}
|
||||
return readJson(result, response)
|
||||
}
|
||||
|
||||
fun webSocket(path : String, request_builder : Request.Builder, ws_listener : WebSocketListener) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
// レスポンスがエラーかボディがカラならエラー状態を設定する
|
||||
// 例外を出すかも
|
||||
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) {
|
||||
publishApiProgress(context.getString(R.string.reading_api, request.method(), progressPath ?: result.caption))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseString(
|
||||
result : TootApiResult,
|
||||
progressPath : String? = null,
|
||||
jsonErrorParser : (json : JSONObject) -> String? = DEFAULT_JSON_ERROR_PARSER
|
||||
) : TootApiResult? {
|
||||
|
||||
val response = result.response !! // nullにならないはず
|
||||
|
||||
try {
|
||||
val account = this.account ?: return TootApiResult("account is null")
|
||||
val access_token = account.getAccessToken()
|
||||
val bodyString = readBodyString(result, progressPath, jsonErrorParser)
|
||||
?: return if(isApiCancelled) null else result
|
||||
|
||||
var url = "wss://" + instance + path
|
||||
result.data = bodyString
|
||||
|
||||
if(access_token != null && access_token.isNotEmpty()) {
|
||||
val delm = if(- 1 != url.indexOf('?')) '&' else '?'
|
||||
url = url + delm + "access_token=" + Uri.encode(access_token)
|
||||
}
|
||||
|
||||
request_builder.url(url)
|
||||
val request = request_builder.build()
|
||||
callback.publishApiProgress(context.getString(R.string.request_api, request.method(), path))
|
||||
val ws = ok_http_client.newWebSocket(request, ws_listener)
|
||||
if(callback.isApiCancelled) {
|
||||
ws.cancel()
|
||||
return null
|
||||
}
|
||||
result.data = ws
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
result.error = instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error)
|
||||
result.error = formatResponse(response, result.caption, result.bodyString ?: NO_INFORMATION)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// レスポンスから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にならないはず
|
||||
|
||||
try {
|
||||
val bodyString = readBodyString(result, progressPath, jsonErrorParser)
|
||||
?: return if(isApiCancelled) null else result
|
||||
|
||||
if(reStartJsonArray.matcher(bodyString).find()) {
|
||||
result.data = JSONArray(bodyString)
|
||||
|
||||
} else if(reStartJsonObject.matcher(bodyString).find()) {
|
||||
val json = JSONObject(bodyString)
|
||||
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
|
||||
}
|
||||
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
result.error = formatResponse(response, result.caption, result.bodyString ?: NO_INFORMATION)
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
fun request(path : String, request_builder : Request.Builder = Request.Builder()) : TootApiResult? {
|
||||
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) {
|
||||
|
||||
log.d("request: $path")
|
||||
|
||||
request_builder.url("https://" + instance + path)
|
||||
|
||||
val access_token = account.getAccessToken()
|
||||
if(access_token?.isNotEmpty() == true) {
|
||||
request_builder.header("Authorization", "Bearer " + access_token)
|
||||
}
|
||||
|
||||
request_builder.build()
|
||||
|
||||
}) return result
|
||||
|
||||
return parseJson(result)
|
||||
} finally {
|
||||
val error = result.error
|
||||
if(error != null) log.d("error: $error")
|
||||
}
|
||||
}
|
||||
|
||||
// 疑似アカウントの追加時に、インスタンスの検証を行う
|
||||
fun checkInstance() : TootApiResult? {
|
||||
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
|
||||
val response = try {
|
||||
val request = Request.Builder().url("https://$instance/api/v1/instance").build()
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
return readJson(result, response)
|
||||
if(! sendRequest(result) {
|
||||
Request.Builder().url("https://$instance/api/v1/instance").build()
|
||||
}) return result
|
||||
return parseJson(result)
|
||||
}
|
||||
|
||||
// クライアントをタンスに登録
|
||||
internal fun registerClient(clientName : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
|
||||
// OAuth2 クライアント登録
|
||||
if(! sendRequest(result) {
|
||||
Request.Builder()
|
||||
.url("https://$instance/api/v1/apps")
|
||||
.post(RequestBody.create(MEDIA_TYPE_FORM_URL_ENCODED, "client_name=" + Uri.encode(clientName)
|
||||
+ "&redirect_uris=" + Uri.encode(REDIRECT_URL)
|
||||
+ "&scopes=read write follow"
|
||||
))
|
||||
.build()
|
||||
}) return result
|
||||
|
||||
return parseJson(result)
|
||||
}
|
||||
|
||||
// クライアントアプリの登録を確認するためのトークンを生成する
|
||||
// oAuth2 Client Credentials の取得
|
||||
// https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow
|
||||
// このトークンはAPIを呼び出すたびに新しく生成される…
|
||||
private fun getClientCredential(client_info : JSONObject) : TootApiResult? {
|
||||
internal fun getClientCredential(client_info : JSONObject) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption
|
||||
|
||||
val response = try {
|
||||
|
||||
val request = Request.Builder()
|
||||
if(! sendRequest(result) {
|
||||
Request.Builder()
|
||||
.url("https://$instance/oauth/token")
|
||||
.post(RequestBody.create(MEDIA_TYPE_FORM_URL_ENCODED, "grant_type=client_credentials"
|
||||
+ "&client_id=" + Uri.encode(client_info.optString("client_id"))
|
||||
+ "&client_secret=" + Uri.encode(client_info.optString("client_secret"))
|
||||
))
|
||||
.build()
|
||||
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError("getClientCredential: " + instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
}) return result
|
||||
|
||||
val r2 = readJson(result, response)
|
||||
val r2 = parseJson(result)
|
||||
val jsonObject = r2?.jsonObject ?: return r2
|
||||
|
||||
val sv = Utils.optStringX(jsonObject, "access_token")
|
||||
if(sv?.isNotEmpty() == true) {
|
||||
result.data = sv
|
||||
} else {
|
||||
result.data = null
|
||||
result.error = "getClientCredential: API returns empty client_credential."
|
||||
result.error = "missing client credential."
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// client_credentialがまだ有効か調べる
|
||||
private fun verifyClientCredential(client_credential : String) : TootApiResult? {
|
||||
internal fun verifyClientCredential(client_credential : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
if(result.error != null) return result
|
||||
val instance = result.caption // same to instance
|
||||
|
||||
val response = try {
|
||||
val request = Request.Builder()
|
||||
if(! sendRequest(result) {
|
||||
Request.Builder()
|
||||
.url("https://$instance/api/v1/apps/verify_credentials")
|
||||
.header("Authorization", "Bearer $client_credential")
|
||||
.build()
|
||||
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError("$instance: " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
}) return result
|
||||
|
||||
return readJson(result, response)
|
||||
return parseJson(result)
|
||||
}
|
||||
|
||||
private fun prepareBrowserUrl(client_info : JSONObject) : String {
|
||||
internal fun prepareBrowserUrl(client_info : JSONObject) : String {
|
||||
val account = this.account
|
||||
|
||||
// 認証ページURLを作る
|
||||
val browser_url = ("https://" + instance + "/oauth/authorize"
|
||||
|
||||
return ("https://" + instance + "/oauth/authorize"
|
||||
+ "?client_id=" + Uri.encode(Utils.optStringX(client_info, "client_id"))
|
||||
+ "&response_type=code"
|
||||
+ "&redirect_uri=" + Uri.encode(REDIRECT_URL)
|
||||
+ "&scope=read write follow"
|
||||
+ "&scopes=read write follow"
|
||||
+ "&scope=read+write+follow"
|
||||
+ "&scopes=read+write+follow"
|
||||
+ "&state=" + (if(account != null) "db:" + account.db_id else "host:" + instance)
|
||||
+ "&grant_type=authorization_code"
|
||||
+ "&approval_prompt=force"
|
||||
// +"&access_type=offline"
|
||||
)
|
||||
|
||||
return browser_url
|
||||
}
|
||||
|
||||
|
||||
|
||||
// クライアントを登録してブラウザで開くURLを生成する
|
||||
fun authorize1(clientNameArg : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(this.instance)
|
||||
@ -256,7 +461,7 @@ class TootApiClient(
|
||||
var client_credential = Utils.optStringX(client_info, KEY_CLIENT_CREDENTIAL)
|
||||
|
||||
// client_credential をまだ取得していないなら取得する
|
||||
if(client_credential == null || client_credential.isEmpty()) {
|
||||
if(client_credential?.isEmpty() != false) {
|
||||
val resultSub = getClientCredential(client_info)
|
||||
client_credential = resultSub?.string
|
||||
if(client_credential?.isNotEmpty() == true) {
|
||||
@ -278,29 +483,14 @@ class TootApiClient(
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth2 クライアント登録
|
||||
val response = try {
|
||||
val request = Request.Builder()
|
||||
.url("https://$instance/api/v1/apps")
|
||||
.post(RequestBody.create(MEDIA_TYPE_FORM_URL_ENCODED, "client_name=" + Uri.encode(client_name)
|
||||
+ "&redirect_uris=" + Uri.encode(REDIRECT_URL)
|
||||
+ "&scopes=read write follow"
|
||||
))
|
||||
.build()
|
||||
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
|
||||
val r2 = readJson(result, response)
|
||||
val r2 = registerClient(client_name)
|
||||
val jsonObject = r2?.jsonObject ?: return r2
|
||||
|
||||
// {"id":999,"redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"******","client_secret":"******"}
|
||||
jsonObject.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||
ClientInfo.save(instance, client_name, jsonObject.toString())
|
||||
result.data = prepareBrowserUrl(jsonObject)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -313,57 +503,44 @@ class TootApiClient(
|
||||
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
|
||||
val client_info = ClientInfo.load(instance, client_name) ?: return result.setError("missing client id")
|
||||
|
||||
var response = try {
|
||||
if(! sendRequest(result) {
|
||||
|
||||
val post_content = ("grant_type=authorization_code"
|
||||
+ "&code=" + Uri.encode(code)
|
||||
+ "&client_id=" + Uri.encode(Utils.optStringX(client_info, "client_id"))
|
||||
+ "&redirect_uri=" + Uri.encode(REDIRECT_URL)
|
||||
+ "&client_secret=" + Uri.encode(Utils.optStringX(client_info, "client_secret"))
|
||||
+ "&scope=read write follow"
|
||||
+ "&scopes=read write follow")
|
||||
+ "&scope=read+write+follow"
|
||||
+ "&scopes=read+write+follow")
|
||||
|
||||
val request = Request.Builder()
|
||||
Request.Builder()
|
||||
.url("https://$instance/oauth/token")
|
||||
.post(RequestBody.create(MEDIA_TYPE_FORM_URL_ENCODED, post_content))
|
||||
.build()
|
||||
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
}) return result
|
||||
|
||||
val token_info : JSONObject
|
||||
|
||||
val r2 = readJson(result, response)
|
||||
val jsonObject = r2?.jsonObject ?: return r2
|
||||
val r2 = parseJson(result)
|
||||
val token_info = r2?.jsonObject ?: return r2
|
||||
|
||||
// {"access_token":"******","token_type":"bearer","scope":"read","created_at":1492334641}
|
||||
jsonObject.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||
token_info = jsonObject
|
||||
result.token_info = jsonObject
|
||||
token_info.put(KEY_AUTH_VERSION, AUTH_VERSION)
|
||||
result.token_info = token_info
|
||||
|
||||
val access_token = Utils.optStringX(token_info, "access_token")
|
||||
if(access_token == null || access_token.isEmpty()) {
|
||||
return result.setError("missing access_token in the response.")
|
||||
}
|
||||
|
||||
response = try {
|
||||
|
||||
// 認証されたアカウントのユーザ名を取得する
|
||||
val request = Request.Builder()
|
||||
// 認証されたアカウントのユーザ名を取得する
|
||||
if(! sendRequest(result) {
|
||||
Request.Builder()
|
||||
.url("https://$instance/api/v1/accounts/verify_credentials")
|
||||
.header("Authorization", "Bearer $access_token")
|
||||
.build()
|
||||
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
}) return result
|
||||
|
||||
return readJson(result, response)
|
||||
return parseJson(result)
|
||||
}
|
||||
|
||||
// アクセストークン手動入力でアカウントを更新する場合
|
||||
@ -372,140 +549,172 @@ class TootApiClient(
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
|
||||
val token_info = JSONObject()
|
||||
val response = try {
|
||||
|
||||
// 指定されたアクセストークンを使って token_info を捏造する
|
||||
token_info.put("access_token", access_token)
|
||||
|
||||
// 認証されたアカウントのユーザ名を取得する
|
||||
val request = Request.Builder()
|
||||
// 認証されたアカウントのユーザ名を取得する
|
||||
if(! sendRequest(result) {
|
||||
Request.Builder()
|
||||
.url("https://$instance/api/v1/accounts/verify_credentials")
|
||||
.header("Authorization", "Bearer $access_token")
|
||||
.build()
|
||||
|
||||
sendRequest(request)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(instance + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
}) return result
|
||||
|
||||
val r2 = readJson(result, response)
|
||||
val r2 = parseJson(result)
|
||||
r2?.jsonObject ?: return r2
|
||||
|
||||
// credentialを読めたならtoken_infoを保存したい
|
||||
val token_info = JSONObject()
|
||||
token_info.put("access_token", access_token)
|
||||
result.token_info = token_info
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun searchMsp(query : String, max_id : String) : TootApiResult? {
|
||||
|
||||
// ユーザトークンを読む
|
||||
var user_token = pref.getString(Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN, null)
|
||||
|
||||
for(nTry in 0 until 3) {
|
||||
if(callback.isApiCancelled) return null
|
||||
|
||||
// ユーザトークンがなければ取得する
|
||||
if(user_token == null || user_token.isEmpty()) {
|
||||
|
||||
callback.publishApiProgress("get MSP user token...")
|
||||
|
||||
val result : TootApiResult = TootApiResult.makeWithCaption("Mastodon Search Portal")
|
||||
if(result.error != null) return result
|
||||
|
||||
if(! sendRequest(result) {
|
||||
|
||||
Request.Builder()
|
||||
.url(mspTokenUrl + "?apikey=" + Uri.encode(mspApiKey))
|
||||
.build()
|
||||
|
||||
}) return result
|
||||
|
||||
val r2 = parseJson(result) { json ->
|
||||
val error = Utils.optStringX(json, "error")
|
||||
if(error == null) {
|
||||
null
|
||||
} else {
|
||||
val type = Utils.optStringX(json, "type")
|
||||
"API returns error: $type $error"
|
||||
}
|
||||
}
|
||||
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}")
|
||||
}
|
||||
|
||||
pref.edit().putString(Pref.KEY_MASTODON_SEARCH_PORTAL_USER_TOKEN, user_token).apply()
|
||||
}
|
||||
|
||||
// ユーザトークンを使って検索APIを呼び出す
|
||||
val result : TootApiResult = TootApiResult.makeWithCaption("Mastodon Search Portal")
|
||||
if(result.error != null) return result
|
||||
|
||||
if(! sendRequest(result) {
|
||||
|
||||
val url = (mspSearchUrl
|
||||
+ "?apikey=" + Uri.encode(mspApiKey)
|
||||
+ "&utoken=" + Uri.encode(user_token)
|
||||
+ "&q=" + Uri.encode(query)
|
||||
+ "&max=" + Uri.encode(max_id))
|
||||
|
||||
Request.Builder().url(url).build()
|
||||
|
||||
}) return result
|
||||
|
||||
var isUserTokenError = false
|
||||
val r2 = parseJson(result) { json ->
|
||||
val error = Utils.optStringX(json, "error")
|
||||
if(error == null) {
|
||||
null
|
||||
} else {
|
||||
// ユーザトークンがダメなら生成しなおす
|
||||
val detail = json.optString("detail")
|
||||
if("utoken" == detail) {
|
||||
isUserTokenError = true
|
||||
}
|
||||
|
||||
val type = Utils.optStringX(json, "type")
|
||||
"API returns error: $type $error"
|
||||
}
|
||||
}
|
||||
if(r2 == null || ! isUserTokenError) return r2
|
||||
}
|
||||
return TootApiResult("MSP user token retry exceeded.")
|
||||
}
|
||||
|
||||
fun searchTootsearch(
|
||||
query : String,
|
||||
max_id : String // 空文字列、もしくはfromに指定するパラメータ
|
||||
) : TootApiResult? {
|
||||
|
||||
val result = TootApiResult.makeWithCaption("Tootsearch")
|
||||
if(result.error != null) return result
|
||||
|
||||
if(! sendRequest(result) {
|
||||
val url = ("https://tootsearch.chotto.moe/api/v1/search"
|
||||
+ "?sort=" + Uri.encode("created_at:desc")
|
||||
+ "&from=" + max_id
|
||||
+ "&q=" + Uri.encode(query))
|
||||
|
||||
Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
}) return result
|
||||
|
||||
return parseJson(result)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// JSONデータ以外を扱うリクエスト
|
||||
|
||||
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
|
||||
fun getHttp(url : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(url)
|
||||
if(result.error != null) return result
|
||||
|
||||
val response = try {
|
||||
sendRequest(Request.Builder().url(url).build(), url)
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return result.setError(url + ": " + Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
try {
|
||||
if(callback.isApiCancelled) return null
|
||||
val request = response.request()
|
||||
if( request != null ){
|
||||
callback.publishApiProgress(context.getString(R.string.reading_api, request.method(), url))
|
||||
}
|
||||
result.readBodyString(response)
|
||||
|
||||
if(callback.isApiCancelled) return null
|
||||
callback.publishApiProgress(context.getString(R.string.parsing_response))
|
||||
if(result.isErrorOrEmptyBody()) return result
|
||||
|
||||
result.data = result.bodyString
|
||||
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
result.error = Utils.formatResponse(response, result.caption, result.bodyString ?: "no information")
|
||||
}
|
||||
return result
|
||||
if(! sendRequest(result, progressPath = url) {
|
||||
Request.Builder().url(url).build()
|
||||
}) return result
|
||||
return parseString(result)
|
||||
|
||||
}
|
||||
|
||||
private fun sendRequest(request : Request, showPath : String? = null) : Response {
|
||||
callback.publishApiProgress(context.getString(R.string.request_api, request.method(), showPath ?: request.url().encodedPath()))
|
||||
val call = ok_http_client.newCall(request)
|
||||
call_callback?.onCallCreated(call)
|
||||
return call.execute()
|
||||
}
|
||||
|
||||
private fun readJson(result : TootApiResult, response : Response) : TootApiResult? {
|
||||
fun webSocket(path : String, request_builder : Request.Builder, ws_listener : WebSocketListener) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(instance)
|
||||
if(result.error != null) return result
|
||||
val account = this.account ?: return TootApiResult("account is null")
|
||||
|
||||
try {
|
||||
if(callback.isApiCancelled) return null
|
||||
val request = response.request()
|
||||
if( request != null ){
|
||||
callback.publishApiProgress(context.getString(R.string.reading_api, request.method(), request.url().encodedPath()))
|
||||
}
|
||||
result.readBodyString(response)
|
||||
|
||||
if(callback.isApiCancelled) return null
|
||||
callback.publishApiProgress(context.getString(R.string.parsing_response))
|
||||
if(result.isErrorOrEmptyBody()) return result
|
||||
var url = "wss://$instance$path"
|
||||
|
||||
val bodyString = result.bodyString
|
||||
if(bodyString?.startsWith("[") == true) {
|
||||
result.data = JSONArray(bodyString)
|
||||
} else if(bodyString?.startsWith("{") == true) {
|
||||
val json = JSONObject(bodyString)
|
||||
val error = Utils.optStringX(json, "error")
|
||||
if(error != null) {
|
||||
result.error = "API returns error: $error"
|
||||
} else {
|
||||
result.data = json
|
||||
}
|
||||
} else {
|
||||
result.error = context.getString(R.string.response_not_json) + "\n" + bodyString
|
||||
val access_token = account.getAccessToken()
|
||||
if(access_token?.isNotEmpty() == true) {
|
||||
val delm = if(- 1 != url.indexOf('?')) '&' else '?'
|
||||
url = url + delm + "access_token=" + Uri.encode(access_token)
|
||||
}
|
||||
|
||||
request_builder.url(url)
|
||||
val request = request_builder.build()
|
||||
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
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
result.error = Utils.formatResponse(response, result.caption, result.bodyString ?: "no information")
|
||||
result.error = result.caption + ": " + Utils.formatError(ex, context.resources, R.string.network_error)
|
||||
}
|
||||
return result
|
||||
|
||||
}
|
||||
|
||||
// private fun parseResponse(tokenInfo : JSONObject?, response : Response) : TootApiResult? {
|
||||
// try {
|
||||
// if(callback.isApiCancelled) return null
|
||||
//
|
||||
// if(! response.isSuccessful) {
|
||||
// return TootApiResult(response, Utils.formatResponse(response, instance ?: "(no instance)"))
|
||||
// }
|
||||
//
|
||||
// val bodyString = response.body()?.string() ?: throw RuntimeException("missing response body.")
|
||||
// if(callback.isApiCancelled) return null
|
||||
//
|
||||
// callback.publishApiProgress(context.getString(R.string.parsing_response))
|
||||
// return if(bodyString.startsWith("{")) {
|
||||
//
|
||||
// val obj = JSONObject(bodyString)
|
||||
//
|
||||
// val error = Utils.optStringX(obj, "error")
|
||||
//
|
||||
// if(error != null)
|
||||
// TootApiResult(context.getString(R.string.api_error, error))
|
||||
// else
|
||||
// TootApiResult(response, tokenInfo, bodyString, obj)
|
||||
//
|
||||
// } else if(bodyString.startsWith("[")) {
|
||||
// val array = JSONArray(bodyString)
|
||||
// TootApiResult(response, tokenInfo, bodyString, array)
|
||||
// } else {
|
||||
// TootApiResult(response, Utils.formatResponse(response, instance ?: "(no instance)", bodyString))
|
||||
// }
|
||||
// } catch(ex : Throwable) {
|
||||
// TootApiClient.log.trace(ex)
|
||||
// return TootApiResult(Utils.formatError(ex, "API data error"))
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -6,26 +6,55 @@ import org.json.JSONObject
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
import jp.juggler.subwaytooter.util.Utils
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
|
||||
open class TootApiResult(
|
||||
@Suppress("unused") val dummy :Int =0,
|
||||
@Suppress("unused") val dummy : Int = 0,
|
||||
var error : String? = null,
|
||||
var response : Response? = null,
|
||||
var bodyString : String? = null
|
||||
) {
|
||||
|
||||
var token_info : JSONObject? = null
|
||||
|
||||
|
||||
var data : Any? = null
|
||||
set(value){
|
||||
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 link_older : String? = null // より古いデータへのリンク
|
||||
var link_newer : String? = null // より新しいデータへの
|
||||
var caption : String = "?"
|
||||
|
||||
constructor() : this(0)
|
||||
|
||||
constructor(error : String) : this(0, error = error)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
constructor(socket : WebSocket) : this(0) {
|
||||
this.data = socket
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("TootApiResult")
|
||||
|
||||
@ -35,7 +64,6 @@ open class TootApiResult(
|
||||
private const val MIMUMEDON_ERROR = "mimumedon.comには対応しません"
|
||||
|
||||
private const val NO_INSTANCE = "missing instance name"
|
||||
const val NO_INFORMATION = "(no information)"
|
||||
|
||||
fun makeWithCaption(caption : String?) : TootApiResult {
|
||||
val result = TootApiResult()
|
||||
@ -49,89 +77,39 @@ open class TootApiResult(
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var link_older : String? = null // より古いデータへのリンク
|
||||
var link_newer : String? = null // より新しいデータへの
|
||||
var caption : String = "?"
|
||||
|
||||
constructor():this(0)
|
||||
|
||||
constructor( error : String) : this(0,error=error)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
constructor( socket : WebSocket) : this(0){
|
||||
this.data = socket
|
||||
}
|
||||
|
||||
|
||||
// return result.setError(...) と書きたい
|
||||
fun setError(error:String) :TootApiResult{
|
||||
fun setError(error : String) : TootApiResult {
|
||||
this.error = error
|
||||
return this
|
||||
}
|
||||
|
||||
// レスポンスボディを読む
|
||||
fun readBodyString(response : Response) {
|
||||
this.response = response
|
||||
this.bodyString = response.body()?.string()
|
||||
}
|
||||
|
||||
// レスポンスがエラーかボディがカラならエラー状態を設定する
|
||||
// エラーがあれば真を返す
|
||||
fun isErrorOrEmptyBody() :Boolean{
|
||||
val response = this.response ?: throw NotImplementedError("not calling readBodyString")
|
||||
if(! response.isSuccessful || bodyString == null ) {
|
||||
error = Utils.formatResponse(response, caption, bodyString ?: NO_INFORMATION)
|
||||
}
|
||||
return error != null
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private fun parseLinkHeader(
|
||||
response : Response?,
|
||||
array : JSONArray
|
||||
) {
|
||||
if( response != null){
|
||||
log.d("array size=%s", array.length() )
|
||||
|
||||
val sv = response.header("Link")
|
||||
if(sv == null) {
|
||||
log.d("missing Link header")
|
||||
} else {
|
||||
// Link: <https://mastodon.juggler.jp/api/v1/timelines/home?limit=XX&max_id=405228>; rel="next",
|
||||
// <https://mastodon.juggler.jp/api/v1/timelines/home?limit=XX&since_id=436946>; rel="prev"
|
||||
val m = reLinkURL.matcher(sv)
|
||||
while(m.find()) {
|
||||
val url = m.group(1)
|
||||
val rel = m.group(2)
|
||||
// log.d("Link %s,%s",rel,url);
|
||||
if("next" == rel) link_older = url
|
||||
if("prev" == rel) link_newer = url
|
||||
}
|
||||
private fun parseLinkHeader( response : Response?, array : JSONArray ) {
|
||||
response ?: return
|
||||
|
||||
log.d("array size=%s", array.length())
|
||||
|
||||
val sv = response.header("Link")
|
||||
if(sv == null) {
|
||||
log.d("missing Link header")
|
||||
} else {
|
||||
// Link: <https://mastodon.juggler.jp/api/v1/timelines/home?limit=XX&max_id=405228>; rel="next",
|
||||
// <https://mastodon.juggler.jp/api/v1/timelines/home?limit=XX&since_id=436946>; rel="prev"
|
||||
val m = reLinkURL.matcher(sv)
|
||||
while(m.find()) {
|
||||
val url = m.group(1)
|
||||
val rel = m.group(2)
|
||||
// log.d("Link %s,%s",rel,url);
|
||||
if("next" == rel) link_older = url
|
||||
if("prev" == rel) link_newer = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val jsonObject :JSONObject?
|
||||
get() = data as? JSONObject
|
||||
|
||||
val jsonArray :JSONArray?
|
||||
get() = data as? JSONArray
|
||||
|
||||
val string: String?
|
||||
get() = data as? String
|
||||
|
||||
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ class TootTaskRunner @JvmOverloads constructor(
|
||||
init {
|
||||
this.refContext = WeakReference(context)
|
||||
this.handler = Handler()
|
||||
this.client = TootApiClient(context, this)
|
||||
this.client = TootApiClient(context, callback=this)
|
||||
this.task = MyTask(this)
|
||||
}
|
||||
|
||||
@ -108,12 +108,12 @@ class TootTaskRunner @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun run(access_info : SavedAccount, callback : TootTask) {
|
||||
client.setAccount(access_info)
|
||||
client.account =access_info
|
||||
run(callback)
|
||||
}
|
||||
|
||||
fun run(instance : String, callback : TootTask) {
|
||||
client.setInstance(instance)
|
||||
client.instance = instance
|
||||
run(callback)
|
||||
}
|
||||
|
||||
@ -122,8 +122,6 @@ class TootTaskRunner @JvmOverloads constructor(
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// implements TootApiClient.Callback
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
import jp.juggler.subwaytooter.util.Utils
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
object TootsearchClient {
|
||||
private val log = LogCategory("TootsearchClient")
|
||||
|
||||
private val ok_http_client = App1.ok_http_client
|
||||
|
||||
fun search(
|
||||
context : Context,
|
||||
query : String,
|
||||
max_id : String, // 空文字列、もしくはfromに指定するパラメータ
|
||||
callback : TootApiCallback
|
||||
) : TootApiResult? {
|
||||
val url = ("https://tootsearch.chotto.moe/api/v1/search"
|
||||
+ "?sort=" + Uri.encode("created_at:desc")
|
||||
+ "&from=" + max_id
|
||||
+ "&q=" + Uri.encode(query))
|
||||
|
||||
val response : Response
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
callback.publishApiProgress("waiting search result...")
|
||||
val call = ok_http_client.newCall(request)
|
||||
response = call.execute()
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
return TootApiResult(Utils.formatError(ex, context.resources, R.string.network_error))
|
||||
}
|
||||
|
||||
if(callback.isApiCancelled) return null
|
||||
val bodyString = response.body()?.string()
|
||||
if(! response.isSuccessful || bodyString == null ) {
|
||||
log.d("response failed.")
|
||||
return TootApiResult(Utils.formatResponse(response, "Tootsearch",bodyString ?: "(no information)"))
|
||||
}
|
||||
|
||||
return try {
|
||||
TootApiResult(response,bodyString,JSONObject(bodyString))
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
TootApiResult(Utils.formatError(ex, "API data error"))
|
||||
}
|
||||
}
|
||||
|
||||
fun getHits(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 getMaxId(root : JSONObject, old : String) : String {
|
||||
val old_from = Utils.parse_int(old, 0)
|
||||
val hits2 = getHits(root)
|
||||
if(hits2 != null) {
|
||||
val size = hits2.length()
|
||||
return if(size == 0) "" else Integer.toString(old_from + hits2.length())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
@ -4,15 +4,9 @@ import org.json.JSONObject
|
||||
import jp.juggler.subwaytooter.util.Utils
|
||||
|
||||
class CustomEmoji(
|
||||
// shortcode (コロンを含まない)
|
||||
val shortcode : String,
|
||||
|
||||
// 画像URL
|
||||
val url : String,
|
||||
|
||||
// アニメーションなしの画像URL
|
||||
val static_url : String?
|
||||
|
||||
val shortcode : String, // shortcode (コロンを含まない)
|
||||
val url : String, // 画像URL
|
||||
val static_url : String? // アニメーションなしの画像URL
|
||||
) : Mappable<String> {
|
||||
|
||||
constructor(src : JSONObject) : this(
|
||||
|
@ -2,4 +2,6 @@ package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
interface Mappable<out T> {
|
||||
val mapKey : T
|
||||
}
|
||||
}
|
||||
|
||||
// EntityUtil の parseMap() でマップを構築する際、マップのキーを返すインタフェース
|
||||
|
@ -9,7 +9,7 @@ class TootNotification(
|
||||
val json : JSONObject,
|
||||
val id : Long,
|
||||
val type : String, // One of: "mention", "reblog", "favourite", "follow"
|
||||
private val created_at : String, // The time the notification was created
|
||||
private val created_at : String?, // The time the notification was created
|
||||
val account : TootAccount?, // The Account sending the notification to the user
|
||||
val status : TootStatus? // The Status associated with the notification, if applicable
|
||||
) {
|
||||
@ -24,7 +24,7 @@ class TootNotification(
|
||||
json = src,
|
||||
id = Utils.optLongX(src, "id"),
|
||||
type = src.notEmptyOrThrow("type"),
|
||||
created_at = src.notEmptyOrThrow("created_at"),
|
||||
created_at = Utils.optStringX(src,"created_at"),
|
||||
account = TootAccount.parse(parser.context, parser.accessInfo, src.optJSONObject("account"), ServiceType.MASTODON),
|
||||
status = TootStatus.parse(parser, src.optJSONObject("status"), ServiceType.MASTODON)
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ import android.text.SpannableString
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
@ -13,7 +14,6 @@ import java.lang.ref.WeakReference
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.TootsearchClient
|
||||
import jp.juggler.subwaytooter.table.HighlightWord
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
@ -391,7 +391,7 @@ class TootStatus(parser : TootParser, src : JSONObject, serviceType : ServiceTyp
|
||||
serviceType : ServiceType = ServiceType.TOOTSEARCH
|
||||
) : TootStatus.List {
|
||||
val result = TootStatus.List()
|
||||
val array = TootsearchClient.getHits(root)
|
||||
val array = TootApiClient.getTootsearchHits(root)
|
||||
if(array != null) {
|
||||
val array_size = array.length()
|
||||
result.ensureCapacity(array_size)
|
||||
|
@ -64,4 +64,13 @@ object ClientInfo {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 単体テスト用。インスタンス名を指定して削除する
|
||||
internal fun delete(instance : String) {
|
||||
try {
|
||||
App1.database.delete(table, "$COL_HOST=?", arrayOf(instance))
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "delete failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import okhttp3.*
|
||||
|
||||
// okhttpそのままだとモックしづらいので
|
||||
// リクエストを投げてレスポンスを得る部分をインタフェースにまとめる
|
||||
|
||||
interface CurrentCallCallback {
|
||||
fun onCallCreated(call : Call)
|
||||
}
|
||||
|
||||
interface SimpleHttpClient{
|
||||
var currentCallCallback : CurrentCallCallback?
|
||||
fun getResponse(request: Request) : Response
|
||||
fun getWebSocket(request: Request, webSocketListener : WebSocketListener): WebSocket
|
||||
}
|
||||
|
||||
class SimpleHttpClientImpl(val okHttpClient:OkHttpClient): SimpleHttpClient{
|
||||
override var currentCallCallback : CurrentCallCallback? = null
|
||||
|
||||
override fun getResponse(request : Request) : Response {
|
||||
val call = okHttpClient.newCall(request)
|
||||
currentCallCallback?.onCallCreated(call)
|
||||
return call.execute()
|
||||
}
|
||||
|
||||
override fun getWebSocket(request : Request, webSocketListener : WebSocketListener) : WebSocket {
|
||||
return okHttpClient.newWebSocket(request,webSocketListener)
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import android.util.SparseBooleanArray
|
||||
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.view.GestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
@ -849,68 +850,6 @@ object Utils {
|
||||
return null
|
||||
}
|
||||
|
||||
fun simplifyErrorHtml(response : Response, sv : String) : String {
|
||||
try {
|
||||
val data = JSONObject(sv)
|
||||
val error = data.getString("error")
|
||||
if(error != null && error.isNotBlank()) return error
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "response body is not JSON or missing 'error' attribute.")
|
||||
}
|
||||
|
||||
// JSONではなかった
|
||||
|
||||
// HTMLならタグの除去を試みる
|
||||
val ct = response.header("content-type")
|
||||
if(ct != null && ct.contains("/html")) {
|
||||
return DecodeOptions().decodeHTML(null, null, sv).toString()
|
||||
}
|
||||
|
||||
// XXX: Amazon S3 が403を返した場合にcontent-typeが?/xmlでserverがAmazonならXMLをパースしてエラーを整形することもできるが、多分必要ない
|
||||
|
||||
return sv
|
||||
}
|
||||
|
||||
fun formatResponse(response : Response, caption : String, bodyString : String? = null) : String {
|
||||
val sb = StringBuilder()
|
||||
try {
|
||||
val empty_length = sb.length
|
||||
|
||||
if(bodyString != null) {
|
||||
sb.append(simplifyErrorHtml(response, bodyString))
|
||||
} else {
|
||||
val body = response.body()
|
||||
if(body != null) {
|
||||
try {
|
||||
val sv = body.string()
|
||||
if(sv != null && sv.isNotBlank()) {
|
||||
sb.append(simplifyErrorHtml(response, sv))
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "response body is not String.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(sb.length == empty_length) 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")
|
||||
}
|
||||
|
||||
fun scanView(view : View?, callback : (view : View) -> Unit) {
|
||||
view ?: return
|
||||
|
Loading…
x
Reference in New Issue
Block a user