投票結果通知に対応。(サーバ側の問題によりプッシュ通知は届きません)

エクスポートを介してクライアントアプリ登録を端末間で再利用するとアクセストークンを更新しても同じ値が返ってきてプッシュ購読がうまくない問題の対応。
This commit is contained in:
tateisu 2019-03-14 02:34:56 +09:00
parent 2485e53287
commit d9487d0e78
21 changed files with 237 additions and 207 deletions

View File

@ -1224,10 +1224,9 @@ class TestTootApiClient {
callback = callback callback = callback
) )
client.account = accessInfo client.account = accessInfo
val result = client.webSocket("/api/v1/streaming/?stream=public:local", val(_,ws) = client.webSocket("/api/v1/streaming/?stream=public:local",
object : WebSocketListener() { object : WebSocketListener() {
}) })
val ws = result?.data as? WebSocket
assertNotNull(ws) assertNotNull(ws)
ws?.cancel() ws?.cancel()
} }

View File

@ -669,7 +669,10 @@ class ActAccountSetting
TootTaskRunner(this@ActAccountSetting).run(account, object : TootTask { TootTaskRunner(this@ActAccountSetting).run(account, object : TootTask {
override fun background(client : TootApiClient) : TootApiResult? { override fun background(client : TootApiClient) : TootApiResult? {
return client.authentication1(Pref.spClientName(this@ActAccountSetting)) return client.authentication1(
Pref.spClientName(this@ActAccountSetting),
forceUpdateClient = false
)
} }
override fun handleResult(result : TootApiResult?) { override fun handleResult(result : TootApiResult?) {

View File

@ -511,7 +511,7 @@ class ActMain : AppCompatActivity()
?: getAttributeColor(this, R.attr.colorLink) ?: getAttributeColor(this, R.attr.colorLink)
// 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす // 背景画像を表示しない設定が変更された時にカラムの背景を設定しなおす
for( column in app_state.column_list){ for(column in app_state.column_list) {
column.fireColumnColor() column.fireColumnColor()
} }
@ -1439,9 +1439,13 @@ class ActMain : AppCompatActivity()
env.tablet_pager.adapter = env.tablet_pager_adapter env.tablet_pager.adapter = env.tablet_pager_adapter
env.tablet_pager.layoutManager = env.tablet_layout_manager env.tablet_pager.layoutManager = env.tablet_layout_manager
env.tablet_pager.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() { env.tablet_pager.addOnScrollListener(object :
androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView : androidx.recyclerview.widget.RecyclerView, newState : Int) { override fun onScrollStateChanged(
recyclerView : androidx.recyclerview.widget.RecyclerView,
newState : Int
) {
super.onScrollStateChanged(recyclerView, newState) super.onScrollStateChanged(recyclerView, newState)
val vs = env.tablet_layout_manager.findFirstVisibleItemPosition() val vs = env.tablet_layout_manager.findFirstVisibleItemPosition()
@ -1456,7 +1460,11 @@ class ActMain : AppCompatActivity()
} }
} }
override fun onScrolled(recyclerView : androidx.recyclerview.widget.RecyclerView, dx : Int, dy : Int) { override fun onScrolled(
recyclerView : androidx.recyclerview.widget.RecyclerView,
dx : Int,
dy : Int
) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
updateColumnStripSelection(- 1, - 1f) updateColumnStripSelection(- 1, - 1f)
} }
@ -1839,7 +1847,7 @@ class ActMain : AppCompatActivity()
} }
// OAuth2 認証コールバック // OAuth2 認証コールバック
// subwaytooter://oauth/?... // subwaytooter://oauth(\d*)/?...
TootTaskRunner(this@ActMain).run(object : TootTask { TootTaskRunner(this@ActMain).run(object : TootTask {
var ta : TootAccount? = null var ta : TootAccount? = null
@ -1892,7 +1900,7 @@ class ActMain : AppCompatActivity()
// Mastodon 認証コールバック // Mastodon 認証コールバック
// エラー時 // エラー時
// subwaytooter://oauth // subwaytooter://oauth(\d*)/
// ?error=access_denied // ?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 // &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 // &state=db%3A3
@ -1901,7 +1909,7 @@ class ActMain : AppCompatActivity()
return TootApiResult(error) return TootApiResult(error)
} }
// subwaytooter://oauth // subwaytooter://oauth(\d*)/
// ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2 // ?code=113cc036e078ac500d3d0d3ad345cd8181456ab087abc67270d40f40a4e9e3c2
// &state=host%3Amastodon.juggler.jp // &state=host%3Amastodon.juggler.jp
@ -1915,9 +1923,11 @@ class ActMain : AppCompatActivity()
return TootApiResult("missing state in callback url.") return TootApiResult("missing state in callback url.")
} }
if(sv.startsWith("db:")) { for( param in sv.split(",")){
try { when {
val dataId = sv.substring(3).toLong(10)
param.startsWith("db:") -> try {
val dataId = param.substring(3).toLong(10)
val sa = SavedAccount.loadAccount(this@ActMain, dataId) val sa = SavedAccount.loadAccount(this@ActMain, dataId)
?: return TootApiResult("missing account db_id=$dataId") ?: return TootApiResult("missing account db_id=$dataId")
this.sa = sa this.sa = sa
@ -1927,11 +1937,17 @@ class ActMain : AppCompatActivity()
return TootApiResult(ex.withCaption("invalid state")) return TootApiResult(ex.withCaption("invalid state"))
} }
} else if(sv.startsWith("host:")) { param.startsWith("host:") -> {
val host = sv.substring(5) val host = param.substring(5)
client.instance = host client.instance = host
} }
else -> {
// ignore other parameter
}
}
}
val instance = client.instance val instance = client.instance
?: return TootApiResult("missing instance in callback url.") ?: return TootApiResult("missing instance in callback url.")

View File

@ -388,9 +388,11 @@ object AppDataExporter {
writeFromTable(writer, KEY_MUTED_APP, MutedApp.table) writeFromTable(writer, KEY_MUTED_APP, MutedApp.table)
writeFromTable(writer, KEY_MUTED_WORD, MutedWord.table) writeFromTable(writer, KEY_MUTED_WORD, MutedWord.table)
writeFromTable(writer, KEY_FAV_MUTE, FavMute.table) writeFromTable(writer, KEY_FAV_MUTE, FavMute.table)
writeFromTable(writer, KEY_CLIENT_INFO, ClientInfo.table)
writeFromTable(writer, KEY_HIGHLIGHT_WORD, HighlightWord.table) writeFromTable(writer, KEY_HIGHLIGHT_WORD, HighlightWord.table)
// 端末間でクライアントIDを再利用することはできなくなった
//writeFromTable(writer, KEY_CLIENT_INFO, ClientInfo.table)
////////////////////////////////////// //////////////////////////////////////
run { run {
writer.name(KEY_COLUMN) writer.name(KEY_COLUMN)
@ -428,8 +430,12 @@ object AppDataExporter {
KEY_MUTED_WORD -> importTable(reader, MutedWord.table, null) KEY_MUTED_WORD -> importTable(reader, MutedWord.table, null)
KEY_FAV_MUTE -> importTable(reader, FavMute.table, null) KEY_FAV_MUTE -> importTable(reader, FavMute.table, null)
KEY_HIGHLIGHT_WORD -> importTable(reader, HighlightWord.table, null) KEY_HIGHLIGHT_WORD -> importTable(reader, HighlightWord.table, null)
KEY_CLIENT_INFO -> importTable(reader, ClientInfo.table, null)
KEY_COLUMN -> result = readColumn(app_state, reader, account_id_map) KEY_COLUMN -> result = readColumn(app_state, reader, account_id_map)
// 端末間でクライアントIDを再利用することはできなくなった
// KEY_CLIENT_INFO -> importTable(reader, ClientInfo.table, null)
else-> reader.skipValue()
} }
} }

View File

@ -1101,14 +1101,14 @@ class Column(
if(n ++ > 0) sb.append(", ") if(n ++ > 0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_reaction)) sb.append(context.getString(R.string.notification_type_reaction))
} }
if(isMisskey && ! dont_show_vote) { if(! dont_show_vote) {
if(n ++ > 0) sb.append(", ") if(n ++ > 0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_vote)) sb.append(context.getString(R.string.notification_type_vote))
} }
val n_max = if(isMisskey) { val n_max = if(isMisskey) {
6 6
} else { } else {
4 5
} }
if(n == 0 || n == n_max) return "" // 全部か皆無なら部分表記は要らない if(n == 0 || n == n_max) return "" // 全部か皆無なら部分表記は要らない
} }
@ -1638,80 +1638,45 @@ class Column(
private fun isFiltered(item : TootNotification) : Boolean { private fun isFiltered(item : TootNotification) : Boolean {
when(quick_filter) { if(when(quick_filter) {
QUICK_FILTER_ALL -> { QUICK_FILTER_ALL -> when(item.type) {
when(item.type) { TootNotification.TYPE_FAVOURITE -> dont_show_favourite
TootNotification.TYPE_FAVOURITE -> if(dont_show_favourite) {
log.d("isFiltered: favourite notification filtered.")
return true
}
TootNotification.TYPE_REBLOG, TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE, TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE -> if(dont_show_boost) { TootNotification.TYPE_QUOTE -> dont_show_boost
log.d("isFiltered: reblog notification filtered.")
return true
}
TootNotification.TYPE_FOLLOW_REQUEST, TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW -> if(dont_show_follow) { TootNotification.TYPE_FOLLOW -> dont_show_follow
log.d("isFiltered: follow notification filtered.")
return true
}
TootNotification.TYPE_MENTION, TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY -> if(dont_show_reply) { TootNotification.TYPE_REPLY -> dont_show_reply
log.d("isFiltered: mention notification filtered.")
return true TootNotification.TYPE_REACTION -> dont_show_reaction
}
TootNotification.TYPE_REACTION -> if(dont_show_reaction) { TootNotification.TYPE_VOTE,
log.d("isFiltered: reaction notification filtered.") TootNotification.TYPE_POLL -> dont_show_vote
return true else -> false
}
TootNotification.TYPE_VOTE -> if(dont_show_vote) {
log.d("isFiltered: vote notification filtered.")
return true
}
}
} }
else -> { else -> when(item.type) {
when(item.type) { TootNotification.TYPE_FAVOURITE -> quick_filter != QUICK_FILTER_FAVOURITE
TootNotification.TYPE_FAVOURITE -> if(quick_filter != QUICK_FILTER_FAVOURITE) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_REBLOG, TootNotification.TYPE_REBLOG,
TootNotification.TYPE_RENOTE, TootNotification.TYPE_RENOTE,
TootNotification.TYPE_QUOTE -> if(quick_filter != QUICK_FILTER_BOOST) { TootNotification.TYPE_QUOTE -> quick_filter != QUICK_FILTER_BOOST
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_FOLLOW_REQUEST, TootNotification.TYPE_FOLLOW_REQUEST,
TootNotification.TYPE_FOLLOW -> if(quick_filter != QUICK_FILTER_FOLLOW) { TootNotification.TYPE_FOLLOW -> quick_filter != QUICK_FILTER_FOLLOW
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_MENTION, TootNotification.TYPE_MENTION,
TootNotification.TYPE_REPLY -> if(quick_filter != QUICK_FILTER_MENTION) { TootNotification.TYPE_REPLY -> quick_filter != QUICK_FILTER_MENTION
TootNotification.TYPE_REACTION -> quick_filter != QUICK_FILTER_REACTION
TootNotification.TYPE_VOTE,
TootNotification.TYPE_POLL -> quick_filter != QUICK_FILTER_VOTE
else -> true
}
}) {
log.d("isFiltered: ${item.type} notification filtered.") log.d("isFiltered: ${item.type} notification filtered.")
return true return true
} }
TootNotification.TYPE_REACTION -> if(quick_filter != QUICK_FILTER_REACTION) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
TootNotification.TYPE_VOTE -> if(quick_filter != QUICK_FILTER_VOTE) {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
else -> {
log.d("isFiltered: ${item.type} notification filtered.")
return true
}
}
}
}
val status = item.status val status = item.status
@ -6985,6 +6950,7 @@ class Column(
if(dont_show_boost) sb.append("&exclude_types[]=reblog") if(dont_show_boost) sb.append("&exclude_types[]=reblog")
if(dont_show_follow) sb.append("&exclude_types[]=follow") if(dont_show_follow) sb.append("&exclude_types[]=follow")
if(dont_show_reply) sb.append("&exclude_types[]=mention") if(dont_show_reply) sb.append("&exclude_types[]=mention")
if(dont_show_vote) sb.append("&exclude_types[]=poll")
} }
else -> { else -> {

View File

@ -658,7 +658,7 @@ class ColumnViewHolder(
vg(cbDontShowReply, column.canFilterReply()) vg(cbDontShowReply, column.canFilterReply())
vg(cbDontShowNormalToot, column.canFilterNormalToot()) vg(cbDontShowNormalToot, column.canFilterNormalToot())
vg(cbDontShowReaction, isNotificationColumn && column.isMisskey) vg(cbDontShowReaction, isNotificationColumn && column.isMisskey)
vg(cbDontShowVote, isNotificationColumn && column.isMisskey) vg(cbDontShowVote, isNotificationColumn )
vg(cbDontShowFavourite, isNotificationColumn && ! column.isMisskey) vg(cbDontShowFavourite, isNotificationColumn && ! column.isMisskey)
vg(cbDontShowFollow, isNotificationColumn) vg(cbDontShowFollow, isNotificationColumn)
@ -1525,8 +1525,6 @@ class ColumnViewHolder(
if(! isNotificationColumn) return if(! isNotificationColumn) return
vg(btnQuickFilterReaction, column.isMisskey) vg(btnQuickFilterReaction, column.isMisskey)
vg(btnQuickFilterVote, column.isMisskey)
vg(btnQuickFilterFavourite, ! column.isMisskey) vg(btnQuickFilterFavourite, ! column.isMisskey)
val insideColumnSetting = Pref.bpMoveNotificationsQuickFilter(activity.pref) val insideColumnSetting = Pref.bpMoveNotificationsQuickFilter(activity.pref)

View File

@ -930,6 +930,19 @@ internal class ItemViewHolder(
} }
} }
TootNotification.TYPE_POLL -> {
val colorBg = 0
if(n_account != null) showBoost(
n_accountRef,
n.time_created_at,
R.drawable.ic_vote,
R.string.end_of_polling_from
)
if(n_status != null) {
showNotificationStatus(n_status, colorBg)
}
}
else -> { else -> {
val colorBg = 0 val colorBg = 0
if(n_account != null) showBoost( if(n_account != null) showBoost(

View File

@ -1303,6 +1303,9 @@ class PollingWorker private constructor(contextArg : Context) {
name name
) )
TootNotification.TYPE_POLL ->
"- " + context.getString(R.string.end_of_polling_from, name)
else -> "- " + "?" else -> "- " + "?"
} }
} }

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.ClientInfo import jp.juggler.subwaytooter.table.ClientInfo
@ -54,7 +55,9 @@ class TootApiClient(
private const val REDIRECT_URL = "subwaytooter://oauth/" private const val REDIRECT_URL = "subwaytooter://oauth/"
// 20181225 3=>4 client credentialの取得時にもscopeの取得が必要になった // 20181225 3=>4 client credentialの取得時にもscopeの取得が必要になった
private const val AUTH_VERSION = 4 // 20190147 4=>5 client id とユーザIDが同じだと同じアクセストークンが返ってくるので複数端末の利用で困る。
// AUTH_VERSIONが古いclient情報は使わない。また、インポートの対象にしない。
private const val AUTH_VERSION = 5
internal const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential" internal const val KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential"
internal const val KEY_CLIENT_SCOPE = "SubwayTooterClientScope" internal const val KEY_CLIENT_SCOPE = "SubwayTooterClientScope"
@ -216,7 +219,7 @@ class TootApiClient(
) )
// APIのエラーを回避するため、重複を排除する // APIのエラーを回避するため、重複を排除する
.toMutableSet() .toMutableSet()
.forEach {put(it) } .forEach { put(it) }
} }
private fun encodeScopeArray(scope_array : JSONArray?) : String? { private fun encodeScopeArray(scope_array : JSONArray?) : String? {
@ -539,8 +542,8 @@ class TootApiClient(
} }
// インスタンス情報を取得する // インスタンス情報を取得する
internal fun parseInstanceInformation(result : TootApiResult?) : Pair<TootApiResult?,TootInstance?> { internal fun parseInstanceInformation(result : TootApiResult?) : Pair<TootApiResult?, TootInstance?> {
var ti: TootInstance? = null var ti : TootInstance? = null
val json = result?.jsonObject val json = result?.jsonObject
if(json != null) { if(json != null) {
val parser = TootParser( val parser = TootParser(
@ -550,7 +553,7 @@ class TootApiClient(
ti = parser.instance(json) ti = parser.instance(json)
if(ti == null) result.setError("can't parse data in instance information.") if(ti == null) result.setError("can't parse data in instance information.")
} }
return Pair(result,ti) return Pair(result, ti)
} }
private fun getAppInfoMisskey(appId : String?) : TootApiResult? { private fun getAppInfoMisskey(appId : String?) : TootApiResult? {
@ -912,13 +915,19 @@ class TootApiClient(
val account = this.account val account = this.account
val client_id = client_info.parseString("client_id") ?: return null val client_id = client_info.parseString("client_id") ?: return null
val state = StringBuilder()
.append((if(account != null) "db:${account.db_id}" else "host:$instance"))
.append(',')
.append("random:${System.currentTimeMillis()}")
.toString()
return ("https://" + instance + "/oauth/authorize" return ("https://" + instance + "/oauth/authorize"
+ "?client_id=" + client_id.encodePercent() + "?client_id=" + client_id.encodePercent()
+ "&response_type=code" + "&response_type=code"
+ "&redirect_uri=" + REDIRECT_URL.encodePercent() + "&redirect_uri=" + REDIRECT_URL.encodePercent()
+ "&scope=$scope_string" + "&scope=$scope_string"
+ "&scopes=$scope_string" + "&scopes=$scope_string"
+ "&state=" + (if(account != null) "db:${account.db_id}" else "host:$instance") + "&state=" + state.encodePercent()
+ "&grant_type=authorization_code" + "&grant_type=authorization_code"
+ "&approval_prompt=force" + "&approval_prompt=force"
+ "&force_login=true" + "&force_login=true"
@ -926,7 +935,11 @@ class TootApiClient(
) )
} }
private fun prepareClientMastodon(clientNameArg : String, ti : TootInstance) : TootApiResult? { private fun prepareClientMastodon(
clientNameArg : String,
ti : TootInstance,
forceUpdateClient : Boolean = false
) : TootApiResult? {
// 前準備 // 前準備
val result = TootApiResult.makeWithCaption(this.instance) val result = TootApiResult.makeWithCaption(this.instance)
if(result.error != null) return result if(result.error != null) return result
@ -937,18 +950,22 @@ class TootApiClient(
var client_info = ClientInfo.load(instance, client_name) var client_info = ClientInfo.load(instance, client_name)
// スコープ一覧を取得する // スコープ一覧を取得する
val scope_string = getScopeString(ti) val scope_string = getScopeString(ti)
if(client_info != null when {
&& AUTH_VERSION == client_info.optInt(KEY_AUTH_VERSION) AUTH_VERSION != client_info?.optInt(KEY_AUTH_VERSION) -> {
&& ! client_info.optBoolean(KEY_IS_MISSKEY) // 古いクライアント情報は使わない。削除もしない。
) { }
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL) client_info.optBoolean(KEY_IS_MISSKEY) -> {
// Misskeyにはclient情報をまだ利用できるかどうか調べる手段がないので、再利用しない
}
else -> {
val old_scope = client_info.parseString(KEY_CLIENT_SCOPE) val old_scope = client_info.parseString(KEY_CLIENT_SCOPE)
// client_credential をまだ取得していないなら取得する // client_credential をまだ取得していないなら取得する
var client_credential = client_info.parseString(KEY_CLIENT_CREDENTIAL)
if(client_credential?.isEmpty() != false) { if(client_credential?.isEmpty() != false) {
val resultSub = getClientCredential(client_info) val resultSub = getClientCredential(client_info)
client_credential = resultSub?.string client_credential = resultSub?.string
@ -966,11 +983,7 @@ class TootApiClient(
val resultSub = verifyClientCredential(client_credential) val resultSub = verifyClientCredential(client_credential)
val currentCC = resultSub?.jsonObject val currentCC = resultSub?.jsonObject
if(currentCC != null) { if(currentCC != null) {
if(old_scope != scope_string || forceUpdateClient) {
var allowReuseCC = true
if(old_scope != scope_string) {
// マストドン2.4でスコープが追加された // マストドン2.4でスコープが追加された
// 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない // 取得時のスコープ指定がマッチしない(もしくは記録されていない)ならクライアント情報を再利用してはいけない
ClientInfo.delete(instance, client_name) ClientInfo.delete(instance, client_name)
@ -979,11 +992,7 @@ class TootApiClient(
revokeClientCredential(client_info, client_credential) revokeClientCredential(client_info, client_credential)
// XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない // XXX クライアントアプリ情報そのものはまだサーバに残っているが、明示的に消す方法は現状存在しない
} else {
allowReuseCC = false
}
if(allowReuseCC) {
// クライアント情報を再利用する // クライアント情報を再利用する
result.data = client_info result.data = client_info
return result return result
@ -991,6 +1000,7 @@ class TootApiClient(
} }
} }
} }
}
val r2 = registerClient(scope_string, client_name) val r2 = registerClient(scope_string, client_name)
client_info = r2?.jsonObject ?: return r2 client_info = r2?.jsonObject ?: return r2
@ -1021,9 +1031,10 @@ class TootApiClient(
private fun authentication1Mastodon( private fun authentication1Mastodon(
clientNameArg : String, clientNameArg : String,
ti : TootInstance ti : TootInstance,
forceUpdateClient : Boolean = false
) : TootApiResult? = ) : TootApiResult? =
prepareClientMastodon(clientNameArg, ti)?.also { result -> prepareClientMastodon(clientNameArg, ti, forceUpdateClient)?.also { result ->
val client_info = result.jsonObject val client_info = result.jsonObject
if(client_info != null) { if(client_info != null) {
result.data = prepareBrowserUrl(getScopeString(ti), client_info) result.data = prepareBrowserUrl(getScopeString(ti), client_info)
@ -1044,13 +1055,16 @@ class TootApiClient(
} }
// クライアントを登録してブラウザで開くURLを生成する // クライアントを登録してブラウザで開くURLを生成する
fun authentication1(clientNameArg : String) : TootApiResult? { fun authentication1(
clientNameArg : String,
forceUpdateClient : Boolean = false
) : TootApiResult? {
var lastRi : TootApiResult? var lastRi : TootApiResult?
// misskeyのインスタンス情報 // misskeyのインスタンス情報
run{ run {
val (ri ,ti) = parseInstanceInformation(getInstanceInformationMisskey()) val (ri, ti) = parseInstanceInformation(getInstanceInformationMisskey())
lastRi = ri lastRi = ri
if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) { if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
return authentication1Misskey(clientNameArg, ti) return authentication1Misskey(clientNameArg, ti)
@ -1058,11 +1072,11 @@ class TootApiClient(
} }
// マストドンのインスタンス情報 // マストドンのインスタンス情報
run{ run {
val (ri,ti) = parseInstanceInformation(getInstanceInformationMastodon()) val (ri, ti) = parseInstanceInformation(getInstanceInformationMastodon())
lastRi = ri lastRi = ri
if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) { if(ti != null && (ri?.response?.code() ?: 0) in 200 until 300) {
return authentication1Mastodon(clientNameArg, ti) return authentication1Mastodon(clientNameArg, ti, forceUpdateClient)
} }
} }
@ -1360,24 +1374,27 @@ class TootApiClient(
return result return result
} }
fun getHttpBytes(url : String) :Pair<TootApiResult?,ByteArray?> { fun getHttpBytes(url : String) : Pair<TootApiResult?, ByteArray?> {
val result = TootApiResult.makeWithCaption(url) val result = TootApiResult.makeWithCaption(url)
if(result.error != null) return Pair(result,null) if(result.error != null) return Pair(result, null)
if( !sendRequest(result, progressPath = url) { if(! sendRequest(result, progressPath = url) {
Request.Builder().url(url).build() Request.Builder().url(url).build()
}) { }) {
return Pair(result, null) return Pair(result, null)
} }
val r2 = parseBytes(result) val r2 = parseBytes(result)
return Pair(r2,r2?.data as? ByteArray) return Pair(r2, r2?.data as? ByteArray)
} }
fun webSocket(path : String, ws_listener : WebSocketListener) : Pair<TootApiResult?,WebSocket?> { fun webSocket(
var ws:WebSocket? = null path : String,
ws_listener : WebSocketListener
) : Pair<TootApiResult?, WebSocket?> {
var ws : WebSocket? = null
val result = TootApiResult.makeWithCaption(instance) val result = TootApiResult.makeWithCaption(instance)
if(result.error != null) return Pair(result,null) if(result.error != null) return Pair(result, null)
val account = this.account ?: return Pair(TootApiResult("account is null"),null) val account = this.account ?: return Pair(TootApiResult("account is null"), null)
try { try {
var url = "wss://$instance$path" var url = "wss://$instance$path"
@ -1394,13 +1411,14 @@ class TootApiClient(
ws = httpClient.getWebSocket(request, ws_listener) ws = httpClient.getWebSocket(request, ws_listener)
if(isApiCancelled) { if(isApiCancelled) {
ws.cancel() ws.cancel()
return Pair(null,null) return Pair(null, null)
} }
} catch(ex : Throwable) { } catch(ex : Throwable) {
log.trace(ex) log.trace(ex)
result.error = "${result.caption}: ${ex.withCaption(context.resources, R.string.network_error)}" result.error =
"${result.caption}: ${ex.withCaption(context.resources, R.string.network_error)}"
} }
return Pair(result,ws) return Pair(result, ws)
} }
@ -1504,7 +1522,10 @@ fun TootApiClient.syncAccountByAcct(
return Pair(result, ar) return Pair(result, ar)
} }
fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : Pair<TootApiResult?,TootStatus?> { fun TootApiClient.syncStatus(
accessInfo : SavedAccount,
urlArg : String
) : Pair<TootApiResult?, TootStatus?> {
var url = urlArg var url = urlArg
@ -1532,18 +1553,18 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : Pair<
.status(result.jsonObject) .status(result.jsonObject)
?.apply { ?.apply {
if(host.equals(accessInfo.host, ignoreCase = true)) { if(host.equals(accessInfo.host, ignoreCase = true)) {
return Pair(result,this) return Pair(result, this)
} }
uri.letNotEmpty { url = it } uri.letNotEmpty { url = it }
} }
} }
?: return Pair(null,null) // cancelled. ?: return Pair(null, null) // cancelled.
} }
// 使いたいタンス上の投稿IDを取得する // 使いたいタンス上の投稿IDを取得する
val parser = TootParser(context, accessInfo) val parser = TootParser(context, accessInfo)
var targetStatus :TootStatus? = null var targetStatus : TootStatus? = null
val result = if(accessInfo.isMisskey) { val result = if(accessInfo.isMisskey) {
request( request(
"/api/ap/show", "/api/ap/show",
@ -1566,13 +1587,13 @@ fun TootApiClient.syncStatus(accessInfo : SavedAccount, urlArg : String) : Pair<
} }
} }
} }
return Pair(result,targetStatus) return Pair(result, targetStatus)
} }
fun TootApiClient.syncStatus( fun TootApiClient.syncStatus(
accessInfo : SavedAccount, accessInfo : SavedAccount,
statusRemote : TootStatus statusRemote : TootStatus
) : Pair<TootApiResult?,TootStatus?> { ) : Pair<TootApiResult?, TootStatus?> {
// URL->URIの順に試す // URL->URIの順に試す
@ -1600,10 +1621,10 @@ fun TootApiClient.syncStatus(
for(uri in uriList) { for(uri in uriList) {
val pair = syncStatus(accessInfo, uri) val pair = syncStatus(accessInfo, uri)
if( pair.second != null || pair.first == null ) { if(pair.second != null || pair.first == null) {
return pair return pair
} }
} }
return Pair(TootApiResult("can't resolve status URL/URI."),null) return Pair(TootApiResult("can't resolve status URL/URI."), null)
} }

View File

@ -128,9 +128,9 @@ class NicoEnquete(
this.votes_count = src.parseInt("votes_count") this.votes_count = src.parseInt("votes_count")
this.myVoted = if(src.optBoolean("voted", false)) 1 else null this.myVoted = if(src.optBoolean("voted", false)) 1 else null
if(this.items == null) { when {
maxVotesCount = null this.items == null -> maxVotesCount = null
} else if(this.multiple){ this.multiple -> {
var max :Int? = null var max :Int? = null
for( item in items){ for( item in items){
val v = item.votes val v = item.votes
@ -138,7 +138,8 @@ class NicoEnquete(
} }
maxVotesCount = max maxVotesCount = max
} else { }
else -> {
var sum :Int?= null var sum :Int?= null
for( item in items){ for( item in items){
val v = item.votes val v = item.votes
@ -146,6 +147,7 @@ class NicoEnquete(
} }
maxVotesCount = sum maxVotesCount = sum
} }
}
} else { } else {
this.type = src.parseString("type") this.type = src.parseString("type")

View File

@ -30,6 +30,8 @@ class TootNotification(parser : TootParser, src : JSONObject) : TimelineItem() {
const val TYPE_VOTE = "poll_vote" const val TYPE_VOTE = "poll_vote"
const val TYPE_FOLLOW_REQUEST = "receiveFollowRequest" const val TYPE_FOLLOW_REQUEST = "receiveFollowRequest"
// (Mastodon 2.8)投票完了
const val TYPE_POLL = "poll"
} }
val json : JSONObject val json : JSONObject

View File

@ -918,7 +918,8 @@ class SavedAccount(
TootNotification.TYPE_REACTION -> notification_reaction TootNotification.TYPE_REACTION -> notification_reaction
TootNotification.TYPE_VOTE -> notification_vote TootNotification.TYPE_VOTE,TootNotification.TYPE_POLL -> notification_vote
else -> false else -> false
} }

View File

@ -48,7 +48,7 @@ class PushSubscriptionHelper(
if(account.notification_follow) n += 4 if(account.notification_follow) n += 4
if(account.notification_mention) n += 8 if(account.notification_mention) n += 8
if(account.isMisskey && account.notification_reaction) n += 16 if(account.isMisskey && account.notification_reaction) n += 16
if(account.isMisskey && account.notification_vote) n += 32 if(account.notification_vote) n += 32
this.flags = n this.flags = n
} }
@ -365,6 +365,7 @@ class PushSubscriptionHelper(
put("favourite", account.notification_favourite) put("favourite", account.notification_favourite)
put("reblog", account.notification_boost) put("reblog", account.notification_boost)
put("mention", account.notification_mention) put("mention", account.notification_mention)
put("poll", account.notification_vote)
}) })
}) })
} }

View File

@ -610,7 +610,7 @@
<CheckBox <CheckBox
android:id="@+id/cbNotificationVote" android:id="@+id/cbNotificationVote"
style="@style/setting_horizontal_stretch" style="@style/setting_horizontal_stretch"
android:text="@string/vote_misskey" android:text="@string/vote_polls"
/> />
</LinearLayout> </LinearLayout>
<LinearLayout style="@style/setting_row_form"> <LinearLayout style="@style/setting_row_form">

View File

@ -415,7 +415,7 @@
<TextView <TextView
style="@style/setting_row_label_indent1" style="@style/setting_row_label_indent1"
android:text="@string/vote_misskey" android:text="@string/vote_polls"
/> />
<LinearLayout style="@style/setting_row_form"> <LinearLayout style="@style/setting_row_form">

View File

@ -565,7 +565,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="0dp" android:layout_margin="0dp"
android:background="@drawable/btn_bg_transparent" android:background="@drawable/btn_bg_transparent"
android:contentDescription="@string/vote_misskey" android:contentDescription="@string/vote_polls"
/> />
</LinearLayout> </LinearLayout>
</HorizontalScrollView> </HorizontalScrollView>

View File

@ -748,7 +748,6 @@
<string name="follow_request_cancelled">Follow request was cancelled.</string> <string name="follow_request_cancelled">Follow request was cancelled.</string>
<string name="confirm_cancel_follow_request_who_from">La demande de suivi de %2$s à %1$s sera rejetée. Vous êtes sûr ?</string> <string name="confirm_cancel_follow_request_who_from">La demande de suivi de %2$s à %1$s sera rejetée. Vous êtes sûr ?</string>
<string name="reaction">Réaction (Misskey)</string> <string name="reaction">Réaction (Misskey)</string>
<string name="vote_misskey">Vote (Misskey)</string>
<string name="timeout_for_embed_media_viewer">Timeout for embed media viewer (unit:seconds, app restart(delete from app history) required)</string> <string name="timeout_for_embed_media_viewer">Timeout for embed media viewer (unit:seconds, app restart(delete from app history) required)</string>
<string name="media_attachment_max_byte_size_movie">Taille maximale en octets des médias vidéo (unité : méga octets. par défaut : 40)</string> <string name="media_attachment_max_byte_size_movie">Taille maximale en octets des médias vidéo (unité : méga octets. par défaut : 40)</string>
<string name="link_color">Couleur des liens (redémarrage requis)</string> <string name="link_color">Couleur des liens (redémarrage requis)</string>

View File

@ -749,7 +749,7 @@
<string name="visibility_local_followers">フォロワー (ローカル)</string> <string name="visibility_local_followers">フォロワー (ローカル)</string>
<string name="visibility_local_unlisted">未収載 (ローカル)</string> <string name="visibility_local_unlisted">未収載 (ローカル)</string>
<string name="vote_misskey">投票 (Misskey)</string> <string name="vote_polls">投票やその結果</string>
<string name="vote_count_text">%1$d票</string> <string name="vote_count_text">%1$d票</string>
<string name="wait_previous_operation">直前の操作が完了するまでお待ちください</string> <string name="wait_previous_operation">直前の操作が完了するまでお待ちください</string>
<string name="with_attachment">添付データあり</string> <string name="with_attachment">添付データあり</string>
@ -880,9 +880,9 @@
<string name="poll_expire_hours">時間</string> <string name="poll_expire_hours">時間</string>
<string name="poll_expire_minutes"></string> <string name="poll_expire_minutes"></string>
<string name="vote_1">1 vote</string> <string name="vote_1">1 vote</string>
<string name="vote_2">%1$d votes</string> <string name="vote_2">%1$d 人。</string>
<string name="vote_expire_at">投票期限 %1$s</string> <string name="vote_expire_at">投票期限 %1$s</string>
<string name="vote_count_unavailable">\?\?\?票</string> <string name="vote_count_unavailable">\?\?\?票</string>
<string name="vote_button">投票</string> <string name="vote_button">投票</string>
<string name="end_of_polling_from">%1$sの調査の終了</string>
</resources> </resources>

View File

@ -769,7 +769,6 @@
<string name="follow_request_cancelled">팔로우 요청 취소됨.</string> <string name="follow_request_cancelled">팔로우 요청 취소됨.</string>
<string name="confirm_cancel_follow_request_who_from">%2$s로부터 %1$s로의 팔로우 요청을 취소할까요\?</string> <string name="confirm_cancel_follow_request_who_from">%2$s로부터 %1$s로의 팔로우 요청을 취소할까요\?</string>
<string name="reaction">반응 (Misskey)</string> <string name="reaction">반응 (Misskey)</string>
<string name="vote_misskey">투표 (Misskey)</string>
<string name="timeout_for_embed_media_viewer">내장 미디어 뷰어 시간제한 (단위:초, 앱 재시작(앱 사용이력에서 삭제) 필요)</string> <string name="timeout_for_embed_media_viewer">내장 미디어 뷰어 시간제한 (단위:초, 앱 재시작(앱 사용이력에서 삭제) 필요)</string>
<string name="link_color">링크 색 (앱 재시작 필요)</string> <string name="link_color">링크 색 (앱 재시작 필요)</string>
<string name="missing_closeable_column">닫을 칼럼이 표시 범위에 없음.</string> <string name="missing_closeable_column">닫을 칼럼이 표시 범위에 없음.</string>

View File

@ -719,7 +719,6 @@
<string name="follow_request_cancelled">Følgingsforespørsel forkastet.</string> <string name="follow_request_cancelled">Følgingsforespørsel forkastet.</string>
<string name="confirm_cancel_follow_request_who_from">Følgingsforespørsel fra %1$s til %2$s vil forkastes. Er du sikker\?</string> <string name="confirm_cancel_follow_request_who_from">Følgingsforespørsel fra %1$s til %2$s vil forkastes. Er du sikker\?</string>
<string name="reaction">Reaksjon (Misskey)</string> <string name="reaction">Reaksjon (Misskey)</string>
<string name="vote_misskey">Stem (Misskey)</string>
<string name="timeout_for_embed_media_viewer">Tidsavbrudd for innebygd mediaviser (enhet:sekunder, programomstart (sletting fra programhistorikk) kreves)</string> <string name="timeout_for_embed_media_viewer">Tidsavbrudd for innebygd mediaviser (enhet:sekunder, programomstart (sletting fra programhistorikk) kreves)</string>
<string name="link_color">Lenkefarge (programomstart kreves)</string> <string name="link_color">Lenkefarge (programomstart kreves)</string>
<string name="missing_closeable_column">mangler lukkbar kolonne i synlig område.</string> <string name="missing_closeable_column">mangler lukkbar kolonne i synlig område.</string>

View File

@ -756,7 +756,7 @@
<string name="follow_request_cancelled">Follow request cancelled.</string> <string name="follow_request_cancelled">Follow request cancelled.</string>
<string name="confirm_cancel_follow_request_who_from">Cancel follow request from %2$s to %1$s?</string> <string name="confirm_cancel_follow_request_who_from">Cancel follow request from %2$s to %1$s?</string>
<string name="reaction">Reaction (Misskey)</string> <string name="reaction">Reaction (Misskey)</string>
<string name="vote_misskey">Vote (Misskey)</string> <string name="vote_polls">voting or its result</string>
<string name="timeout_for_embed_media_viewer">Timeout for embedded media viewer (Unit:seconds, app restart(delete from app history) required)</string> <string name="timeout_for_embed_media_viewer">Timeout for embedded media viewer (Unit:seconds, app restart(delete from app history) required)</string>
<string name="link_color">Link color (app restart required)</string> <string name="link_color">Link color (app restart required)</string>
<string name="missing_closeable_column">Missing closeable column in visible range.</string> <string name="missing_closeable_column">Missing closeable column in visible range.</string>
@ -875,8 +875,10 @@
<string name="poll_expire_hours">hours</string> <string name="poll_expire_hours">hours</string>
<string name="poll_expire_minutes">minutes</string> <string name="poll_expire_minutes">minutes</string>
<string name="vote_1">1 vote</string> <string name="vote_1">1 vote</string>
<string name="vote_2">%1$d votes</string> <string name="vote_2">%1$d votes.</string>
<string name="vote_expire_at">time limit: %1$s</string> <string name="vote_expire_at">time limit: %1$s</string>
<string name="vote_count_unavailable">\?\?\? votes</string> <string name="vote_count_unavailable">\?\?\? votes</string>
<string name="vote_button">Vote</string> <string name="vote_button">Vote</string>
<string name="end_of_polling_from">End of polling from %1$s</string>
</resources> </resources>