diff --git a/LICENSE.txt b/LICENSE.txt
index 10699666..6b7f2363 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -186,7 +186,7 @@ Apache License
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2013 Christopher Jenkins
+ Copyright 2017-2018 tateisu
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/app/build.gradle b/app/build.gradle
index 7292d00d..ddfa5131 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -12,8 +12,8 @@ android {
minSdkVersion 21
targetSdkVersion 27
- versionCode 266
- versionName "2.6.6"
+ versionCode 267
+ versionName "2.6.7"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// https://stackoverflow.com/questions/47791227/java-lang-illegalstateexception-dex-archives-setting-dex-extension-only-for
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ce263801..2c2189a3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -77,6 +77,19 @@
/>
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt
index 010fd2db..859b674f 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt
@@ -38,8 +38,7 @@ import android.widget.LinearLayout
import android.widget.TextView
import jp.juggler.subwaytooter.action.*
import jp.juggler.subwaytooter.api.*
-import jp.juggler.subwaytooter.api.entity.EntityId
-import jp.juggler.subwaytooter.api.entity.EntityIdLong
+import jp.juggler.subwaytooter.api.entity.*
import org.apache.commons.io.IOUtils
@@ -52,8 +51,6 @@ import java.util.ArrayList
import java.util.HashSet
import java.util.regex.Pattern
-import jp.juggler.subwaytooter.api.entity.TootAccount
-import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.AccountPicker
import jp.juggler.subwaytooter.dialog.DlgTextInput
import jp.juggler.subwaytooter.table.AcctColor
@@ -1464,15 +1461,13 @@ class ActMain : AppCompatActivity()
// ActOAuthCallbackで受け取ったUriを処理する
private fun handleIntentUri(uri : Uri) {
-
- if("subwaytooter" == uri.scheme) {
- try {
+
+ when(uri.scheme){
+ "subwaytooter","misskeyclientproto" -> return try{
handleOAuth2CallbackUri(uri)
} catch(ex : Throwable) {
log.trace(ex)
}
-
- return
}
val url = uri.toString()
@@ -1627,59 +1622,108 @@ class ActMain : AppCompatActivity()
override fun background(client : TootApiClient) : TootApiResult? {
- // エラー時
- // subwaytooter://oauth
- // ?error=access_denied
- // &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
- // &state=db%3A3
- val error = uri.getQueryParameter("error_description")
- if(error?.isNotEmpty() == true) {
- return TootApiResult(error)
- }
-
- // subwaytooter://oauth
- // ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
- // &state=host%3Amastodon.juggler.jp
-
- val code = uri.getQueryParameter("code")
- if(code?.isEmpty() != false) {
- return TootApiResult("missing code in callback url.")
- }
-
- val sv = uri.getQueryParameter("state")
- if(sv?.isEmpty() != false) {
- return TootApiResult("missing state in callback url.")
- }
-
- if(sv.startsWith("db:")) {
- try {
- val dataId = sv.substring(3).toLong(10)
- val sa = SavedAccount.loadAccount(this@ActMain, dataId)
- ?: return TootApiResult("missing account db_id=$dataId")
- this.sa = sa
- client.account = sa
- } catch(ex : Throwable) {
- log.trace(ex)
- return TootApiResult(ex.withCaption("invalid state"))
+ val uriStr = uri.toString()
+ if( uriStr.startsWith("subwaytooter://misskey/auth_callback")
+ || uriStr.startsWith("misskeyclientproto://misskeyclientproto/auth_callback")
+ ){
+
+ // Misskey 認証コールバック
+ val token = uri.getQueryParameter("token")
+ if(token?.isEmpty() != false) {
+ return TootApiResult("missing token in callback URL")
+ }
+ val prefDevice = PrefDevice.prefDevice(this@ActMain)
+
+ val db_id = prefDevice.getLong(PrefDevice.LAST_AUTH_DB_ID,-1L)
+
+ val instance = prefDevice.getString(PrefDevice.LAST_AUTH_INSTANCE,null)
+ ?: return TootApiResult("missing instance name.")
+
+ if( db_id != -1L ){
+ try {
+ val sa = SavedAccount.loadAccount(this@ActMain, db_id)
+ ?: return TootApiResult("missing account db_id=$db_id")
+ this.sa = sa
+ client.account = sa
+ } catch(ex : Throwable) {
+ log.trace(ex)
+ return TootApiResult(ex.withCaption("invalid state"))
+ }
+ }else{
+ client.instance = instance
}
- } else if(sv.startsWith("host:")) {
- val host = sv.substring(5)
- client.instance = host
+ this.host = instance
+ val client_name = Pref.spClientName(this@ActMain)
+ val result = client.authentication2Misskey(client_name, token)
+ this.ta = TootParser(
+ this@ActMain,
+ object : LinkHelper {
+ override val host : String?
+ get() = instance
+ },
+ serviceType = ServiceType.MISSKEY
+ ).account(result?.jsonObject)
+ return result
+
+ }else{
+ // Mastodon 認証コールバック
+
+ // エラー時
+ // subwaytooter://oauth
+ // ?error=access_denied
+ // &error_description=%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E6%89%80%E6%9C%89%E8%80%85%E3%81%BE%E3%81%9F%E3%81%AF%E8%AA%8D%E8%A8%BC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%8C%E8%A6%81%E6%B1%82%E3%82%92%E6%8B%92%E5%90%A6%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82
+ // &state=db%3A3
+ val error = uri.getQueryParameter("error_description")
+ if(error?.isNotEmpty() == true) {
+ return TootApiResult(error)
+ }
+
+ // subwaytooter://oauth
+ // ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
+ // &state=host%3Amastodon.juggler.jp
+
+ val code = uri.getQueryParameter("code")
+ if(code?.isEmpty() != false) {
+ return TootApiResult("missing code in callback url.")
+ }
+
+ val sv = uri.getQueryParameter("state")
+ if(sv?.isEmpty() != false) {
+ return TootApiResult("missing state in callback url.")
+ }
+
+ if(sv.startsWith("db:")) {
+ try {
+ val dataId = sv.substring(3).toLong(10)
+ val sa = SavedAccount.loadAccount(this@ActMain, dataId)
+ ?: return TootApiResult("missing account db_id=$dataId")
+ this.sa = sa
+ client.account = sa
+ } catch(ex : Throwable) {
+ log.trace(ex)
+ return TootApiResult(ex.withCaption("invalid state"))
+ }
+
+ } else if(sv.startsWith("host:")) {
+ val host = sv.substring(5)
+ client.instance = host
+ }
+
+ val instance = client.instance
+ ?: return TootApiResult("missing instance in callback url.")
+
+ this.host = instance
+ val client_name = Pref.spClientName(this@ActMain)
+ val result = client.authentication2(client_name, code)
+ this.ta = TootParser(this@ActMain, object : LinkHelper {
+ override val host : String?
+ get() = instance
+ })
+ .account(result?.jsonObject)
+ return result
}
- val instance = client.instance
- ?: return TootApiResult("missing instance in callback url.")
-
- this.host = instance
- val client_name = Pref.spClientName(this@ActMain)
- val result = client.authentication2(client_name, code)
- this.ta = TootParser(this@ActMain, object : LinkHelper {
- override val host : String?
- get() = instance
- })
- .account(result?.jsonObject)
- return result
}
override fun handleResult(result : TootApiResult?) {
@@ -1757,7 +1801,13 @@ class ActMain : AppCompatActivity()
// アカウント追加時
val user = ta.username + "@" + host
- val row_id = SavedAccount.insert(host, user, jsonObject, token_info)
+ val row_id = SavedAccount.insert(
+ host,
+ user,
+ jsonObject,
+ token_info,
+ isMisskey = token_info.optBoolean(TootApiClient.KEY_IS_MISSKEY)
+ )
val account = SavedAccount.loadAccount(this@ActMain, row_id)
if(account != null) {
var bModified = false
diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt
index 588996e2..aeba1950 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt
@@ -102,14 +102,17 @@ class App1 : Application() {
// 2018/5/16 v252 25=>26 SubscriptionServerKey テーブルを丸ごと変更
// 2018/8/5 v264 26 => 27 SavedAccountテーブルに項目追加
// 2018/8/17 v267 27 => 28 SavedAccountテーブルに項目追加
- internal const val DB_VERSION = 28
+ // 2018/8/19 v267 28 => 29 ContentWarningMisskey, MediaShownMisskey テーブルを追加
+ internal const val DB_VERSION = 29
private val tableList = arrayOf(
LogData,
SavedAccount,
ClientInfo,
MediaShown,
+ MediaShownMisskey,
ContentWarning,
+ ContentWarningMisskey,
NotificationTracking,
MutedApp,
UserRelation,
diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.kt b/app/src/main/java/jp/juggler/subwaytooter/Column.kt
index e1d98788..91ace5bb 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/Column.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/Column.kt
@@ -4590,6 +4590,8 @@ class Column(
if(access_info.isMisskey) {
if(parser!=null) parser.serviceType = ServiceType.MISSKEY
params.put("limit", 100)
+ val apiKey = access_info.token_info?.parseString(TootApiClient.KEY_API_KEY_MISSKEY)
+ if( apiKey?.isNotEmpty() == true) params.put("i",apiKey)
}
return params
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.kt b/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.kt
index d8ac3476..782c6645 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/PrefDevice.kt
@@ -14,4 +14,7 @@ object PrefDevice {
internal const val KEY_DEVICE_TOKEN = "device_token"
internal const val KEY_INSTALL_ID = "install_id"
+ const val LAST_AUTH_INSTANCE="lastAuthInstance"
+ const val LAST_AUTH_SECRET="lastAuthSecret"
+ const val LAST_AUTH_DB_ID ="lastAuthDbId"
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt
index 8be6ea47..2ad9a561 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.kt
@@ -70,22 +70,25 @@ internal class StatusButtons(
if(access_info.isNicoru(status.account)) R.attr.ic_nicoru else R.attr.btn_favourite
val replies_count = status.replies_count
+
setButton(
btnReply,
true,
color_normal,
R.attr.btn_reply,
- when{
+ when {
replies_count == null || status.visibility == TootStatus.VISIBILITY_DIRECT -> ""
- else->when(Pref.ipRepliesCount(activity.pref)){
- Pref.RC_SIMPLE -> when{
- replies_count >= 1 -> "1+"
- else -> replies_count.toString()
+ else -> when(Pref.ipRepliesCount(activity.pref)) {
+ Pref.RC_SIMPLE -> when {
+ replies_count >= 2L -> "1+"
+ replies_count == 1L -> "1"
+ else -> ""
}
Pref.RC_ACTUAL -> replies_count.toString()
- else->""
+ else -> ""
}
- }
+ },
+ activity.getString(R.string.reply)
)
// ブーストボタン
@@ -95,33 +98,27 @@ internal class StatusButtons(
false,
color_accent,
R.attr.ic_mail,
- ""
+ "",
+ activity.getString(R.string.boost)
)
- // マストドン2.4.0から非公開トゥートもブーストできるようになった
- // TootStatus.VISIBILITY_PRIVATE == status.visibility -> setButton(
- // btnBoost,
- // false,
- // color_accent,
- // R.attr.ic_lock,
- // ""
- // )
+
activity.app_state.isBusyBoost(access_info, status) -> setButton(
btnBoost,
false,
color_normal,
R.attr.btn_refresh,
- "?"
+ "?",
+ activity.getString(R.string.boost)
)
- else -> {
- setButton(
- btnBoost,
- true,
- if(status.reblogged) color_accent else color_normal,
- R.attr.btn_boost,
- status.reblogs_count?.toString() ?: ""
- )
- }
+ else -> setButton(
+ btnBoost,
+ true,
+ if(status.reblogged) color_accent else color_normal,
+ R.attr.btn_boost,
+ status.reblogs_count?.toString() ?: "",
+ activity.getString(R.string.boost)
+ )
}
when {
@@ -130,18 +127,18 @@ internal class StatusButtons(
false,
color_normal,
R.attr.btn_refresh,
- "?"
+ "?",
+ activity.getString(R.string.favourite)
)
- else -> {
- setButton(
- btnFavourite,
- true,
- if(status.favourited) color_accent else color_normal,
- fav_icon_attr,
- status.favourites_count?.toString() ?: ""
- )
- }
+ else -> setButton(
+ btnFavourite,
+ true,
+ if(status.favourited) color_accent else color_normal,
+ fav_icon_attr,
+ status.favourites_count?.toString() ?: "",
+ activity.getString(R.string.favourite)
+ )
}
val account = status.account
@@ -163,12 +160,14 @@ internal class StatusButtons(
enabled : Boolean,
color : Int,
icon_attr : Int,
- text : String
+ text : String,
+ contentDescription : String
) {
val d = Styler.getAttributeDrawable(activity, icon_attr).mutate()
d.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
b.setCompoundDrawablesRelativeWithIntrinsicBounds(d, null, null, null)
b.text = text
+ b.contentDescription = contentDescription + text
b.setTextColor(color)
b.isEnabled = enabled
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/Styler.kt b/app/src/main/java/jp/juggler/subwaytooter/Styler.kt
index aee62be0..962ad900 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/Styler.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/Styler.kt
@@ -157,36 +157,43 @@ object Styler {
// follow button
val color_attr : Int
val icon_attr : Int
+ val contentDescription : String
when {
relation.blocking -> {
icon_attr = R.attr.ic_block
color_attr = R.attr.colorImageButton
+ contentDescription = context.getString(R.string.follow)
}
relation.muting -> {
icon_attr = R.attr.ic_mute
color_attr = R.attr.colorImageButton
+ contentDescription = context.getString(R.string.follow)
}
relation.getFollowing(who) -> {
icon_attr = R.attr.ic_follow_cross
color_attr = R.attr.colorImageButtonAccent
+ contentDescription = context.getString(R.string.unfollow)
}
relation.getRequested(who) -> {
icon_attr = R.attr.ic_follow_wait
color_attr = R.attr.colorRegexFilterError
+ contentDescription = context.getString(R.string.unfollow)
}
else -> {
icon_attr = R.attr.ic_follow_plus
color_attr = R.attr.colorImageButton
+ contentDescription = context.getString(R.string.follow)
}
}
val color = Styler.getAttributeColor(context, color_attr)
setIconCustomColor(context, ibFollow, color, icon_attr)
+ ibFollow.contentDescription = contentDescription
}
// 色を指定してRippleDrawableを生成する
diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt
index e69536e7..47e9de9c 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt
@@ -39,8 +39,7 @@ object Action_Account {
return if(bPseudoAccount || bInputAccessToken) {
client.getInstanceInformation()
} else {
- val client_name = Pref.spClientName(activity)
- client.authentication1(client_name)
+ client.authentication1(Pref.spClientName(activity))
}
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_ListMember.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_ListMember.kt
index 6fe5ab4d..9b252112 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_ListMember.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_ListMember.kt
@@ -46,7 +46,6 @@ object Action_ListMember {
var result : TootApiResult?
- // TODO: リスト追加時に 422 既に登録されてます みたいなエラーが出る
if(bFollow) {
val relation : TootRelationShip?
if(access_info.isLocalUser(local_who)) {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt
index e42135f1..581fae8d 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt
@@ -2,17 +2,15 @@ package jp.juggler.subwaytooter.api
import android.content.Context
import android.content.SharedPreferences
+import jp.juggler.subwaytooter.*
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.R
+import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootInstance
-import jp.juggler.subwaytooter.put
import jp.juggler.subwaytooter.util.*
import okhttp3.*
import org.json.JSONArray
@@ -65,6 +63,19 @@ class TootApiClient(
private const val AUTH_VERSION = 3
private const val REDIRECT_URL = "subwaytooter://oauth/"
+ const val KEY_IS_MISSKEY = "isMisskey"
+ const val KEY_API_KEY_MISSKEY = "apiKeyMisskey"
+
+ // APIからsecretを得られないバグがあるので定数を渡す
+ const val appSecretError =
+ "Currently Misskey does not allow client registration from API, please tell me notify instance name that you want login via Subway Tooter."
+ val testAppSecretMap = mapOf(
+ Pair("misskey.xyz", "NGiWNZFP37WiAee3SGcVe8eSiDyLbbWf")
+ , Pair("misskey.jp", "GO45N7JgeEWtlNUS4xRcOFY56JMjUTZk")
+ , Pair("msky.cafe", "lvU12i7CXAB5xiqkABwzyJRzdAqhf0k3")
+ , Pair("misskey.m544.net", "SLcaqff0Puymh4Fl30JCc09i6uumwJ4t")
+ )
+
private const val NO_INFORMATION = "(no information)"
private val reStartJsonArray = Pattern.compile("\\A\\s*\\[")
@@ -183,6 +194,19 @@ class TootApiClient(
else -> "read+write+follow"
}
+ fun getScopeArrayMisskey(@Suppress("UNUSED_PARAMETER") ti : TootInstance) =
+ JSONArray().apply {
+ put("account-read")
+ put("account-write")
+ put("note-write")
+ put("reaction-write")
+ put("following-write")
+ put("drive-read")
+ put("drive-write")
+ put("notification-read")
+ put("notification-write")
+ }
+
}
@Suppress("unused")
@@ -440,42 +464,331 @@ class TootApiClient(
}
}
+ //////////////////////////////////////////////////////////////////////
+ // misskey authentication
+
// 疑似アカウントの追加時に、インスタンスの検証を行う
- fun getInstanceInformation() : TootApiResult? {
+ private fun getInstanceInformationMisskey() : TootApiResult? {
+ val result = TootApiResult.makeWithCaption(instance)
+ if(result.error != null) return result
+ if(sendRequest(result) {
+ JSONObject().apply {
+ put("dummy", 1)
+ }
+ .toPostRequestBuilder()
+ .url("https://$instance/api/meta")
+ .build()
+ }) {
+ parseJson(result) ?: return null
+ result.jsonObject?.put(KEY_IS_MISSKEY, true)
+ }
+ return result
+ }
+
+ // インスタンス情報を取得する
+ private fun getInstanceInformation2Misskey() : TootApiResult? {
+ val r = getInstanceInformationMisskey()
+ if(r != null) {
+ val json = r.jsonObject
+ if(json != null) {
+ val parser = TootParser(
+ context,
+ object : LinkHelper {
+ override val host : String?
+ get() = instance
+ },
+ serviceType = ServiceType.MISSKEY
+ )
+ val ti = parser.instance(json)
+ if(ti != null) {
+ r.data = ti
+ } else {
+ r.setError("can't parse data in /api/meta")
+ }
+ }
+ }
+ return r
+ }
+
+ private fun getAppInfoMisskey(appId : String?) : TootApiResult? {
+ appId ?: return TootApiResult("missing app id")
+ val result = TootApiResult.makeWithCaption(instance)
+ if(result.error != null) return result
+ if(sendRequest(result) {
+ JSONObject().apply {
+ put("appId", appId)
+ }
+ .toPostRequestBuilder()
+ .url("https://$instance/api/app/show")
+ .build()
+ }) {
+ parseJson(result) ?: return null
+ result.jsonObject?.put(KEY_IS_MISSKEY, true)
+ }
+ return result
+ }
+
+ private fun prepareBrowserUrlMisskey(@Suppress("UNUSED_PARAMETER") clientInfo : JSONObject) : String? {
+
+ val result = TootApiResult.makeWithCaption(instance)
+
+ if(result.error != null) {
+ showToast(context, false, result.error)
+ return null
+ }
+
+ val appSecret = testAppSecretMap[instance?.toLowerCase()]
+ if(appSecret == null) {
+ showToast(context, true, appSecretError)
+ return null
+ }
+
+ if(! sendRequest(result) {
+ JSONObject().apply {
+ put("appSecret", appSecret)
+ }
+ .toPostRequestBuilder()
+ .url("https://$instance/api/auth/session/generate")
+ .build()
+ }
+ ) {
+ val error = result.error
+ if(error != null) {
+ showToast(context, false, error)
+ return null
+ }
+ return null
+ }
+
+ parseJson(result) ?: return null
+
+ val jsonObject = result.jsonObject
+ if(jsonObject == null) {
+ showToast(context, false, result.error)
+ return null
+ }
+ // {"token":"0ba88e2d-4b7d-4599-8d90-dc341a005637","url":"https://misskey.xyz/auth/0ba88e2d-4b7d-4599-8d90-dc341a005637"}
+
+ // ブラウザで開くURL
+ val url = jsonObject.parseString("url")
+ if(url?.isEmpty() != false) {
+ showToast(context, false, "missing 'url' in auth session response.")
+ return null
+ }
+
+ val e = PrefDevice.prefDevice(context)
+ .edit()
+ .putString(PrefDevice.LAST_AUTH_INSTANCE, instance)
+ .putString(PrefDevice.LAST_AUTH_SECRET, appSecret)
+
+ val account = this.account
+ if(account != null) {
+ e.putLong(PrefDevice.LAST_AUTH_DB_ID, account.db_id)
+ } else {
+ e.remove(PrefDevice.LAST_AUTH_DB_ID)
+ }
+
+ e.apply()
+
+ return url
+ }
+
+ private fun registerClientMisskey(
+ scope_array : JSONArray,
+ client_name : String
+ ) : TootApiResult? {
+ val result = TootApiResult.makeWithCaption(instance)
+ if(result.error != null) return result
+ if(sendRequest(result) {
+ JSONObject().apply {
+ put("nameId", "SubwayTooter")
+ put("name", client_name)
+ put("description", "Android app for federated SNS")
+ put("callbackUrl", "subwaytooter://misskey/auth_callback")
+ put("permission", scope_array)
+ }
+ .toPostRequestBuilder()
+ .url("https://$instance/api/app/create")
+ .build()
+ }) {
+ parseJson(result) ?: return null
+ }
+ return result
+ }
+
+ private fun authentication1Misskey(clientNameArg : String, ti : TootInstance) : TootApiResult? {
+ val result = TootApiResult.makeWithCaption(this.instance)
+ if(result.error != null) return result
+ val instance = result.caption // same to instance
+
+ // クライアントIDがアプリ上に保存されているか?
+ val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
+ val client_info = ClientInfo.load(instance, client_name)
+
+ // スコープ一覧を取得する
+ val scope_array = getScopeArrayMisskey(ti)
+
+ if(client_info != null && client_info.optBoolean(KEY_IS_MISSKEY)) {
+ val r2 = getAppInfoMisskey(client_info.parseString("id"))
+ val tmpClientInfo = r2?.jsonObject
+ // tmpClientInfo はsecretを含まないので保存してはいけない
+ if(tmpClientInfo != null // アプリが登録済みで
+ && client_name == tmpClientInfo.parseString("name") // クライアント名が一致してて
+ && tmpClientInfo.optJSONArray("permission")?.length() == scope_array.length() // パーミッションが同じ
+ ) {
+ // クライアント情報を再利用する
+ result.data = prepareBrowserUrlMisskey(client_info)
+ return result
+ } else {
+ // XXX appSecretを使ってクライアント情報を削除できるようにするべきだが、該当するAPIが存在しない
+ }
+ }
+
+ val r2 = registerClientMisskey(scope_array, client_name)
+ val jsonObject = r2?.jsonObject ?: return r2
+
+ // {
+ // "createdAt": "2018-08-19T00:43:10.105Z",
+ // "userId": null,
+ // "name": "Via芸",
+ // "nameId": "test1",
+ // "description": "test1",
+ // "permission": [
+ // "account-read",
+ // "account-write",
+ // "note-write",
+ // "reaction-write",
+ // "following-write",
+ // "drive-read",
+ // "drive-write",
+ // "notification-read",
+ // "notification-write"
+ // ],
+ // "callbackUrl": "test1://test1/auth_callback",
+ // "id": "5b78bd1ea0db0527f25815c3",
+ // "iconUrl": "https://misskey.xyz/files/app-default.jpg"
+ // }
+
+ // 2018/8/19現在、/api/app/create のレスポンスにsecretが含まれないので認証に使えない
+ // https://github.com/syuilo/misskey/issues/2343
+
+ jsonObject.put(KEY_IS_MISSKEY, true)
+ jsonObject.put(KEY_AUTH_VERSION, AUTH_VERSION)
+ ClientInfo.save(instance, client_name, jsonObject.toString())
+ result.data = prepareBrowserUrlMisskey(jsonObject)
+
+ return result
+ }
+
+ // oAuth2認証の続きを行う
+ fun authentication2Misskey(clientNameArg : String, token : String) : TootApiResult? {
+ val result = TootApiResult.makeWithCaption(instance)
+ if(result.error != null) return result
+ val instance = result.caption // same to instance
+ val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
+
+ @Suppress("UNUSED_VARIABLE")
+ val client_info = ClientInfo.load(instance, client_name)
+ ?: return result.setError("missing client id")
+
+ // XXX: client_info中にsecretがあればそれを使う
+ val appSecret = testAppSecretMap[instance.toLowerCase()]
+ ?: return result.setError(appSecretError)
+
+ if(! sendRequest(result) {
+ JSONObject().apply {
+ put("appSecret", appSecret)
+ put("token", token)
+ }
+ .toPostRequestBuilder()
+ .url("https://$instance/api/auth/session/userkey")
+ .build()
+ }
+ ) {
+ return result
+ }
+
+ parseJson(result) ?: return null
+
+ val token_info = result.jsonObject ?: return result
+
+ // {"accessToken":"XSdxQcaCCS5VDRNigfDDj9xNDBpPlD8K","user":{…}}
+
+ val access_token = token_info.parseString("accessToken")
+ if(access_token?.isEmpty() != false) {
+ return result.setError("missing accessToken in the response.")
+ }
+
+ val user = token_info.optJSONObject("user")
+ ?: result.setError("missing user in the response.")
+ token_info.remove("user")
+
+ val apiKey = "$access_token$appSecret".encodeUTF8().digestSHA256().encodeHexLower()
+
+ // ユーザ情報を読めたならtokenInfoを保存する
+ token_info.put(KEY_IS_MISSKEY, true)
+ token_info.put(KEY_AUTH_VERSION, AUTH_VERSION)
+ token_info.put(KEY_API_KEY_MISSKEY, apiKey)
+
+ // tokenInfoとユーザ情報の入ったresultを返す
+ result.tokenInfo = token_info
+ result.data = user
+ return result
+ }
+
+ //////////////////////////////////////////////////////////////////////
+
+ ////////////////////////////////////////////////////////////////
+
+ // 疑似アカウントの追加時に、インスタンスの検証を行う
+ private fun getInstanceInformationMastodon() : TootApiResult? {
val result = TootApiResult.makeWithCaption(instance)
if(result.error != null) return result
if(sendRequest(result) {
Request.Builder().url("https://$instance/api/v1/instance").build()
}
- && 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
- }
+ parseJson(result) ?: return null
}
// misskeyの事は忘れて本来のエラー情報を返す
return result
}
+ private fun getInstanceInformation2Mastodon() : TootApiResult? {
+ val r = getInstanceInformationMastodon()
+ if(r != null) {
+ val json = r.jsonObject
+ if(json != null) {
+ val parser = TootParser(context, object : LinkHelper {
+ override val host : String?
+ get() = instance
+ })
+ val ti = parser.instance(json)
+ if(ti != null) {
+ r.data = ti
+ } else {
+ r.setError("can't parse data in /api/v1/instance")
+ }
+ }
+ }
+ return r
+ }
+
+ // 疑似アカウントの追加時に、インスタンスの検証を行う
+ fun getInstanceInformation() : TootApiResult? {
+ // マストドンのインスタンス情報を読めたら、それはマストドンのインスタンス
+ val r1 = getInstanceInformationMastodon() ?: return null // null means cancelled.
+ if(r1.jsonObject != null) return r1
+
+ // misskeyのインスタンス情報を読めたら、それはmisskeyのインスタンス
+ val r2 = getInstanceInformationMisskey() ?: return null // null means cancelled.
+ if(r2.jsonObject != null) return r2
+
+ return r1 // 通信エラーの表示ならr1でもr2でも構わないはず
+ }
+
// インスタンス情報を取得する
internal fun getInstanceInformation2() : TootApiResult? {
val r = getInstanceInformation()
@@ -498,7 +811,7 @@ class TootApiClient(
}
// クライアントをタンスに登録
- internal fun registerClient(scope_string : String, clientName : String) : TootApiResult? {
+ private fun registerClient(scope_string : String, clientName : String) : TootApiResult? {
val result = TootApiResult.makeWithCaption(this.instance)
if(result.error != null) return result
val instance = result.caption // same to instance
@@ -577,8 +890,8 @@ class TootApiClient(
return parseJson(result)
}
- // // client_credentialを無効にする
- internal fun revokeClientCredential(
+ // client_credentialを無効にする
+ private fun revokeClientCredential(
client_info : JSONObject,
client_credential : String
) : TootApiResult? {
@@ -619,21 +932,19 @@ class TootApiClient(
+ "&redirect_uri=" + REDIRECT_URL.encodePercent()
+ "&scope=$scope_string"
+ "&scopes=$scope_string"
- + "&state=" + (if(account != null) "db:" + account.db_id else "host:" + instance)
+ + "&state=" + (if(account != null) "db:${account.db_id}" else "host:$instance")
+ "&grant_type=authorization_code"
+ "&approval_prompt=force"
// +"&access_type=offline"
)
}
- // クライアントを登録してブラウザで開くURLを生成する
- fun authentication1(clientNameArg : String) : TootApiResult? {
-
- // インスタンス情報の取得
- val ri = getInstanceInformation2()
- val ti = ri?.data as? TootInstance ?: return ri
- val scope_string = getScopeString(ti)
+ private fun authentication1Mastodon(
+ clientNameArg : String,
+ ti : TootInstance
+ ) : TootApiResult? {
+ // 前準備
val result = TootApiResult.makeWithCaption(this.instance)
if(result.error != null) return result
val instance = result.caption // same to instance
@@ -641,7 +952,12 @@ class TootApiClient(
// クライアントIDがアプリ上に保存されているか?
val client_name = if(clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME
val client_info = ClientInfo.load(instance, client_name)
- if(client_info != null) {
+
+ // スコープ一覧を取得する
+
+ val scope_string = getScopeString(ti)
+
+ if(client_info != null && ! client_info.optBoolean(KEY_IS_MISSKEY)) {
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL)
val old_scope = client_info.parseString(KEY_CLIENT_SCOPE)
@@ -672,7 +988,7 @@ class TootApiClient(
// client credential をタンスから消去する
revokeClientCredential(client_info, client_credential)
- // FIXME クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
+ // XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
} else {
// クライアント情報を再利用する
result.data = prepareBrowserUrl(scope_string, client_info)
@@ -694,6 +1010,26 @@ class TootApiClient(
return result
}
+ // クライアントを登録してブラウザで開くURLを生成する
+ fun authentication1(clientNameArg : String) : TootApiResult? {
+
+ // マストドンのインスタンス情報
+ var ri = getInstanceInformation2Mastodon()
+ var ti = ri?.data as? TootInstance
+ if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
+ return authentication1Mastodon(clientNameArg, ti)
+ }
+
+ // misskeyのインスタンス情報
+ ri = getInstanceInformation2Misskey()
+ ti = ri?.data as? TootInstance
+ if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
+ return authentication1Misskey(clientNameArg, ti)
+ }
+
+ return ri
+ }
+
// oAuth2認証の続きを行う
fun authentication2(clientNameArg : String, code : String) : TootApiResult? {
val result = TootApiResult.makeWithCaption(instance)
@@ -819,7 +1155,7 @@ class TootApiClient(
.append("?apikey=").append(mspApiKey.encodePercent())
.append("&utoken=").append(user_token.encodePercent())
.append("&q=").append(query.encodePercent())
- .append("&max=").append(max_id?.encodePercent() ?:"")
+ .append("&max=").append(max_id?.encodePercent() ?: "")
Request.Builder().url(url.toString()).build()
}) return result
@@ -882,14 +1218,14 @@ class TootApiClient(
return result
}
- fun requestJson(req : Request) : TootApiResult? {
- val result = TootApiResult.makeWithCaption(req.url().host())
- if(result.error != null) return result
- if(sendRequest(result, progressPath = null) { req }) {
- parseJson(result)
- }
- return result
- }
+// fun requestJson(req : Request) : TootApiResult? {
+// val result = TootApiResult.makeWithCaption(req.url().host())
+// if(result.error != null) return result
+// if(sendRequest(result, progressPath = null) { req }) {
+// parseJson(result)
+// }
+// return result
+// }
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
fun getHttp(url : String) : TootApiResult? {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt
new file mode 100644
index 00000000..1262c4c6
--- /dev/null
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityId.kt
@@ -0,0 +1,114 @@
+package jp.juggler.subwaytooter.api.entity
+
+import android.content.Intent
+import android.os.Bundle
+import org.json.JSONObject
+
+abstract class EntityId : Comparable {
+
+ abstract fun toLong() : Long
+ abstract fun putMisskeyUntil(dst : JSONObject) : JSONObject
+ abstract fun putMisskeySince(dst : JSONObject) : JSONObject
+
+
+ companion object {
+
+ fun from(x : Long) = EntityIdLong(x)
+
+ fun mayNull(x : Long?) = when(x) {
+ null -> null
+ else -> EntityIdLong(x)
+ }
+
+ fun from(x : String) = EntityIdString(x)
+
+ fun mayNull(x : String?) = when(x) {
+ null -> null
+ else -> EntityIdString(x)
+ }
+
+ fun String.decode():EntityId?{
+ if(this.isEmpty()) return null
+ if(this[0]=='L') return from(this.substring(1).toLong())
+ if(this[0]=='S') return from(this.substring(1))
+ return null
+ }
+
+ fun from(intent: Intent, key:String)=
+ intent.getStringExtra(key)?.decode()
+
+ fun from(bundle: Bundle, key:String)=
+ bundle.getString(key)?.decode()
+
+ fun from(data : JSONObject, key : String): EntityId?{
+ val o = data.opt(key)
+ if(o is Long) return EntityIdLong(o)
+ return (o as? String)?.decode()
+ }
+ }
+
+ private fun encode():String{
+ val prefix = when(this){
+ is EntityIdLong ->'L'
+ is EntityIdString->'S'
+ else -> error("unknown type")
+ }
+ return "$prefix$this"
+ }
+
+ fun putTo(data : Intent, key : String) :Intent = data.putExtra( key,encode())
+
+ fun putTo(bundle:Bundle, key : String) = bundle.putString( key,encode())
+
+ fun putTo(data:JSONObject, key : String):JSONObject = data.put( key,encode())
+
+}
+
+class EntityIdLong(val x : Long) : EntityId() {
+
+ override fun compareTo(other : EntityId) = when(other) {
+ this -> 0
+ is EntityIdLong -> x.compareTo(other.x)
+ else -> error("EntityIdLong: compare with ${other::javaClass.name}")
+ }
+
+ override fun equals(other : Any?) =when(other) {
+ is EntityIdLong -> x == other.x
+ is EntityIdString -> x.toString() == other.x
+ else -> false
+ }
+
+ override fun hashCode() = (x xor x.ushr(32)).toInt()
+
+ override fun toString() = x.toString()
+
+ override fun toLong() = x
+
+ override fun putMisskeyUntil(dst : JSONObject) : JSONObject = dst.put("untilDate", x)
+ override fun putMisskeySince(dst : JSONObject) : JSONObject = dst.put("sinceDate", x)
+
+}
+
+class EntityIdString(val x : String) : EntityId() {
+
+ override fun compareTo(other : EntityId) = when(other) {
+ is EntityIdString -> x.compareTo(other.x)
+ else -> error("EntityIdLong: compare with ${other::javaClass.name}")
+ }
+
+ override fun equals(other : Any?) =when(other) {
+ is EntityIdString -> x == other.x
+ is EntityIdLong -> x == other.x.toString()
+ else -> false
+ }
+
+ override fun hashCode() = x.hashCode()
+
+ override fun toString() = x
+
+ override fun toLong() = TootStatus.INVALID_ID // error("can't convert string ID to long")
+
+ override fun putMisskeyUntil(dst : JSONObject) : JSONObject = dst.put("untilId", x)
+ override fun putMisskeySince(dst : JSONObject) : JSONObject = dst.put("sinceId", x)
+
+}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt
index f8e26b48..073c1b9e 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt
@@ -31,7 +31,8 @@ class TootInstance(parser : TootParser, src : JSONObject) {
// A description for the instance
val description : String?
- // An email address which can be used to contact the instance administrator
+ // An email address which can be used to contact the instance administrator
+ // misskeyの場合はURLらしい
val email : String?
val version : String?
@@ -57,40 +58,59 @@ class TootInstance(parser : TootParser, src : JSONObject) {
enum class InstanceType {
Mastodon,
- Pleroma
+ Pleroma,
+ Misskey
}
- private val instanceType : InstanceType
+ val instanceType : InstanceType
// XXX: urls をパースしてない。使ってないから…
init {
- this.uri = src.parseString("uri")
- this.title = src.parseString("title")
- this.description = src.parseString("description")
- this.email = src.parseString("email")
- this.version = src.parseString("version")
- this.decoded_version = VersionString(version)
- this.stats = parseItem(::Stats, src.optJSONObject("stats"))
- this.thumbnail = src.parseString("thumbnail")
-
- this.max_toot_chars = src.parseInt("max_toot_chars")
-
- this.instanceType = when {
- rePleroma.matcher(version ?: "").find() -> InstanceType.Pleroma
- else -> InstanceType.Mastodon
- }
-
- languages = src.optJSONArray("languages")?.toStringArrayList()
-
- val parser2 = TootParser(
- parser.context,
- object : LinkHelper {
- override val host : String
- get() = uri ?: "?"
+ if(parser.serviceType == ServiceType.MISSKEY){
+
+ this.uri = parser.linkHelper.host
+ this.title = parser.linkHelper.host
+ this.description = "(Misskey instance)"
+ this.email = src.optJSONObject("maintainer")?.parseString("url")
+ this.version = src.parseString("version")
+ this.decoded_version = VersionString(version)
+ this.stats = null
+ this.thumbnail = null
+ this.max_toot_chars = 1000
+ this.instanceType = InstanceType.Misskey
+ this.languages = ArrayList().also{ it.add("?")}
+ this.contact_account = null
+
+ }else {
+ this.uri = src.parseString("uri")
+ this.title = src.parseString("title")
+ this.description = src.parseString("description")
+ this.email = src.parseString("email")
+ this.version = src.parseString("version")
+ this.decoded_version = VersionString(version)
+ this.stats = parseItem(::Stats, src.optJSONObject("stats"))
+ this.thumbnail = src.parseString("thumbnail")
+
+ this.max_toot_chars = src.parseInt("max_toot_chars")
+
+ this.instanceType = when {
+ rePleroma.matcher(version ?: "").find() -> InstanceType.Pleroma
+ else -> InstanceType.Mastodon
}
- )
- contact_account = parseItem(::TootAccount, parser2, src.optJSONObject("contact_account"))
+
+ languages = src.optJSONArray("languages")?.toStringArrayList()
+
+ val parser2 = TootParser(
+ parser.context,
+ object : LinkHelper {
+ override val host : String
+ get() = uri ?: "?"
+ }
+ )
+ contact_account =
+ parseItem(::TootAccount, parser2, src.optJSONObject("contact_account"))
+ }
}
class Stats(src : JSONObject) {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.kt b/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.kt
index 2868b6af..5e0a63c7 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/LoginForm.kt
@@ -100,7 +100,7 @@ object LoginForm {
activity, R.layout.lv_spinner_dropdown, ArrayList()
) {
- internal val nameFilter : Filter = object : Filter() {
+ val nameFilter : Filter = object : Filter() {
override fun convertResultToString(value : Any) : CharSequence {
return value as String
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt b/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt
index 9610002f..083f3b6e 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarning.kt
@@ -4,6 +4,7 @@ import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import jp.juggler.subwaytooter.App1
+import jp.juggler.subwaytooter.api.entity.EntityIdString
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.util.LogCategory
@@ -42,6 +43,9 @@ object ContentWarning :TableCompanion{
}
fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
+
+ if( status.idAccessOrOriginal is EntityIdString) return ContentWarningMisskey.isShown(status,default_value)
+
try {
App1.database.query(
table,
@@ -69,6 +73,10 @@ object ContentWarning :TableCompanion{
}
fun save(status : TootStatus, is_shown : Boolean) {
+
+ if( status.idAccessOrOriginal is EntityIdString) return ContentWarningMisskey.save(status,is_shown)
+
+
try {
val now = System.currentTimeMillis()
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarningMisskey.kt b/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarningMisskey.kt
new file mode 100644
index 00000000..e31f8c1f
--- /dev/null
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/ContentWarningMisskey.kt
@@ -0,0 +1,91 @@
+package jp.juggler.subwaytooter.table
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+
+import jp.juggler.subwaytooter.App1
+import jp.juggler.subwaytooter.api.entity.TootStatus
+import jp.juggler.subwaytooter.util.LogCategory
+
+object ContentWarningMisskey :TableCompanion{
+ private val log = LogCategory("ContentWarning")
+
+ private const val table = "content_warning"
+ private const val COL_HOST = "h"
+ private const val COL_STATUS_ID = "si"
+ private const val COL_SHOWN = "sh"
+ private const val COL_TIME_SAVE = "time_save"
+
+ private val projection_shown = arrayOf(COL_SHOWN)
+
+ override fun onDBCreate(db : SQLiteDatabase) {
+ log.d("onDBCreate!")
+ db.execSQL(
+ "create table if not exists $table"
+ + "(_id INTEGER PRIMARY KEY"
+ + ",$COL_HOST text not null"
+ + ",$COL_STATUS_ID text not null"
+ + ",$COL_SHOWN integer not null"
+ + ",$COL_TIME_SAVE integer default 0"
+ + ")"
+ )
+ db.execSQL(
+ "create unique index if not exists ${table}_status_id on $table($COL_HOST,$COL_STATUS_ID)"
+ )
+ }
+
+ override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) {
+ if(oldVersion < 29 && newVersion >= 29 ) {
+ db.execSQL("drop table if exists $table")
+ onDBCreate(db)
+ }
+ }
+
+ fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
+ try {
+ App1.database.query(
+ table,
+ projection_shown,
+ "h=? and si=?",
+ arrayOf(
+ status.hostAccessOrOriginal,
+ status.idAccessOrOriginal.toString()
+ ),
+ null,
+ null,
+ null
+ ).use { cursor ->
+ if(cursor.moveToFirst()) {
+ val iv = cursor.getInt(cursor.getColumnIndex(COL_SHOWN))
+ return 0 != iv
+ }
+
+ }
+ } catch(ex : Throwable) {
+ log.e(ex, "load failed.")
+ }
+
+ return default_value
+ }
+
+ fun save(status : TootStatus, is_shown : Boolean) {
+ try {
+ val now = System.currentTimeMillis()
+
+ val cv = ContentValues()
+ cv.put(COL_HOST, status.hostAccessOrOriginal)
+ cv.put(COL_STATUS_ID, status.idAccessOrOriginal.toString())
+ cv.put(COL_SHOWN, is_shown.b2i())
+ cv.put(COL_TIME_SAVE, now)
+ App1.database.replace(table, null, cv)
+
+ // 古いデータを掃除する
+ val expire = now - 86400000L * 365
+ App1.database.delete(table, "$COL_TIME_SAVE", arrayOf(expire.toString()))
+
+ } catch(ex : Throwable) {
+ log.e(ex, "save failed.")
+ }
+
+ }
+}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt
index 2fb0648a..874a6e1d 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/MediaShown.kt
@@ -4,6 +4,7 @@ import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import jp.juggler.subwaytooter.App1
+import jp.juggler.subwaytooter.api.entity.EntityIdString
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.util.LogCategory
@@ -42,6 +43,7 @@ object MediaShown:TableCompanion {
}
fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
+ if( status.idAccessOrOriginal is EntityIdString) return MediaShownMisskey.isShown(status,default_value)
try {
App1.database.query(
table,
@@ -68,6 +70,7 @@ object MediaShown:TableCompanion {
}
fun save(status : TootStatus, is_shown : Boolean) {
+ if( status.idAccessOrOriginal is EntityIdString) return MediaShownMisskey.save(status,is_shown)
try {
val now = System.currentTimeMillis()
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MediaShownMisskey.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MediaShownMisskey.kt
new file mode 100644
index 00000000..02d651a9
--- /dev/null
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/MediaShownMisskey.kt
@@ -0,0 +1,90 @@
+package jp.juggler.subwaytooter.table
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+
+import jp.juggler.subwaytooter.App1
+import jp.juggler.subwaytooter.api.entity.TootStatus
+import jp.juggler.subwaytooter.util.LogCategory
+
+object MediaShownMisskey:TableCompanion {
+ private val log = LogCategory("MediaShown")
+
+ private const val table = "media_shown"
+ private const val COL_HOST = "h"
+ private const val COL_STATUS_ID = "si"
+ private const val COL_SHOWN = "sh"
+ private const val COL_TIME_SAVE = "time_save"
+
+ private val projection_shown = arrayOf(COL_SHOWN)
+
+ override fun onDBCreate(db : SQLiteDatabase) {
+ log.d("onDBCreate!")
+ db.execSQL(
+ "create table if not exists $table"
+ + "(_id INTEGER PRIMARY KEY"
+ + ",$COL_HOST text not null"
+ + ",$COL_STATUS_ID text not null"
+ + ",$COL_SHOWN integer not null"
+ + ",$COL_TIME_SAVE integer default 0"
+ + ")"
+ )
+ db.execSQL(
+ "create unique index if not exists ${table}_status_id on $table($COL_HOST,$COL_STATUS_ID)"
+ )
+ }
+
+ override fun onDBUpgrade(db : SQLiteDatabase, oldVersion : Int, newVersion : Int) {
+ if(oldVersion < 29 && newVersion >= 29) {
+ db.execSQL("drop table if exists $table")
+ onDBCreate(db)
+ }
+ }
+
+ fun isShown(status : TootStatus, default_value : Boolean) : Boolean {
+ try {
+ App1.database.query(
+ table,
+ projection_shown,
+ "h=? and si=?",
+ arrayOf(
+ status.hostAccessOrOriginal,
+ status.idAccessOrOriginal.toString()
+ ),
+ null,
+ null,
+ null
+ ).use { cursor ->
+ if(cursor.moveToFirst()) {
+ return 0 != cursor.getInt(cursor.getColumnIndex(COL_SHOWN))
+ }
+
+ }
+ } catch(ex : Throwable) {
+ log.e(ex, "load failed.")
+ }
+
+ return default_value
+ }
+
+ fun save(status : TootStatus, is_shown : Boolean) {
+ try {
+ val now = System.currentTimeMillis()
+
+ val cv = ContentValues()
+ cv.put(COL_HOST, status.hostAccessOrOriginal)
+ cv.put(COL_STATUS_ID, status.idAccessOrOriginal.toString())
+ cv.put(COL_SHOWN, is_shown.b2i())
+ cv.put(COL_TIME_SAVE, now)
+ App1.database.replace(table, null, cv)
+
+ // 古いデータを掃除する
+ val expire = now - 86400000L * 365
+ App1.database.delete(table, "$COL_TIME_SAVE", arrayOf(expire.toString()))
+
+ } catch(ex : Throwable) {
+ log.e(ex, "save failed.")
+ }
+
+ }
+}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt
index f8d7c33f..5d6ed072 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt
@@ -4,12 +4,10 @@ import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Resources
-import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
-import android.support.v7.widget.RecyclerView
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
@@ -32,7 +30,6 @@ import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.lang.ref.WeakReference
-import java.lang.reflect.Field
import java.security.MessageDigest
import java.util.LinkedList
import java.util.Locale
@@ -48,7 +45,7 @@ object Utils {
val log = LogCategory("Utils")
- val hex =
+ val hexLower =
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
/////////////////////////////////////////////
@@ -430,11 +427,22 @@ fun String.encodeUTF8() = this.toByteArray(charsetUTF8)
fun ByteArray.decodeUTF8() = this.toString(charsetUTF8)
fun StringBuilder.appendHex2(value : Int) : StringBuilder {
- this.append(Utils.hex[(value shr 4) and 15])
- this.append(Utils.hex[value and 15])
+ this.append(Utils.hexLower[(value shr 4) and 15])
+ this.append(Utils.hexLower[value and 15])
return this
}
+fun ByteArray.encodeHexLower() : String {
+ val size = this.size
+ val sb = StringBuilder(size *2)
+ for( i in 0 until size){
+ val value = this[i].toInt()
+ sb.append(Utils.hexLower[(value shr 4) and 15])
+ sb.append(Utils.hexLower[value and 15])
+ }
+ return sb.toString()
+}
+
fun String?.optInt() : Int? {
return try {
this?.toInt(10)