試験機能:misskeyのタンスを疑似アカウントで追加して、ローカルTLとグローバルTLを見れる

This commit is contained in:
tateisu 2018-08-17 04:58:30 +09:00
parent b58552c990
commit 7ceb9e4f51
14 changed files with 701 additions and 283 deletions

View File

@ -101,7 +101,8 @@ class App1 : Application() {
// 2018/5/16 v252 24=>25 SubscriptionServerKey テーブルを追加
// 2018/5/16 v252 25=>26 SubscriptionServerKey テーブルを丸ごと変更
// 2018/8/5 v264 26 => 27 SavedAccountテーブルに項目追加
internal const val DB_VERSION = 27
// 2018/8/17 v267 27 => 28 SavedAccountテーブルに項目追加
internal const val DB_VERSION = 28
private val tableList = arrayOf(
LogData,

View File

@ -7,19 +7,17 @@ import android.os.AsyncTask
import android.os.SystemClock
import android.view.Gravity
import jp.juggler.subwaytooter.api.*
import org.json.JSONException
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.*
import jp.juggler.subwaytooter.util.*
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
enum class StreamingIndicatorState {
NONE,
@ -309,6 +307,9 @@ class Column(
private val streamPath : String?
get() {
// misskeyの疑似アカウントはストリーミング対応していない
if(access_info.isPseudo && access_info.isMisskey) return null
return when(column_type) {
TYPE_HOME, TYPE_NOTIFICATIONS -> "/api/v1/streaming/?stream=user"
TYPE_LOCAL -> "/api/v1/streaming/?stream=public:local"
@ -320,7 +321,7 @@ class Column(
TYPE_HASHTAG -> when(instance_local) {
true -> "/api/v1/streaming/?stream=" + Uri.encode("hashtag:local") + "&tag=" + hashtag.encodePercent()
else -> "/api/v1/streaming/?stream=hashtag&tag=" + hashtag.encodePercent()
// タグ先頭の#を含まない
// タグ先頭の#を含まない
}
else -> null
}
@ -816,7 +817,7 @@ class Column(
}
}
fun removeUser(targetAccount : SavedAccount,columnType:Int,who_id:Long){
fun removeUser(targetAccount : SavedAccount, columnType : Int, who_id : Long) {
if(column_type == columnType && targetAccount.acct == access_info.acct) {
val tmp_list = ArrayList<TimelineItem>(list_data.size)
for(o in list_data) {
@ -1502,13 +1503,36 @@ class Column(
log.d("getStatusesPinned: list size=%s", list_pinned?.size ?: - 1)
}
fun getStatuses(client : TootApiClient, path_base : String) : TootApiResult? {
fun getStatuses(
client : TootApiClient,
path_base : String,
isMisskey : Boolean = false
) : TootApiResult? {
val params = JSONObject()
if(isMisskey) {
parser.serviceType = ServiceType.MISSKEY
params.put("limit", 100)
if(with_attachment) {
params.put("mediaOnly", true)
}
}
val time_start = SystemClock.elapsedRealtime()
val result = client.request(path_base)
val result = if(isMisskey) {
client.request(path_base, params.toPostRequestBuilder())
} else {
client.request(path_base)
}
var jsonArray = result?.jsonArray
if(jsonArray != null) {
saveRange(result, true, true)
if(isMisskey) {
saveRangeMisskey(jsonArray, true, true)
} else {
saveRange(result, true, true)
}
//
var src = parser.statusList(jsonArray)
@ -1540,9 +1564,16 @@ class Column(
log.d("loading-statuses: timeout.")
break
}
val path = path_base + delimiter + "max_id=" + max_id
val result2 = client.request(path)
val result2 = if(isMisskey) {
params.put("untilId", max_id)
client.request(path_base, params.toPostRequestBuilder())
} else {
val path = path_base + delimiter + "max_id=" + max_id
client.request(path)
}
jsonArray = result2?.jsonArray
if(jsonArray == null) {
log.d("loading-statuses: error or cancelled.")
break
@ -1552,9 +1583,16 @@ class Column(
addWithFilterStatus(list_tmp, src)
if(! saveRangeEnd(result2)) {
log.d("loading-statuses: missing range info.")
break
if(isMisskey) {
if(! saveRangeEndMisskey(jsonArray)) {
log.d("loading-statuses: missing range info.")
break
}
} else {
if(! saveRangeEnd(result2)) {
log.d("loading-statuses: missing range info.")
break
}
}
}
}
@ -1748,9 +1786,22 @@ class Column(
TYPE_DIRECT_MESSAGES -> return getStatuses(client, PATH_DIRECT_MESSAGES)
TYPE_LOCAL -> return getStatuses(client, makePublicLocalUrl())
TYPE_FEDERATE -> return getStatuses(client, makePublicFederateUrl())
TYPE_LOCAL -> return when(access_info.isMisskey) {
true -> getStatuses(
client,
"/api/notes/local-timeline",
isMisskey = true
)
else -> getStatuses(client, makePublicLocalUrl())
}
TYPE_FEDERATE -> return when(access_info.isMisskey) {
true -> getStatuses(
client,
"/api/notes/global-timeline",
isMisskey = true
)
else -> return getStatuses(client, makePublicFederateUrl())
}
TYPE_PROFILE -> {
@ -1903,7 +1954,10 @@ class Column(
//
} else {
this.list_tmp = addOne(this.list_tmp, target_status)
this.list_tmp = addOne(this.list_tmp, TootMessageHolder(context.getString(R.string.toot_context_parse_failed)))
this.list_tmp = addOne(
this.list_tmp,
TootMessageHolder(context.getString(R.string.toot_context_parse_failed))
)
}
// カードを取得する
@ -2132,6 +2186,31 @@ class Column(
}
}
private fun saveRangeMisskey(src : JSONArray?, bBottom : Boolean, bTop : Boolean) {
src ?: return
var id_min : String? = null
var id_max : String? = null
for(i in 0 until src.length()) {
val id = src.optJSONObject(i)?.optString("id", null) ?: continue
if(id_min == null || id < id_min) id_min = id
if(id_max == null || id > id_max) id_max = id
}
if(bBottom) {
when {
id_min == null -> max_id = ""
max_id.isEmpty() || id_min < max_id -> max_id = id_min
}
}
if(bTop) {
when {
id_max == null -> {
}
since_id.isEmpty() || id_max > since_id -> since_id = id_max
}
}
}
private fun saveRangeEnd(result : TootApiResult?) : Boolean {
if(result != null) {
if(result.link_older == null) {
@ -2147,6 +2226,23 @@ class Column(
return false
}
private fun saveRangeEndMisskey(src : JSONArray?) : Boolean {
if(src != null) {
var id_min : String? = null
for(i in 0 until src.length()) {
val id = src.optJSONObject(i)?.optString("id", null) ?: continue
if(id_min == null || id < id_min) id_min = id
}
if(id_min?.isEmpty() != false) {
max_id = ""
} else {
max_id = id_min
return true
}
}
return false
}
private fun addRange(bBottom : Boolean, path : String) : String {
val delimiter = if(- 1 != path.indexOf('?')) '&' else '?'
if(bBottom) {
@ -2610,18 +2706,43 @@ class Column(
fun getStatusList(
client : TootApiClient,
path_base : String
path_base : String,
isMisskey : Boolean = false
) : TootApiResult? {
val params = JSONObject()
if(isMisskey) {
parser.serviceType = ServiceType.MISSKEY
params.put("limit", 100)
if(with_attachment) {
params.put("mediaOnly", true)
}
}
val time_start = SystemClock.elapsedRealtime()
val delimiter = if(- 1 != path_base.indexOf('?')) '&' else '?'
val last_since_id = since_id
val result = client.request(addRange(bBottom, path_base))
val result = if(isMisskey) {
if(bBottom) {
if(max_id.isNotEmpty()) params.put("untilId", max_id)
} else {
if(since_id.isNotEmpty()) params.put("sinceId", since_id)
}
client.request(path_base, params.toPostRequestBuilder())
} else {
client.request(addRange(bBottom, path_base))
}
var jsonArray = result?.jsonArray
if(jsonArray != null) {
saveRange(result, bBottom, ! bBottom)
if(isMisskey) {
saveRangeMisskey(jsonArray, bBottom, ! bBottom)
} else {
saveRange(result, bBottom, ! bBottom)
}
var src = parser.statusList(jsonArray)
list_tmp = addWithFilterStatus(null, src)
@ -2657,8 +2778,14 @@ class Column(
break
}
val path = path_base + delimiter + "max_id=" + max_id
val result2 = client.request(path)
val result2 = if(isMisskey) {
params.put("untilId", max_id)
client.request(path_base, params.toPostRequestBuilder())
} else {
val path = path_base + delimiter + "max_id=" + max_id
client.request(path)
}
jsonArray = result2?.jsonArray
if(jsonArray == null) {
log.d("refresh-status-bottom: error or cancelled.")
@ -2669,9 +2796,16 @@ class Column(
addWithFilterStatus(list_tmp, src)
if(! saveRangeEnd(result2)) {
log.d("refresh-status-bottom: saveRangeEnd failed.")
break
if(isMisskey) {
if(! saveRangeEndMisskey(jsonArray)) {
log.d("refresh-status-bottom: saveRangeEnd failed.")
break
}
} else {
if(! saveRangeEnd(result2)) {
log.d("refresh-status-bottom: saveRangeEnd failed.")
break
}
}
}
} else {
@ -2711,6 +2845,13 @@ class Column(
break
}
if(isMisskey) {
log.d("refresh-status-offset: misskey does not allows gap reading.")
addOne(list_tmp, TootGap(max_id, last_since_id))
bGapAdded = true
break
}
val path =
path_base + delimiter + "max_id=" + max_id + "&since_id=" + last_since_id
val result2 = client.request(path)
@ -2767,9 +2908,23 @@ class Column(
TYPE_DIRECT_MESSAGES -> getStatusList(client, PATH_DIRECT_MESSAGES)
TYPE_LOCAL -> getStatusList(client, makePublicLocalUrl())
TYPE_LOCAL -> when(access_info.isMisskey) {
true -> getStatusList(
client,
"/api/notes/local-timeline",
isMisskey = true
)
else -> getStatusList(client, makePublicLocalUrl())
}
TYPE_FEDERATE -> getStatusList(client, makePublicFederateUrl())
TYPE_FEDERATE -> when(access_info.isMisskey) {
true -> getStatusList(
client,
"/api/notes/global-timeline",
isMisskey = true
)
else -> getStatusList(client, makePublicFederateUrl())
}
TYPE_FAVOURITES -> getStatusList(client, PATH_FAVOURITES)
@ -3015,15 +3170,15 @@ class Column(
//
val scroll_save = this@Column.scroll_save
when {
// ViewHolderがある場合は増加件数分+deltaの位置にスクロールする
// ViewHolderがある場合は増加件数分+deltaの位置にスクロールする
sp != null -> {
sp.adapterIndex += added
val delta = if(bSilent) 0f else - 20f
holder?.setScrollPosition(sp, delta)
}
// ViewHolderがなくて保存中の位置がある場合、増加件数分ずらす。deltaは難しいので反映しない
// ViewHolderがなくて保存中の位置がある場合、増加件数分ずらす。deltaは難しいので反映しない
scroll_save != null -> scroll_save.adapterIndex += added
// 保存中の位置がない場合、保存中の位置を新しく作る
// 保存中の位置がない場合、保存中の位置を新しく作る
else -> this@Column.scroll_save =
ScrollPosition(toAdapterIndex(added), 0)
}
@ -3054,6 +3209,8 @@ class Column(
return
}
viewHolder?.refreshLayout?.isRefreshing = true
bRefreshLoading = true
@ -3557,9 +3714,9 @@ class Column(
log.d("onStart: column is in initial loading.")
return
}
// フィルタ一覧のリロードが必要
if( filter_reload_required ){
if(filter_reload_required) {
filter_reload_required = false
startLoading()
return
@ -3659,8 +3816,11 @@ class Column(
return canStreaming() && column_type != TYPE_NOTIFICATIONS
}
internal fun canStreaming() : Boolean {
return ! access_info.isNA && if(access_info.isPseudo) isPublicStream else streamPath != null
internal fun canStreaming() = when {
access_info.isNA -> false
access_info.isMisskey -> false
access_info.isPseudo -> isPublicStream
else -> streamPath != null
}
private val streamCallback = object : StreamReader.StreamCallback {
@ -3918,6 +4078,7 @@ class Column(
} else {
PATH_LOCAL
}
}
private fun makePublicFederateUrl() : String {
@ -3940,7 +4101,7 @@ class Column(
}
private fun loadFilter2(client : TootApiClient) : ArrayList<TootFilter>? {
if( access_info.isPseudo ) return null
if(access_info.isPseudo) return null
val column_context = getFilterContext()
if(column_context == 0) return null
val result = client.request(PATH_FILTERS)
@ -3955,10 +4116,12 @@ class Column(
val tree = WordTrieTree()
for(filter in filterList) {
if((filter.context and column_context) != 0) {
tree.add(filter.phrase,validator = when(filter.whole_word){
true -> WordTrieTree.WORD_VALIDATOR
tree.add(
filter.phrase, validator = when(filter.whole_word) {
true -> WordTrieTree.WORD_VALIDATOR
else -> WordTrieTree.EMPTY_VALIDATOR
})
}
)
}
}
return tree
@ -4023,5 +4186,4 @@ class Column(
}
}
}

View File

@ -1028,7 +1028,13 @@ internal class ItemViewHolder(
}
btnSearchTag, llTrendTag -> when(item) {
is TootGap -> column.startGap(item)
is TootGap -> {
if( access_info.isMisskey){
showToast(activity,false, "Misskey does not allows gap reading.")
}else {
column.startGap(item)
}
}
is TootDomainBlock -> {
val domain = item.domain

View File

@ -25,7 +25,7 @@ internal fun findAccountByName(
) {
TootTaskRunner(activity).run(access_info, object : TootTask {
internal var who : TootAccount? = null
var who : TootAccount? = null
override fun background(client : TootApiClient) : TootApiResult? {
@ -38,10 +38,9 @@ internal fun findAccountByName(
for(i in 0 until array.length()) {
val a = parser.account(array.optJSONObject(i))
if(a != null) {
if(a.username == user && access_info.getFullAcct(a).equals(
user + "@" + host,
ignoreCase = true
)) {
if(a.username == user
&& access_info.getFullAcct(a).equals("$user@$host", ignoreCase = true)
) {
who = a
break
}
@ -62,11 +61,14 @@ internal fun findAccountByName(
// 既に存在する場合は再利用する
// 実アカウントを返すことはない
internal fun addPseudoAccount(
context : Context, host : String
context : Context,
host : String,
isMisskey : Boolean = false
) : SavedAccount? {
try {
val username = "?"
val full_acct = username + "@" + host
val full_acct = "$username@$host"
var account = SavedAccount.loadAccountByAcct(context, full_acct)
if(account != null) {
@ -77,7 +79,8 @@ internal fun addPseudoAccount(
account_info.put("username", username)
account_info.put("acct", username)
val row_id = SavedAccount.insert(host, full_acct, account_info, JSONObject())
val row_id =
SavedAccount.insert(host, full_acct, account_info, JSONObject(), isMisskey = isMisskey)
account = SavedAccount.loadAccount(context, row_id)
if(account == null) {
throw RuntimeException("loadAccount returns null.")
@ -132,7 +135,7 @@ internal fun loadRelation1(
client : TootApiClient, access_info : SavedAccount, who_id : Long
) : RelationResult {
val rr = RelationResult()
rr.result = client.request("/api/v1/accounts/relationships?id=" + who_id)
rr.result = client.request("/api/v1/accounts/relationships?id=$who_id")
val r2 = rr.result
val jsonArray = r2?.jsonArray
if(jsonArray != null) {

View File

@ -95,7 +95,7 @@ object Action_Account {
} else {
// 疑似アカウントを追加
val a = addPseudoAccount(activity, instance)
val a = addPseudoAccount(activity, instance, data.optBoolean("isMisskey",false))
if(a != null) {
showToast(activity, false, R.string.server_confirmed)
val pos = App1.getAppState(activity).column_list.size

View File

@ -18,6 +18,7 @@ object TootAccountMap{
ServiceType.MASTODON -> requireNotNull(parser.linkHelper.host)
ServiceType.TOOTSEARCH -> "?tootsearch"
ServiceType.MSP -> "?msp"
ServiceType.MISSKEY -> "?misskey"
}
}

View File

@ -445,10 +445,36 @@ class TootApiClient(
fun getInstanceInformation() : TootApiResult? {
val result = TootApiResult.makeWithCaption(instance)
if(result.error != null) return result
if(! sendRequest(result) {
if(sendRequest(result) {
Request.Builder().url("https://$instance/api/v1/instance").build()
}) return result
return parseJson(result)
}
&& parseJson(result) != null
&& result.jsonObject != null
) {
// インスタンス情報のjsonを読めたらマストドンのインスタンス
return result
}
// misskeyか試してみる
val r2 = TootApiResult.makeWithCaption(instance)
if(sendRequest(r2) {
Request.Builder().post(RequestBody.create(MEDIA_TYPE_JSON,JSONObject().apply{
put("dummy",1)
}.toString()))
.url("https://$instance/api/notes/local-timeline").build()
}
) {
if(parseJson(r2) != null && r2.jsonArray != null) {
r2.data = JSONObject().apply{
put("isMisskey", true)
}
return r2
}
}
// misskeyの事は忘れて本来のエラー情報を返す
return result
}
// インスタンス情報を取得する
@ -845,7 +871,7 @@ class TootApiClient(
////////////////////////////////////////////////////////////////////////
// JSONデータ以外を扱うリクエスト
fun http(req:Request) : TootApiResult? {
fun http(req : Request) : TootApiResult? {
val result = TootApiResult.makeWithCaption(req.url().host())
if(result.error != null) return result
@ -853,20 +879,19 @@ class TootApiClient(
return result
}
fun requestJson(req:Request) : TootApiResult? {
fun requestJson(req : Request) : TootApiResult? {
val result = TootApiResult.makeWithCaption(req.url().host())
if(result.error != null) return result
if( sendRequest(result, progressPath = null) { req } ){
if(sendRequest(result, progressPath = null) { req }) {
parseJson(result)
}
return result
}
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
fun getHttp(url:String): TootApiResult? {
fun getHttp(url : String) : TootApiResult? {
val result = http(Request.Builder().url(url).build())
if(result !=null && result.error == null){
if(result != null && result.error == null) {
parseString(result)
}
return result

View File

@ -4,4 +4,5 @@ enum class ServiceType {
MASTODON,
TOOTSEARCH,
MSP,
MISSKEY,
}

View File

@ -90,106 +90,155 @@ open class TootAccount(
init {
var sv : String?
// 絵文字データは先に読んでおく
this.custom_emojis = parseMapOrNull(::CustomEmoji, src.optJSONArray("emojis"))
this.profile_emojis = parseMapOrNull(::NicoProfileEmoji, src.optJSONArray("profile_emojis"))
// 疑似アカウントにacctとusernameだけ
this.url = src.parseString("url")
this.username = src.notEmptyOrThrow("username")
//
sv = src.parseString("display_name")
this.display_name = if(sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
//
this.note = src.parseString("note")
this.source = parseSource(src.optJSONObject("source"))
this.movedRef = TootAccountRef.mayNull(
parser,
src.optJSONObject("moved")?.let {
TootAccount(parser, it)
}
)
this.locked = src.optBoolean("locked")
this.fields = parseFields(src.optJSONArray("fields"))
this.bot = src.optBoolean("bot", false)
// this.user_hides_network = src.optBoolean("user_hides_network")
when(parser.serviceType) {
ServiceType.MASTODON -> {
val hostAccess = parser.linkHelper.host
this.id = src.parseLong("id") ?: INVALID_ID
this.acct = src.notEmptyOrThrow("acct")
this.host = findHostFromUrl(acct, hostAccess, url)
?: throw RuntimeException("can't get host from acct or url")
this.followers_count = src.parseLong("followers_count")
this.following_count = src.parseLong("following_count")
this.statuses_count = src.parseLong("statuses_count")
this.created_at = src.parseString("created_at")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.parseString("avatar")
this.avatar_static = src.parseString("avatar_static")
this.header = src.parseString("header")
this.header_static = src.parseString("header_static")
}
if(parser.serviceType == ServiceType.MISSKEY) {
ServiceType.TOOTSEARCH -> {
// tootsearch のアカウントのIDはどのタンス上のものか分からないので役に立たない
this.id = INVALID_ID // src.parseLong( "id", INVALID_ID)
sv = src.notEmptyOrThrow("acct")
this.host = findHostFromUrl(sv, null, url)
?: throw RuntimeException("can't get host from acct or url")
this.acct = this.username + "@" + this.host
this.followers_count = src.parseLong("followers_count")
this.following_count = src.parseLong("following_count")
this.statuses_count = src.parseLong("statuses_count")
this.created_at = src.parseString("created_at")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.parseString("avatar")
this.avatar_static = src.parseString("avatar_static")
this.header = src.parseString("header")
this.header_static = src.parseString("header_static")
}
val instance = src.parseString("host") ?: parser.linkHelper.host ?: error("missing host")
ServiceType.MSP -> {
this.id = src.parseLong("id") ?: INVALID_ID
// MSPはLTLの情報しか持ってないのでacctは常にホスト名部分を持たない
this.host = findHostFromUrl(null, null, url)
?: throw RuntimeException("can't get host from url")
this.acct = this.username + "@" + host
this.followers_count = null
this.following_count = null
this.statuses_count = null
this.created_at = null
this.time_created_at = 0L
val avatar = src.parseString("avatar")
this.avatar = avatar
this.avatar_static = avatar
this.header = null
this.header_static = null
}
this.custom_emojis = null
this.profile_emojis = null
this.username = src.notEmptyOrThrow("username")
this.url = "https://$instance/@$username"
//
sv = src.parseString("name")
this.display_name = if(sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
//
this.note = src.parseString("description")
this.source = null
this.movedRef = null
this.locked = src.optBoolean("isLocked")
this.fields = null
this.bot = src.optBoolean("isBot", false)
// this.user_hides_network = src.optBoolean("user_hides_network")
this.id = INVALID_ID
this.acct = "$username@$instance"
this.host = instance
this.followers_count = src.parseLong("followersCount") ?: -1L
this.following_count = src.parseLong("followingCount") ?: -1L
this.statuses_count = src.parseLong("notesCount") ?: -1L
this.created_at = src.parseString("createdAt")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.parseString("avatarUrl")
this.avatar_static = src.parseString("avatarUrl")
this.header =src.parseString("bannerUrl")
this.header_static = src.parseString("bannerUrl")
} else {
// 絵文字データは先に読んでおく
this.custom_emojis = parseMapOrNull(::CustomEmoji, src.optJSONArray("emojis"))
this.profile_emojis =
parseMapOrNull(::NicoProfileEmoji, src.optJSONArray("profile_emojis"))
// 疑似アカウントにacctとusernameだけ
this.url = src.parseString("url")
this.username = src.notEmptyOrThrow("username")
//
sv = src.parseString("display_name")
this.display_name = if(sv?.isNotEmpty() == true) sv.sanitizeBDI() else username
//
this.note = src.parseString("note")
this.source = parseSource(src.optJSONObject("source"))
this.movedRef = TootAccountRef.mayNull(
parser,
src.optJSONObject("moved")?.let {
TootAccount(parser, it)
}
)
this.locked = src.optBoolean("locked")
this.fields = parseFields(src.optJSONArray("fields"))
this.bot = src.optBoolean("bot", false)
// this.user_hides_network = src.optBoolean("user_hides_network")
when(parser.serviceType) {
ServiceType.MASTODON -> {
val hostAccess = parser.linkHelper.host
this.id = src.parseLong("id") ?: INVALID_ID
this.acct = src.notEmptyOrThrow("acct")
this.host = findHostFromUrl(acct, hostAccess, url)
?: throw RuntimeException("can't get host from acct or url")
this.followers_count = src.parseLong("followers_count")
this.following_count = src.parseLong("following_count")
this.statuses_count = src.parseLong("statuses_count")
this.created_at = src.parseString("created_at")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.parseString("avatar")
this.avatar_static = src.parseString("avatar_static")
this.header = src.parseString("header")
this.header_static = src.parseString("header_static")
}
ServiceType.TOOTSEARCH -> {
// tootsearch のアカウントのIDはどのタンス上のものか分からないので役に立たない
this.id = INVALID_ID // src.parseLong( "id", INVALID_ID)
sv = src.notEmptyOrThrow("acct")
this.host = findHostFromUrl(sv, null, url)
?: throw RuntimeException("can't get host from acct or url")
this.acct = this.username + "@" + this.host
this.followers_count = src.parseLong("followers_count")
this.following_count = src.parseLong("following_count")
this.statuses_count = src.parseLong("statuses_count")
this.created_at = src.parseString("created_at")
this.time_created_at = TootStatus.parseTime(this.created_at)
this.avatar = src.parseString("avatar")
this.avatar_static = src.parseString("avatar_static")
this.header = src.parseString("header")
this.header_static = src.parseString("header_static")
}
ServiceType.MSP -> {
this.id = src.parseLong("id") ?: INVALID_ID
// MSPはLTLの情報しか持ってないのでacctは常にホスト名部分を持たない
this.host = findHostFromUrl(null, null, url)
?: throw RuntimeException("can't get host from url")
this.acct = this.username + "@" + host
this.followers_count = null
this.following_count = null
this.statuses_count = null
this.created_at = null
this.time_created_at = 0L
val avatar = src.parseString("avatar")
this.avatar = avatar
this.avatar_static = avatar
this.header = null
this.header_static = null
}
else -> error("will not happen")
}
}
}

View File

@ -13,4 +13,9 @@ class TootApplication(
name = src.parseString("name"),
website = src.parseString("website")
)
constructor(src:String?):this(
name = src,
website = null
)
}

View File

@ -18,8 +18,10 @@ import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.*
import org.json.JSONArray
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
@Suppress("MemberVisibilityCanPrivate")
class TootStatus(parser : TootParser, src : JSONObject) :
@ -155,133 +157,271 @@ class TootStatus(parser : TootParser, src : JSONObject) :
init {
this.json = src
this.uri = src.parseString("uri") // MSPだとuriは提供されない
this.url = src.parseString("url") // 頻繁にnullになる
this.created_at = src.parseString("created_at")
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = parseMapOrNull(::CustomEmoji, src.optJSONArray("emojis"), log)
this.profile_emojis =
parseMapOrNull(::NicoProfileEmoji, src.optJSONArray("profile_emojis"), log)
val who = parser.account(src.optJSONObject("account"))
?: throw RuntimeException("missing account")
this.accountRef = TootAccountRef(parser, who)
this.reblogs_count = src.parseLong("reblogs_count")
this.favourites_count = src.parseLong("favourites_count")
this.replies_count = src.parseLong("replies_count")
if( parser.serviceType == ServiceType.MISSKEY) {
val instance = parser.linkHelper.host
val misskeyId = src.parseString("id")
this.host_access = parser.linkHelper.host
this.uri = "https://$instance/notes/$misskeyId"
this.url = "https://$instance/notes/$misskeyId"
this.created_at = src.parseString("createdAt")
this.time_created_at = parseTime(this.created_at)
this.id = INVALID_ID
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = null
this.profile_emojis = null
val who = parser.account(src.optJSONObject("user"))
?: throw RuntimeException("missing account")
this.accountRef = TootAccountRef(parser, who)
this.reblogs_count = 0L
this.favourites_count = 0L
this.replies_count = 0L
this.reblogged = false
this.favourited = false
this.media_attachments = parseMediaAttachmentMisskey(src.optJSONArray("media"))
this.visibility = src.parseString("visibility")
this.sensitive = src.optBoolean("sensitive")
this.in_reply_to_id = null
this.in_reply_to_account_id = null
this.mentions = null
this.tags = null
this.application = parseItem(::TootApplication, src.optJSONObject("appId"), log)
this.pinned = parser.pinned
this.muted = false
this.language = null
this.decoded_mentions = HTMLDecoder.decodeMentions(
parser.linkHelper,
this.mentions,
this
) ?: EMPTY_SPANNABLE
// this.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
// content
this.content = src.parseString("text")
var options = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapCustom = custom_emojis,
emojiMapProfile = profile_emojis,
attachmentList = media_attachments,
highlightTrie = parser.highlightTrie
)
this.decoded_content = options.decodeHTML(content)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
// spoiler_text
this.spoiler_text = reWhitespace
.matcher(src.parseString("cw") ?: "")
.replaceAll(" ")
.sanitizeBDI()
options = DecodeOptions(
parser.context,
emojiMapCustom = custom_emojis,
emojiMapProfile = profile_emojis,
highlightTrie = parser.highlightTrie
)
this.decoded_spoiler_text = options.decodeEmoji(spoiler_text)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
this.enquete = null
this.reblog = parser.status(src.optJSONObject("renote"))
when(parser.serviceType) {
ServiceType.MASTODON -> {
this.host_access = parser.linkHelper.host
}else{
this.uri = src.parseString("uri") // MSPだとuriは提供されない
this.url = src.parseString("url") // 頻繁にnullになる
this.created_at = src.parseString("created_at")
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis = parseMapOrNull(::CustomEmoji, src.optJSONArray("emojis"), log)
this.profile_emojis =
parseMapOrNull(::NicoProfileEmoji, src.optJSONArray("profile_emojis"), log)
val who = parser.account(src.optJSONObject("account"))
?: throw RuntimeException("missing account")
this.accountRef = TootAccountRef(parser, who)
this.reblogs_count = src.parseLong("reblogs_count")
this.favourites_count = src.parseLong("favourites_count")
this.replies_count = src.parseLong("replies_count")
when(parser.serviceType) {
ServiceType.MASTODON -> {
this.host_access = parser.linkHelper.host
this.id = src.parseLong("id") ?: INVALID_ID
this.reblogged = src.optBoolean("reblogged")
this.favourited = src.optBoolean("favourited")
this.time_created_at = parseTime(this.created_at)
this.media_attachments =
parseListOrNull(::TootAttachment, src.optJSONArray("media_attachments"), log)
this.visibility = src.parseString("visibility")
this.sensitive = src.optBoolean("sensitive")
}
this.id = src.parseLong("id") ?: INVALID_ID
this.reblogged = src.optBoolean("reblogged")
this.favourited = src.optBoolean("favourited")
this.time_created_at = parseTime(this.created_at)
this.media_attachments =
parseListOrNull(::TootAttachment, src.optJSONArray("media_attachments"), log)
this.visibility = src.parseString("visibility")
this.sensitive = src.optBoolean("sensitive")
ServiceType.TOOTSEARCH -> {
this.host_access = null
// 投稿元タンスでのIDを調べる。失敗するかもしれない
this.id = findStatusIdFromUri(uri, url)
this.time_created_at = TootStatus.parseTime(this.created_at)
this.media_attachments =
parseListOrNull(::TootAttachment, src.optJSONArray("media_attachments"), log)
this.visibility = VISIBILITY_PUBLIC
this.sensitive = src.optBoolean("sensitive")
}
ServiceType.MSP -> {
this.host_access = null
// MSPのデータはLTLから呼んだものなので、常に投稿元タンスでのidが得られる
this.id = src.parseLong("id") ?: INVALID_ID
this.time_created_at = parseTimeMSP(created_at)
this.media_attachments =
TootAttachmentMSP.parseList(src.optJSONArray("media_attachments"))
this.visibility = VISIBILITY_PUBLIC
this.sensitive = src.optInt("sensitive", 0) != 0
}
else-> error("will not happen")
}
ServiceType.TOOTSEARCH -> {
this.host_access = null
// 投稿元タンスでのIDを調べる。失敗するかもしれない
this.id = findStatusIdFromUri(uri, url)
this.time_created_at = TootStatus.parseTime(this.created_at)
this.media_attachments =
parseListOrNull(::TootAttachment, src.optJSONArray("media_attachments"), log)
this.visibility = VISIBILITY_PUBLIC
this.sensitive = src.optBoolean("sensitive")
this.in_reply_to_id = src.parseString("in_reply_to_id")
this.in_reply_to_account_id = src.parseString("in_reply_to_account_id")
this.mentions = parseListOrNull(::TootMention, src.optJSONArray("mentions"), log)
this.tags = parseListOrNull(::TootTag, src.optJSONArray("tags"))
this.application = parseItem(::TootApplication, src.optJSONObject("application"), log)
this.pinned = parser.pinned || src.optBoolean("pinned")
this.muted = src.optBoolean("muted")
this.language = src.parseString("language")
this.decoded_mentions = HTMLDecoder.decodeMentions(
parser.linkHelper,
this.mentions,
this
) ?: EMPTY_SPANNABLE
// this.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
// content
this.content = src.parseString("content")
var options = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapCustom = custom_emojis,
emojiMapProfile = profile_emojis,
attachmentList = media_attachments,
highlightTrie = parser.highlightTrie
)
this.decoded_content = options.decodeHTML(content)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
ServiceType.MSP -> {
this.host_access = null
// MSPのデータはLTLから呼んだものなので、常に投稿元タンスでのidが得られる
this.id = src.parseLong("id") ?: INVALID_ID
this.time_created_at = parseTimeMSP(created_at)
this.media_attachments =
TootAttachmentMSP.parseList(src.optJSONArray("media_attachments"))
this.visibility = VISIBILITY_PUBLIC
this.sensitive = src.optInt("sensitive", 0) != 0
// spoiler_text
this.spoiler_text = reWhitespace
.matcher(src.parseString("spoiler_text") ?: "")
.replaceAll(" ")
.sanitizeBDI()
options = DecodeOptions(
parser.context,
emojiMapCustom = custom_emojis,
emojiMapProfile = profile_emojis,
highlightTrie = parser.highlightTrie
)
this.decoded_spoiler_text = options.decodeEmoji(spoiler_text)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
this.enquete = NicoEnquete.parse(
parser,
this,
media_attachments,
src.parseString("enquete")
)
// Pinned TL を取得した時にreblogが登場することはないので、reblogについてpinned 状態を気にする必要はない
this.reblog = parser.status(src.optJSONObject("reblog"))
}
}
private fun parseMediaAttachmentMisskey2(src : JSONObject?) : TootAttachment? {
src?: return null
val mimeType = src.parseString("type")
val url = src.parseString("url")
val thumbnailUrl = src.parseString("thumbnailUrl")
val dst = JSONObject()
dst.put("id",-1L)
dst.put("type", when{
mimeType?.startsWith("image/") ==true -> TootAttachmentLike.TYPE_IMAGE
mimeType?.startsWith("video/") ==true -> TootAttachmentLike.TYPE_VIDEO
else-> TootAttachmentLike.TYPE_UNKNOWN
})
dst.put("url",url)
dst.put("remote_url",url)
dst.put("text_url",url)
dst.put("preview_url",thumbnailUrl)
dst.put("description",src.parseString("comment"))
return parseItem(::TootAttachment,dst)
}
private fun parseMediaAttachmentMisskey(src : JSONArray?) : ArrayList<TootAttachmentLike>? {
var rv :ArrayList<TootAttachmentLike>? = null
if(src!=null){
for(i in 0 until src.length() ){
val item = try{
parseMediaAttachmentMisskey2(src.optJSONObject(i))
}catch(ex:Throwable){
log.e(ex,"parseMediaAttachmentMisskey")
null
}
if( item != null ){
if(rv==null) rv = ArrayList()
rv.add(item)
}
}
}
this.in_reply_to_id = src.parseString("in_reply_to_id")
this.in_reply_to_account_id = src.parseString("in_reply_to_account_id")
this.mentions = parseListOrNull(::TootMention, src.optJSONArray("mentions"), log)
this.tags = parseListOrNull(::TootTag, src.optJSONArray("tags"))
this.application = parseItem(::TootApplication, src.optJSONObject("application"), log)
this.pinned = parser.pinned || src.optBoolean("pinned")
this.muted = src.optBoolean("muted")
this.language = src.parseString("language")
this.decoded_mentions = HTMLDecoder.decodeMentions(
parser.linkHelper,
this.mentions,
this
) ?: EMPTY_SPANNABLE
// this.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
// content
this.content = src.parseString("content")
var options = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapCustom = custom_emojis,
emojiMapProfile = profile_emojis,
attachmentList = media_attachments,
highlightTrie = parser.highlightTrie
)
this.decoded_content = options.decodeHTML(content)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
// spoiler_text
this.spoiler_text = reWhitespace
.matcher(src.parseString("spoiler_text") ?: "")
.replaceAll(" ")
.sanitizeBDI()
options = DecodeOptions(
parser.context,
emojiMapCustom = custom_emojis,
emojiMapProfile = profile_emojis,
highlightTrie = parser.highlightTrie
)
this.decoded_spoiler_text = options.decodeEmoji(spoiler_text)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
this.enquete = NicoEnquete.parse(
parser,
this,
media_attachments,
src.parseString("enquete")
)
// Pinned TL を取得した時にreblogが登場することはないので、reblogについてpinned 状態を気にする必要はない
this.reblog = parser.status(src.optJSONObject("reblog"))
return rv
}
///////////////////////////////////////////////////

View File

@ -28,7 +28,8 @@ class SavedAccount(
val acct : String,
hostArg : String? = null,
var token_info : JSONObject? = null,
var loginAccount : TootAccount? = null // 疑似アカウントではnull
var loginAccount : TootAccount? = null, // 疑似アカウントではnull
var isMisskey :Boolean = false // 疑似アカウントでのみtrue
) : LinkHelper {
val username : String
@ -156,6 +157,8 @@ class SavedAccount(
this.sound_uri = cursor.getString(cursor.getColumnIndex(COL_SOUND_URI))
this.default_text = cursor.getString(cursor.getColumnIndex(COL_DEFAULT_TEXT)) ?: ""
this.isMisskey = cursor.getInt(cursor.getColumnIndex(COL_IS_MISSKEY)).i2b()
}
val isNA : Boolean
@ -405,6 +408,9 @@ class SavedAccount(
// スキーマ27から
private const val COL_DEFAULT_TEXT = "default_text"
// スキーマ28から
private const val COL_IS_MISSKEY = "is_misskey"
/////////////////////////////////
// login information
const val INVALID_DB_ID = - 1L
@ -466,6 +472,8 @@ class SavedAccount(
// 以下はDBスキーマ27で更新
+ ",$COL_DEFAULT_TEXT text default ''"
// 以下はDBスキーマ28で更新
+ ",$COL_IS_MISSKEY integer default 0"
+ ")"
)
db.execSQL("create index if not exists ${table}_user on ${table}(u)")
@ -592,7 +600,14 @@ class SavedAccount(
}
}
if(oldVersion < 28 && newVersion >= 28) {
try {
db.execSQL("alter table $table add column $COL_IS_MISSKEY integer default 0")
} catch(ex : Throwable) {
log.trace(ex)
}
}
}
// 横断検索用の、何とも紐ついていないアカウント
@ -621,7 +636,8 @@ class SavedAccount(
host : String,
acct : String,
account : JSONObject,
token : JSONObject
token : JSONObject,
isMisskey : Boolean = false
) : Long {
try {
val cv = ContentValues()
@ -629,6 +645,7 @@ class SavedAccount(
cv.put(COL_USER, acct)
cv.put(COL_ACCOUNT, account.toString())
cv.put(COL_TOKEN, token.toString())
cv.put(COL_IS_MISSKEY, isMisskey.b2i() )
return App1.database.insert(table, null, cv)
} catch(ex : Throwable) {
log.trace(ex)

View File

@ -20,7 +20,10 @@ import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.webkit.MimeTypeMap
import android.widget.Toast
import jp.juggler.subwaytooter.api.TootApiClient
import me.drakeet.support.toast.ToastCompat
import okhttp3.Request
import okhttp3.RequestBody
import org.apache.commons.io.IOUtils
import org.json.JSONArray
import org.json.JSONObject
@ -651,6 +654,9 @@ fun JSONObject.parseInt(key : String) : Int? {
}
}
fun JSONObject.toPostRequestBuilder()=
Request.Builder().post(RequestBody.create(TootApiClient.MEDIA_TYPE_JSON,this.toString()))
////////////////////////////////////////////////////////////////////
// Bundle

View File

@ -836,6 +836,8 @@ mimumedon.com
mindful.masto.host
minidon.bacardi55.org
misanthropy.wang
misskey.xyz
misskey.jp
mist.so
mistermi.me
mn.kitetu.com