SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamSpec.kt

211 lines
6.7 KiB
Kotlin
Raw Normal View History

2020-12-21 03:13:03 +01:00
package jp.juggler.subwaytooter.streaming
import jp.juggler.subwaytooter.Column
2020-12-21 21:16:33 +01:00
import jp.juggler.subwaytooter.ColumnType
import jp.juggler.subwaytooter.api.entity.TimelineItem
import jp.juggler.subwaytooter.encodeQuery
import jp.juggler.subwaytooter.makeHashtagQueryParams
import jp.juggler.subwaytooter.streaming.StreamSpec.Companion.CHANNEL
import jp.juggler.subwaytooter.streaming.StreamSpec.Companion.PARAMS
import jp.juggler.subwaytooter.streaming.StreamSpec.Companion.STREAM
import jp.juggler.util.*
import java.io.StringWriter
private fun StringWriter.appendValue(v: Any?) {
when (v) {
is JsonArray -> {
append('[')
v.forEachIndexed { i, child ->
if (i > 0) append(',')
appendValue(child)
}
append(']')
}
is JsonObject -> {
append('{')
v.entries.sortedBy { it.key }.forEachIndexed { i, child ->
if (i > 0) append(',')
append(child.key)
append('=')
appendValue(child)
}
append('}')
}
else -> append(v.toString())
}
}
2020-12-21 03:13:03 +01:00
class StreamSpec(
2020-12-21 21:16:33 +01:00
val params: JsonObject,
val path: String,
val name: String,
val streamFilter: Column.(String?,TimelineItem)->Boolean = { _, _ -> true }
2020-12-21 03:13:03 +01:00
) {
2020-12-21 21:16:33 +01:00
companion object {
const val STREAM = "stream"
const val CHANNEL = "channel"
const val PARAMS = "params"
}
val keyString = "$path?${params.toString(indentFactor = 0, sort = true)}"
val channelId = keyString.digestSHA256Base64Url()
override fun hashCode(): Int = keyString.hashCode()
override fun equals(other: Any?): Boolean {
if (other is StreamSpec) return keyString == other.keyString
return false
}
fun match(stream: JsonArray): Boolean {
return true
}
}
private fun Column.streamKeyMastodon(): StreamSpec? {
val root = type.streamKeyMastodon(this) ?: return null
val filter = type.streamFilterMastodon
val path = "/api/v1/streaming/?${root.encodeQuery()}"
val sw = StringWriter()
synchronized(sw.buffer) {
sw.append(root.string(STREAM)!!)
root.entries.sortedBy { it.key }.forEach { pair ->
val (k, v) = pair
if (k != STREAM && v !is JsonArray && v !is JsonObject) {
sw.append(',').append(k).append('=').appendValue(v)
}
}
root.entries.sortedBy { it.key }.forEach { pair ->
val (k, v) = pair
if (v is JsonArray || v is JsonObject) {
sw.append(',').append(k).append('=').appendValue(v)
}
}
}
return StreamSpec(root, path, sw.toString(),streamFilter=filter)
}
fun Column.streamKeyMisskey(): StreamSpec? {
// 使われ方は StreamConnection.subscribe を参照のこと
fun x(channel: String, params: JsonObject = JsonObject()) =
jsonObject(CHANNEL to channel, PARAMS to params)
val misskeyApiToken = access_info.misskeyApiToken
val root = when (misskeyApiToken) {
null -> when (type) {
ColumnType.LOCAL -> x("localTimeline")
else -> null
}
else -> when (type) {
ColumnType.HOME ->
x("homeTimeline")
ColumnType.LOCAL ->
x("localTimeline")
ColumnType.MISSKEY_HYBRID ->
x("hybridTimeline")
ColumnType.FEDERATE ->
x("globalTimeline")
ColumnType.NOTIFICATIONS ->
x("main")
ColumnType.MISSKEY_ANTENNA_TL ->
x("antenna", jsonObject { put("antennaId", profile_id.toString()) })
ColumnType.LIST_TL ->
x("userList", jsonObject { put("listId", profile_id.toString()) })
ColumnType.HASHTAG ->
x("hashtag", jsonObject { put("q", hashtag) })
else -> null
}
} ?: return null
val path = when {
// Misskey 11以降は統合されてる
misskeyVersion >= 11 -> "/streaming"
// Misskey 10 認証なし
// Misskey 8.25 からLTLだけ認証なしでも見れるようになった
access_info.isPseudo -> when (type) {
ColumnType.LOCAL -> "/local-timeline"
else -> null
}
// Misskey 10 認証あり
// Misskey 8.25 からLTLだけ認証なしでも見れるようになった
else -> when (type) {
ColumnType.HOME, ColumnType.NOTIFICATIONS -> "/"
ColumnType.LOCAL -> "/local-timeline"
ColumnType.MISSKEY_HYBRID -> "/hybrid-timeline"
ColumnType.FEDERATE -> "/global-timeline"
ColumnType.LIST_TL -> "/user-list?listId=${profile_id.toString()}"
// タグやアンテナには対応しない
else -> null
}
} ?: return null
val sw = StringWriter()
synchronized(sw.buffer) {
sw.append(root.string(CHANNEL)!!)
val params = root.jsonObject(PARAMS)!!
params.entries.sortedBy { it.key }.forEach { pair ->
val (k, v) = pair
if (v !is JsonArray && v !is JsonObject) {
sw.append(',').append(k).append('=').appendValue(v)
}
}
params.entries.sortedBy { it.key }.forEach { pair ->
val (k, v) = pair
if (v is JsonArray || v is JsonObject) {
sw.append(',').append(k).append('=').appendValue(v)
}
}
}
return StreamSpec(root, path, sw.toString())
}
// 公開ストリームなら真
val Column.isPublicStream: Boolean
get() {
return when (type) {
ColumnType.LOCAL,
ColumnType.FEDERATE,
ColumnType.HASHTAG,
ColumnType.LOCAL_AROUND,
ColumnType.FEDERATED_AROUND,
ColumnType.DOMAIN_TIMELINE -> true
else -> false
}
}
val Column.streamSpec: StreamSpec?
get() = when {
// 疑似アカウントではストリーミングAPIを利用できない
// 2.1 では公開ストリームのみ利用できるらしい
(access_info.isNA || access_info.isPseudo && !isPublicStream) -> null
access_info.isMastodon -> streamKeyMastodon()
access_info.isMisskey -> streamKeyMisskey()
else -> null
}
fun Column.canStreaming() = when {
access_info.isNA -> false
access_info.isPseudo -> isPublicStream && streamSpec != null
else -> streamSpec != null
}
2020-12-21 03:13:03 +01:00
2020-12-21 21:16:33 +01:00
fun Column.canAutoRefresh() = canStreaming()