2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter.api.entity
|
|
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.text.Spannable
|
|
|
|
|
import android.text.SpannableString
|
|
|
|
|
import jp.juggler.subwaytooter.App1
|
|
|
|
|
import jp.juggler.subwaytooter.Pref
|
|
|
|
|
import jp.juggler.subwaytooter.R
|
2018-01-12 10:01:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiClient
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
|
|
|
|
|
import java.lang.ref.WeakReference
|
|
|
|
|
import java.util.regex.Pattern
|
|
|
|
|
|
|
|
|
|
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.*
|
|
|
|
|
|
|
|
|
|
@Suppress("MemberVisibilityCanPrivate")
|
2018-01-20 07:51:14 +01:00
|
|
|
|
class TootStatus(parser : TootParser, src : JSONObject, serviceType : ServiceType) :TimelineItem(){
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
val json : JSONObject
|
|
|
|
|
|
|
|
|
|
// A Fediverse-unique resource ID
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// MSP から取得したデータだと uri は提供されずnullになる
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val uri : String?
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// URL to the status page (can be remote)
|
|
|
|
|
// ブーストだとnullになる
|
|
|
|
|
val url : String?
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// 投稿元タンスのホスト名
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val host_original : String
|
|
|
|
|
get() = account.host
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// 取得タンスのホスト名。トゥート検索サービスでは提供されずnullになる
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val host_access : String?
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// ステータスID。
|
|
|
|
|
// host_access が null の場合は投稿元タンスでのIDかもしれない。
|
|
|
|
|
// 取得に失敗するとINVALID_IDになる
|
|
|
|
|
val id : Long
|
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
// The TootAccount which posted the status
|
|
|
|
|
val account : TootAccount
|
|
|
|
|
|
|
|
|
|
//The number of reblogs for the status
|
2018-01-10 16:47:35 +01:00
|
|
|
|
var reblogs_count : Long? = null // アプリから変更する。検索サービスでは提供されない(null)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
//The number of favourites for the status
|
2018-01-10 16:47:35 +01:00
|
|
|
|
var favourites_count : Long? = null // アプリから変更する。検索サービスでは提供されない(null)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// Whether the authenticated user has reblogged the status
|
|
|
|
|
var reblogged : Boolean = false // アプリから変更する
|
|
|
|
|
|
|
|
|
|
// Whether the authenticated user has favourited the status
|
|
|
|
|
var favourited : Boolean = false // アプリから変更する
|
|
|
|
|
|
|
|
|
|
// Whether the authenticated user has muted the conversation this status from
|
|
|
|
|
var muted : Boolean = false // アプリから変更する
|
|
|
|
|
|
|
|
|
|
// 固定されたトゥート
|
|
|
|
|
var pinned : Boolean = false // アプリから変更する
|
|
|
|
|
|
|
|
|
|
//Whether media attachments should be hidden by default
|
|
|
|
|
val sensitive : Boolean
|
|
|
|
|
|
|
|
|
|
// The detected language for the status, if detected
|
|
|
|
|
val language : String?
|
|
|
|
|
|
|
|
|
|
//If not empty, warning text that should be displayed before the actual content
|
|
|
|
|
val spoiler_text : String?
|
|
|
|
|
val decoded_spoiler_text : Spannable
|
|
|
|
|
|
|
|
|
|
// Body of the status; this will contain HTML (remote HTML already sanitized)
|
|
|
|
|
val content : String?
|
|
|
|
|
val decoded_content : Spannable
|
|
|
|
|
|
|
|
|
|
//Application from which the status was posted
|
|
|
|
|
val application : TootApplication?
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
val custom_emojis : HashMap<String, CustomEmoji>?
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
val profile_emojis : HashMap<String, NicoProfileEmoji>?
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// The time the status was created
|
|
|
|
|
val created_at : String?
|
|
|
|
|
|
|
|
|
|
// null or the ID of the status it replies to
|
|
|
|
|
val in_reply_to_id : String?
|
|
|
|
|
|
|
|
|
|
// null or the ID of the account it replies to
|
|
|
|
|
val in_reply_to_account_id : String?
|
|
|
|
|
|
|
|
|
|
// null or the reblogged Status
|
|
|
|
|
val reblog : TootStatus?
|
|
|
|
|
|
|
|
|
|
//One of: public, unlisted, private, direct
|
|
|
|
|
val visibility : String?
|
|
|
|
|
|
|
|
|
|
// An array of Attachments
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val media_attachments : ArrayList<TootAttachmentLike>?
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// An array of Mentions
|
2018-01-10 16:47:35 +01:00
|
|
|
|
var mentions : ArrayList<TootMention>? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
//An array of Tags
|
2018-01-10 16:47:35 +01:00
|
|
|
|
var tags : ArrayList<TootTag>? = null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
// public Spannable decoded_tags;
|
|
|
|
|
var decoded_mentions : Spannable = EMPTY_SPANNABLE
|
|
|
|
|
|
|
|
|
|
var conversation_main : Boolean = false
|
|
|
|
|
|
|
|
|
|
var enquete : NicoEnquete? = null
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////
|
|
|
|
|
// 以下はentityから取得したデータではなく、アプリ内部で使う
|
|
|
|
|
|
|
|
|
|
class AutoCW(
|
|
|
|
|
var refActivity : WeakReference<Any>? = null,
|
|
|
|
|
var cell_width : Int = 0,
|
|
|
|
|
var decoded_spoiler_text : Spannable? = null,
|
|
|
|
|
var originalLineCount : Int = 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// アプリ内部で使うワークエリア
|
|
|
|
|
var auto_cw : AutoCW? = null
|
|
|
|
|
|
|
|
|
|
// 会話の流れビューで後から追加する
|
|
|
|
|
var card : TootCard? = null
|
|
|
|
|
|
|
|
|
|
var highlight_sound : HighlightWord? = null
|
|
|
|
|
|
|
|
|
|
var hasHighlight : Boolean = false
|
|
|
|
|
|
|
|
|
|
class List : ArrayList<TootStatus>()
|
|
|
|
|
|
|
|
|
|
val time_created_at : Long
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
this.json = src
|
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
this.uri = Utils.optStringX(src, "uri") // MSPだとuriは提供されない
|
|
|
|
|
this.url = Utils.optStringX(src, "url") // 頻繁にnullになる
|
|
|
|
|
this.created_at = Utils.optStringX(src, "created_at")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
2018-01-11 10:31:25 +01:00
|
|
|
|
this.custom_emojis = parseMapOrNull(::CustomEmoji, src.optJSONArray("emojis"), log)
|
|
|
|
|
this.profile_emojis = parseMapOrNull(::NicoProfileEmoji, src.optJSONArray("profile_emojis"), log)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
this.account = TootAccount.parse(
|
2018-01-04 19:52:25 +01:00
|
|
|
|
parser.context,
|
|
|
|
|
parser.accessInfo,
|
|
|
|
|
src.optJSONObject("account"),
|
|
|
|
|
serviceType
|
|
|
|
|
) ?: throw RuntimeException("missing account")
|
2018-01-11 10:31:25 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
when(serviceType) {
|
|
|
|
|
ServiceType.MASTODON -> {
|
|
|
|
|
this.host_access = parser.accessInfo.host
|
2018-01-11 10:31:25 +01:00
|
|
|
|
|
|
|
|
|
this.id = Utils.optLongX(src, "id", INVALID_ID)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
this.reblogs_count = Utils.optLongX(src, "reblogs_count")
|
|
|
|
|
this.favourites_count = Utils.optLongX(src, "favourites_count")
|
|
|
|
|
this.reblogged = src.optBoolean("reblogged")
|
|
|
|
|
this.favourited = src.optBoolean("favourited")
|
|
|
|
|
|
|
|
|
|
this.time_created_at = parseTime(this.created_at)
|
2018-01-11 10:31:25 +01:00
|
|
|
|
this.media_attachments = parseListOrNull(::TootAttachment, src.optJSONArray("media_attachments"), log)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.visibility = Utils.optStringX(src, "visibility")
|
|
|
|
|
this.sensitive = src.optBoolean("sensitive")
|
|
|
|
|
|
|
|
|
|
}
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
|
|
|
|
ServiceType.TOOTSEARCH -> {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.host_access = null
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// 投稿元タンスでのIDを調べる。失敗するかもしれない
|
|
|
|
|
this.id = findStatusIdFromUri(uri, url)
|
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
|
|
|
|
this.reblogs_count = Utils.optLongX(src, "reblogs_count")
|
|
|
|
|
this.favourites_count = Utils.optLongX(src, "favourites_count")
|
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.time_created_at = TootStatus.parseTime(this.created_at)
|
2018-01-11 10:31:25 +01:00
|
|
|
|
this.media_attachments = parseListOrNull(::TootAttachment, src.optJSONArray("media_attachments"), log)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.visibility = VISIBILITY_PUBLIC
|
|
|
|
|
this.sensitive = src.optBoolean("sensitive")
|
|
|
|
|
|
|
|
|
|
}
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
|
|
|
|
ServiceType.MSP -> {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.host_access = null
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// MSPのデータはLTLから呼んだものなので、常に投稿元タンスでのidが得られる
|
|
|
|
|
this.id = Utils.optLongX(src, "id", INVALID_ID)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
this.time_created_at = parseTimeMSP(created_at)
|
|
|
|
|
this.media_attachments = TootAttachmentMSP.parseList(src.optJSONArray("media_attachments"))
|
|
|
|
|
this.visibility = VISIBILITY_PUBLIC
|
2018-01-10 16:47:35 +01:00
|
|
|
|
this.sensitive = src.optInt("sensitive", 0) != 0
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-11 10:31:25 +01:00
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.in_reply_to_id = Utils.optStringX(src, "in_reply_to_id")
|
|
|
|
|
this.in_reply_to_account_id = Utils.optStringX(src, "in_reply_to_account_id")
|
2018-01-11 10:31:25 +01:00
|
|
|
|
this.mentions = parseListOrNull(::TootMention, src.optJSONArray("mentions"), log)
|
|
|
|
|
this.tags = parseListOrNull(::TootTag, src.optJSONArray("tags"))
|
|
|
|
|
this.application = parseItem(::TootApplication, src.optJSONObject("application"), log)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
this.pinned = parser.pinned || src.optBoolean("pinned")
|
|
|
|
|
this.muted = src.optBoolean("muted")
|
|
|
|
|
this.language = Utils.optStringX(src, "language")
|
|
|
|
|
this.decoded_mentions = HTMLDecoder.decodeMentions(parser.accessInfo, this.mentions, this) ?: EMPTY_SPANNABLE
|
|
|
|
|
// this.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
|
|
|
|
|
|
|
|
|
|
// content
|
|
|
|
|
this.content = Utils.optStringX(src, "content")
|
2018-01-11 10:31:25 +01:00
|
|
|
|
var options = DecodeOptions(
|
|
|
|
|
short = true,
|
|
|
|
|
decodeEmoji = true,
|
|
|
|
|
emojiMapCustom = custom_emojis,
|
|
|
|
|
emojiMapProfile = profile_emojis,
|
|
|
|
|
attachmentList = media_attachments,
|
|
|
|
|
highlightTrie = parser.highlightTrie
|
|
|
|
|
|
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
this.decoded_content = options.decodeHTML(parser.context, parser.accessInfo, 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 = Utils.sanitizeBDI(
|
|
|
|
|
reWhitespace
|
|
|
|
|
.matcher(Utils.optStringX(src, "spoiler_text") ?: "")
|
|
|
|
|
.replaceAll(" ")
|
|
|
|
|
)
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
options = DecodeOptions(
|
|
|
|
|
emojiMapCustom = custom_emojis,
|
|
|
|
|
emojiMapProfile = profile_emojis,
|
|
|
|
|
highlightTrie = parser.highlightTrie
|
|
|
|
|
)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
this.decoded_spoiler_text = options.decodeEmoji(parser.context, 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.context,
|
|
|
|
|
parser.accessInfo,
|
|
|
|
|
this,
|
|
|
|
|
media_attachments,
|
|
|
|
|
Utils.optStringX(src, "enquete")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Pinned TL を取得した時にreblogが登場することはないので、reblogについてpinned 状態を気にする必要はない
|
|
|
|
|
this.reblog = parse(parser, src.optJSONObject("reblog"), serviceType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////
|
|
|
|
|
// ユーティリティ
|
|
|
|
|
|
|
|
|
|
val hostAccessOrOriginal : String
|
|
|
|
|
get() = validHost(host_access) ?: validHost(host_original) ?: "(null)"
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
// id != -1L ? id : findStatusIdFromUri();
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val idAccessOrOriginal : Long
|
|
|
|
|
get() = id
|
|
|
|
|
|
|
|
|
|
val busyKey : String
|
|
|
|
|
get() = hostAccessOrOriginal + ":" + idAccessOrOriginal
|
|
|
|
|
|
|
|
|
|
fun checkMuted(muted_app : HashSet<String>?, muted_word : WordTrieTree?) : Boolean {
|
|
|
|
|
|
|
|
|
|
// app mute
|
2018-01-10 16:47:35 +01:00
|
|
|
|
if(muted_app != null) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val name = application?.name
|
|
|
|
|
if(name != null && muted_app.contains(name)) return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// word mute
|
|
|
|
|
if(muted_word != null) {
|
|
|
|
|
if(muted_word.matchShort(decoded_content)) return true
|
|
|
|
|
if(muted_word.matchShort(decoded_spoiler_text)) return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// reblog
|
|
|
|
|
return true == reblog?.checkMuted(muted_app, muted_word)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun hasMedia() : Boolean {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
return (media_attachments?.size ?: 0) > 0
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun canPin(access_info : SavedAccount) : Boolean {
|
|
|
|
|
return (reblog == null
|
|
|
|
|
&& access_info.isMe(account)
|
|
|
|
|
&& (VISIBILITY_PUBLIC == visibility || VISIBILITY_UNLISTED == visibility))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
internal val log = LogCategory("TootStatus")
|
|
|
|
|
|
|
|
|
|
const val VISIBILITY_PUBLIC = "public"
|
|
|
|
|
const val VISIBILITY_UNLISTED = "unlisted"
|
|
|
|
|
const val VISIBILITY_PRIVATE = "private"
|
|
|
|
|
const val VISIBILITY_DIRECT = "direct"
|
|
|
|
|
const val VISIBILITY_WEB_SETTING = "web_setting"
|
|
|
|
|
|
|
|
|
|
private val reWhitespace = Pattern.compile("[\\s\\t\\x0d\\x0a]+")
|
|
|
|
|
|
|
|
|
|
val EMPTY_SPANNABLE = SpannableString("")
|
|
|
|
|
|
|
|
|
|
// OStatus
|
|
|
|
|
@Suppress("HasPlatformType")
|
|
|
|
|
val reTootUriOS = Pattern.compile("tag:([^,]*),[^:]*:objectId=(\\d+):objectType=Status", Pattern.CASE_INSENSITIVE)
|
|
|
|
|
|
|
|
|
|
// ActivityPub 1
|
|
|
|
|
@Suppress("HasPlatformType")
|
|
|
|
|
val reTootUriAP1 = Pattern.compile("https?://([^/]+)/users/[A-Za-z0-9_]+/statuses/(\\d+)")
|
|
|
|
|
|
|
|
|
|
// ActivityPub 2
|
|
|
|
|
@Suppress("HasPlatformType")
|
|
|
|
|
val reTootUriAP2 = Pattern.compile("https?://([^/]+)/@[A-Za-z0-9_]+/(\\d+)")
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
const val INVALID_ID = - 1L
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
fun parse(
|
|
|
|
|
parser : TootParser,
|
|
|
|
|
src : JSONObject?,
|
|
|
|
|
serviceType : ServiceType = ServiceType.MASTODON
|
|
|
|
|
) : TootStatus? {
|
|
|
|
|
if(src == null) return null
|
|
|
|
|
return try {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
TootStatus(parser, src, serviceType)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
log.e(ex, "parse failed.")
|
|
|
|
|
null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseList(
|
|
|
|
|
parser : TootParser,
|
|
|
|
|
array : JSONArray?,
|
|
|
|
|
serviceType : ServiceType = ServiceType.MASTODON
|
|
|
|
|
) : TootStatus.List {
|
|
|
|
|
val result = TootStatus.List()
|
|
|
|
|
if(array != null) {
|
|
|
|
|
val array_size = array.length()
|
|
|
|
|
result.ensureCapacity(array_size)
|
|
|
|
|
for(i in 0 until array_size) {
|
|
|
|
|
val src = array.optJSONObject(i) ?: continue
|
|
|
|
|
val item = parse(parser, src, serviceType) ?: continue
|
|
|
|
|
result.add(item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseListOrNull(
|
|
|
|
|
parser : TootParser,
|
|
|
|
|
array : JSONArray?,
|
2018-01-10 16:47:35 +01:00
|
|
|
|
serviceType : ServiceType = ServiceType.MASTODON
|
2018-01-04 19:52:25 +01:00
|
|
|
|
) : TootStatus.List? {
|
|
|
|
|
array ?: return null
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val result = parseList(parser, array, serviceType)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
return if(result.isEmpty()) null else result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun parseListTootsearch(
|
|
|
|
|
parser : TootParser,
|
|
|
|
|
root : JSONObject,
|
|
|
|
|
serviceType : ServiceType = ServiceType.TOOTSEARCH
|
|
|
|
|
) : TootStatus.List {
|
|
|
|
|
val result = TootStatus.List()
|
2018-01-12 10:01:25 +01:00
|
|
|
|
val array = TootApiClient.getTootsearchHits(root)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
if(array != null) {
|
|
|
|
|
val array_size = array.length()
|
|
|
|
|
result.ensureCapacity(array_size)
|
|
|
|
|
for(i in 0 until array.length()) {
|
|
|
|
|
try {
|
|
|
|
|
val src = array.optJSONObject(i) ?: continue
|
|
|
|
|
val src2 = src.optJSONObject("_source")
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val item = parse(parser, src2, serviceType) ?: continue
|
2018-01-04 19:52:25 +01:00
|
|
|
|
result.add(item)
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val tz_utc = TimeZone.getTimeZone("UTC")
|
|
|
|
|
|
|
|
|
|
private val reTime = Pattern.compile("\\A(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)")
|
|
|
|
|
|
|
|
|
|
private val reMSPTime = Pattern.compile("\\A(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)\\D+(\\d+)")
|
|
|
|
|
|
|
|
|
|
fun parseTime(strTime : String?) : Long {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
if(strTime != null && strTime.isNotEmpty()) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
try {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val m = reTime.matcher(strTime)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
if(! m.find()) {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
log.d("invalid time format: %s", strTime)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} else {
|
|
|
|
|
val g = GregorianCalendar(tz_utc)
|
|
|
|
|
g.set(
|
|
|
|
|
Utils.parse_int(m.group(1), 1),
|
|
|
|
|
Utils.parse_int(m.group(2), 1) - 1,
|
|
|
|
|
Utils.parse_int(m.group(3), 1),
|
|
|
|
|
Utils.parse_int(m.group(4), 0),
|
|
|
|
|
Utils.parse_int(m.group(5), 0),
|
|
|
|
|
Utils.parse_int(m.group(6), 0)
|
|
|
|
|
)
|
|
|
|
|
g.set(Calendar.MILLISECOND, Utils.parse_int(m.group(7), 0))
|
|
|
|
|
return g.timeInMillis
|
|
|
|
|
}
|
|
|
|
|
} catch(ex : Throwable) { // ParseException, ArrayIndexOutOfBoundsException
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
log.e(ex, "TootStatus.parseTime failed. src=%s", strTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return 0L
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun parseTimeMSP(strTime : String?) : Long {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
if(strTime != null && strTime.isNotEmpty()) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
try {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
val m = reMSPTime.matcher(strTime)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
if(! m.find()) {
|
2018-01-10 16:47:35 +01:00
|
|
|
|
log.d("invalid time format: %s", strTime)
|
2018-01-04 19:52:25 +01:00
|
|
|
|
} else {
|
|
|
|
|
val g = GregorianCalendar(tz_utc)
|
|
|
|
|
g.set(
|
|
|
|
|
Utils.parse_int(m.group(1), 1),
|
|
|
|
|
Utils.parse_int(m.group(2), 1) - 1,
|
|
|
|
|
Utils.parse_int(m.group(3), 1),
|
|
|
|
|
Utils.parse_int(m.group(4), 0),
|
|
|
|
|
Utils.parse_int(m.group(5), 0),
|
|
|
|
|
Utils.parse_int(m.group(6), 0)
|
|
|
|
|
)
|
|
|
|
|
g.set(Calendar.MILLISECOND, 500)
|
|
|
|
|
return g.timeInMillis
|
|
|
|
|
}
|
|
|
|
|
} catch(ex : Throwable) { // ParseException, ArrayIndexOutOfBoundsException
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
log.e(ex, "parseTimeMSP failed. src=%s", strTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return 0L
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
|
|
|
|
|
|
|
|
|
fun formatTime(context : Context, t : Long, bAllowRelative : Boolean) : String {
|
2018-01-17 02:16:26 +01:00
|
|
|
|
if(bAllowRelative && Pref.bpRelativeTimestamp(App1.pref)) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
|
var delta = now - t
|
|
|
|
|
val sign = context.getString(if(delta > 0) R.string.ago else R.string.later)
|
|
|
|
|
delta = if(delta >= 0) delta else - delta
|
|
|
|
|
when {
|
|
|
|
|
delta < 1000L -> return context.getString(R.string.time_within_second)
|
|
|
|
|
|
|
|
|
|
delta < 60000L -> {
|
|
|
|
|
val v = (delta / 1000L).toInt()
|
|
|
|
|
return context.getString(if(v > 1) R.string.relative_time_second_2 else R.string.relative_time_second_1, v, sign)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delta < 3600000L -> {
|
|
|
|
|
val v = (delta / 60000L).toInt()
|
|
|
|
|
return context.getString(if(v > 1) R.string.relative_time_minute_2 else R.string.relative_time_minute_1, v, sign)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delta < 86400000L -> {
|
|
|
|
|
val v = (delta / 3600000L).toInt()
|
|
|
|
|
return context.getString(if(v > 1) R.string.relative_time_hour_2 else R.string.relative_time_hour_1, v, sign)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delta < 40 * 86400000L -> {
|
|
|
|
|
val v = (delta / 86400000L).toInt()
|
|
|
|
|
return context.getString(if(v > 1) R.string.relative_time_day_2 else R.string.relative_time_day_1, v, sign)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
date_format.timeZone = TimeZone.getDefault()
|
|
|
|
|
return date_format.format(Date(t))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 公開範囲を比較する
|
|
|
|
|
// 公開範囲が広い => 大きい
|
|
|
|
|
// aの方が小さい(狭い)ならマイナス
|
|
|
|
|
// aの方が大きい(狭い)ならプラス
|
|
|
|
|
// IndexOutOfBoundsException 公開範囲が想定外
|
|
|
|
|
@Suppress("unused")
|
|
|
|
|
fun compareVisibility(a : String, b : String) : Int {
|
|
|
|
|
val ia = parseVisibility(a)
|
|
|
|
|
val ib = parseVisibility(b)
|
|
|
|
|
if(ia < ib) return - 1
|
|
|
|
|
return if(ia > ib) 1 else 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// visibilityが既知の文字列か調べる。ダメなら例外を出す
|
|
|
|
|
fun parseVisibility(a : String?) : Int {
|
|
|
|
|
if(TootStatus.VISIBILITY_DIRECT == a) return 0
|
|
|
|
|
if(TootStatus.VISIBILITY_PRIVATE == a) return 1
|
|
|
|
|
if(TootStatus.VISIBILITY_UNLISTED == a) return 2
|
|
|
|
|
if(TootStatus.VISIBILITY_PUBLIC == a) return 3
|
|
|
|
|
if(TootStatus.VISIBILITY_WEB_SETTING == a) return 4
|
|
|
|
|
throw IndexOutOfBoundsException("visibility not in range")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun isVisibilitySpoilRequired(current_visibility : String?, max_visibility : String?) : Boolean {
|
|
|
|
|
return try {
|
|
|
|
|
val cvi = parseVisibility(current_visibility)
|
|
|
|
|
val mvi = parseVisibility(max_visibility)
|
|
|
|
|
cvi > mvi
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-10 16:47:35 +01:00
|
|
|
|
|
|
|
|
|
private fun validHost(host : String?) : String? {
|
|
|
|
|
return if(host != null && host.isNotEmpty() && host != "?") host else null
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 投稿元タンスでのステータスIDを調べる
|
2018-01-11 10:31:25 +01:00
|
|
|
|
fun findStatusIdFromUri(uri : String?, url : String?) : Long {
|
|
|
|
|
|
|
|
|
|
// pleromaのuriやURL からはステータスIDは取れません
|
|
|
|
|
// uri https://pleroma.miniwa.moe/objects/d6e83d3c-cf9e-46ac-8245-f91716088e17
|
|
|
|
|
// url https://pleroma.miniwa.moe/objects/d6e83d3c-cf9e-46ac-8245-f91716088e17
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
|
|
|
|
try {
|
2018-01-11 10:31:25 +01:00
|
|
|
|
if(uri?.isNotEmpty() == true) {
|
2018-01-04 19:52:25 +01:00
|
|
|
|
// https://friends.nico/users/(who)/statuses/(status_id)
|
|
|
|
|
var m = reTootUriAP1.matcher(uri)
|
|
|
|
|
if(m.find()) return m.group(2).toLong(10)
|
|
|
|
|
|
|
|
|
|
// tag:mstdn.osaka,2017-12-19:objectId=5672321:objectType=Status
|
|
|
|
|
m = reTootUriOS.matcher(uri)
|
|
|
|
|
if(m.find()) return m.group(2).toLong(10)
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
m = reTootUriAP2.matcher(uri)
|
|
|
|
|
if(m.find()) return m.group(2).toLong(10)
|
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
log.e("can't parse status uri: $uri")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
2018-01-11 10:31:25 +01:00
|
|
|
|
|
|
|
|
|
if(url?.isNotEmpty() == true) {
|
|
|
|
|
|
|
|
|
|
// https://friends.nico/users/(who)/statuses/(status_id)
|
|
|
|
|
var m = reTootUriAP1.matcher(url)
|
|
|
|
|
if(m.find()) return m.group(2).toLong(10)
|
|
|
|
|
|
|
|
|
|
// https://friends.nico/@(who)/(status_id)
|
|
|
|
|
m = reTootUriAP2.matcher(url)
|
|
|
|
|
if(m.find()) return m.group(2).toLong(10)
|
|
|
|
|
|
|
|
|
|
|
2018-01-10 16:47:35 +01:00
|
|
|
|
log.e("can't parse status URL: $url")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
2018-01-11 10:31:25 +01:00
|
|
|
|
log.e(ex, "can't parse status number: $uri,$url")
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 10:31:25 +01:00
|
|
|
|
return INVALID_ID
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|