mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-01-28 01:29:23 +01:00
Mastodon 2.4 のWebPush REST APIに対応
This commit is contained in:
parent
3f3279243f
commit
74e434a771
@ -12,8 +12,8 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
|
||||
versionCode 246
|
||||
versionName "2.4.6"
|
||||
versionCode 248
|
||||
versionName "2.4.8"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// https://stackoverflow.com/questions/47791227/java-lang-illegalstateexception-dex-archives-setting-dex-extension-only-for
|
||||
|
@ -101,7 +101,7 @@ class ActAccountSetting
|
||||
private lateinit var swNSFWOpen : Switch
|
||||
private lateinit var swDontShowTimeout : Switch
|
||||
private lateinit var btnOpenBrowser : Button
|
||||
private lateinit var btnPushTest : Button
|
||||
private lateinit var btnPushSubscription : Button
|
||||
private lateinit var cbNotificationMention : CheckBox
|
||||
private lateinit var cbNotificationBoost : CheckBox
|
||||
private lateinit var cbNotificationFavourite : CheckBox
|
||||
@ -273,7 +273,7 @@ class ActAccountSetting
|
||||
swNSFWOpen = findViewById(R.id.swNSFWOpen)
|
||||
swDontShowTimeout = findViewById(R.id.swDontShowTimeout)
|
||||
btnOpenBrowser = findViewById(R.id.btnOpenBrowser)
|
||||
btnPushTest= findViewById(R.id.btnPushTest)
|
||||
btnPushSubscription = findViewById(R.id.btnPushSubscription)
|
||||
cbNotificationMention = findViewById(R.id.cbNotificationMention)
|
||||
cbNotificationBoost = findViewById(R.id.cbNotificationBoost)
|
||||
cbNotificationFavourite = findViewById(R.id.cbNotificationFavourite)
|
||||
@ -318,7 +318,7 @@ class ActAccountSetting
|
||||
btnFields = findViewById(R.id.btnFields)
|
||||
|
||||
btnOpenBrowser.setOnClickListener(this)
|
||||
btnPushTest.setOnClickListener(this)
|
||||
btnPushSubscription.setOnClickListener(this)
|
||||
btnAccessToken.setOnClickListener(this)
|
||||
btnInputAccessToken.setOnClickListener(this)
|
||||
btnAccountRemove.setOnClickListener(this)
|
||||
@ -407,6 +407,7 @@ class ActAccountSetting
|
||||
btnAccessToken.isEnabled = enabled
|
||||
btnInputAccessToken.isEnabled = enabled
|
||||
btnVisibility.isEnabled = enabled
|
||||
btnPushSubscription.isEnabled = enabled
|
||||
|
||||
btnNotificationSoundEdit.isEnabled = Build.VERSION.SDK_INT < 26 && enabled
|
||||
btnNotificationSoundReset.isEnabled = Build.VERSION.SDK_INT < 26 && enabled
|
||||
@ -489,8 +490,8 @@ class ActAccountSetting
|
||||
R.id.btnAccountRemove -> performAccountRemove()
|
||||
R.id.btnVisibility -> performVisibility()
|
||||
R.id.btnOpenBrowser -> open_browser("https://" + account.host + "/")
|
||||
R.id.btnPushTest-> startTest()
|
||||
|
||||
R.id.btnPushSubscription -> startTest()
|
||||
|
||||
R.id.btnUserCustom -> ActNickname.open(
|
||||
this,
|
||||
full_acct,
|
||||
@ -843,22 +844,22 @@ class ActAccountSetting
|
||||
else -> fields[i].first
|
||||
}
|
||||
)
|
||||
et.setText( text )
|
||||
et.setText(text)
|
||||
et.isEnabled = true
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler,et)
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler, et)
|
||||
invalidator.register(text)
|
||||
}
|
||||
|
||||
listEtFieldValue.forEachIndexed { i, et ->
|
||||
val text =decodeOptions.decodeEmoji(
|
||||
val text = decodeOptions.decodeEmoji(
|
||||
when {
|
||||
i >= fields.size -> ""
|
||||
else -> fields[i].second
|
||||
}
|
||||
)
|
||||
et.setText( text )
|
||||
et.setText(text)
|
||||
et.isEnabled = true
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler,et)
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler, et)
|
||||
invalidator.register(text)
|
||||
}
|
||||
|
||||
@ -866,7 +867,7 @@ class ActAccountSetting
|
||||
val fields = src.fields
|
||||
|
||||
listEtFieldName.forEachIndexed { i, et ->
|
||||
val text = decodeOptionsNoCustomEmoji.decodeEmoji(
|
||||
val text = decodeOptionsNoCustomEmoji.decodeEmoji(
|
||||
when {
|
||||
fields == null || i >= fields.size -> ""
|
||||
else -> fields[i].first
|
||||
@ -874,12 +875,12 @@ class ActAccountSetting
|
||||
)
|
||||
et.setText(text)
|
||||
et.isEnabled = true
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler,et)
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler, et)
|
||||
invalidator.register(text)
|
||||
}
|
||||
|
||||
listEtFieldValue.forEachIndexed { i, et ->
|
||||
val text = decodeOptions.decodeHTML(
|
||||
val text = decodeOptions.decodeHTML(
|
||||
when {
|
||||
fields == null || i >= fields.size -> ""
|
||||
else -> fields[i].second
|
||||
@ -887,7 +888,7 @@ class ActAccountSetting
|
||||
)
|
||||
et.text = text
|
||||
et.isEnabled = true
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler,et)
|
||||
val invalidator = NetworkEmojiInvalidator(et.handler, et)
|
||||
invalidator.register(text)
|
||||
}
|
||||
}
|
||||
@ -1328,73 +1329,25 @@ class ActAccountSetting
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private fun startTest() {
|
||||
TootTaskRunner(this).run(account,object:TootTask{
|
||||
val sb = StringBuilder()
|
||||
TootTaskRunner(this).run(account, object : TootTask {
|
||||
val wps = WebPushSubscription(this@ActAccountSetting,verbose = true)
|
||||
|
||||
private fun addLog(s:String) {
|
||||
if(sb.isNotEmpty()) sb.append('\n')
|
||||
sb.append(s)
|
||||
override fun background(client : TootApiClient) : TootApiResult? {
|
||||
return wps.updateSubscription(client,account)
|
||||
}
|
||||
|
||||
override fun background(client : TootApiClient) : TootApiResult? { // TODO
|
||||
// インスタンスバージョンの確認
|
||||
var r = client.getInstanceInformation2()
|
||||
val ti = r?.data as? TootInstance ?: return r
|
||||
if(!ti.isEnoughVersion(TootInstance.VERSION_2_4)){
|
||||
addLog("Too old instance version ${ti.version} that does not support Push API.")
|
||||
return r
|
||||
}
|
||||
|
||||
// プッシュ通知の登録
|
||||
var json :JSONObject? = JSONObject().also{
|
||||
it.put("subscription",JSONObject().also {
|
||||
it.put("endpoint","${PollingWorker.APP_SERVER}/webpushcallback")
|
||||
it.put("keys",JSONObject().also {
|
||||
it.put(
|
||||
"p256dh",
|
||||
"BEm_a0bdPDhf0SOsrnB2-ategf1hHoCnpXgQsFj5JCkcoMrMt2WHoPfEYOYPzOIs9mZE8ZUaD7VA5vouy0kEkr8="
|
||||
)
|
||||
it.put("auth", "eH_C8rq2raXqlcBVDa1gLg==")
|
||||
})
|
||||
})
|
||||
it.put("data","<<DATA>>")
|
||||
}
|
||||
var req = Request.Builder().post(
|
||||
RequestBody.create(TootApiClient.MEDIA_TYPE_JSON,json.toString())
|
||||
)
|
||||
r = client.request("/api/v1/push/subscription",req)
|
||||
var response = r?.response
|
||||
if( response != null ){
|
||||
when(response.code()){
|
||||
404 ->{
|
||||
addLog("this instance has no API endpoint 'POST /api/v1/push/subscription'. instance version is ${ti.version}")
|
||||
return r
|
||||
}
|
||||
403 ->{
|
||||
addLog("Your access token does not contains push scope. updating access token is recommended.")
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
addLog( "${response.request()}" )
|
||||
addLog("${response.code()} ${response.message()}")
|
||||
json = r?.jsonObject
|
||||
if(json != null) {
|
||||
addLog(json.toString())
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
override fun handleResult(result : TootApiResult?) {
|
||||
val e = result?.error
|
||||
if(e != null) addLog(e)
|
||||
AlertDialog.Builder(this@ActAccountSetting)
|
||||
.setMessage(sb)
|
||||
.setPositiveButton(R.string.close,null)
|
||||
.show()
|
||||
result ?: return
|
||||
val log = wps.log
|
||||
if( log.isNotEmpty() ){
|
||||
AlertDialog.Builder(this@ActAccountSetting)
|
||||
.setMessage(log)
|
||||
.setPositiveButton(R.string.close, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -23,9 +23,9 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
|
||||
if(data != null) {
|
||||
for((key, value) in data) {
|
||||
log.d("onMessageReceived: %s=%s", key, value)
|
||||
|
||||
if("notification_tag" == key) {
|
||||
tag = value
|
||||
when(key){
|
||||
"notification_tag" -> tag = value
|
||||
"acct" -> tag= "acct<>$value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,92 @@ class PollingWorker private constructor(c : Context) {
|
||||
return s
|
||||
}
|
||||
|
||||
fun getDeviceId(context : Context) : String? {
|
||||
val prefDevice = PrefDevice.prefDevice(context)
|
||||
var device_token = prefDevice.getString(PrefDevice.KEY_DEVICE_TOKEN, null)
|
||||
if(device_token?.isNotEmpty() == true) return device_token
|
||||
|
||||
try {
|
||||
device_token = FirebaseInstanceId.getInstance().token
|
||||
if(device_token?.isNotEmpty() == true) {
|
||||
prefDevice.edit().putString(PrefDevice.KEY_DEVICE_TOKEN, device_token)
|
||||
.apply()
|
||||
return device_token
|
||||
}
|
||||
log.e("getDeviceId: missing device token.")
|
||||
return null
|
||||
} catch(ex : Throwable) {
|
||||
log.e("getDeviceId: could not get device token.")
|
||||
log.trace(ex)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// インストールIDを生成する前に、各データの通知登録キャッシュをクリアする
|
||||
// トークンがまだ生成されていない場合、このメソッドは null を返します。
|
||||
fun prepareInstallId(
|
||||
context : Context,
|
||||
job : JobItem? = null
|
||||
) : String? {
|
||||
val prefDevice = PrefDevice.prefDevice(context)
|
||||
|
||||
var sv = prefDevice.getString(PrefDevice.KEY_INSTALL_ID, null)
|
||||
if(sv?.isNotEmpty() == true) return sv
|
||||
SavedAccount.clearRegistrationCache()
|
||||
|
||||
try {
|
||||
var device_token = prefDevice.getString(PrefDevice.KEY_DEVICE_TOKEN, null)
|
||||
if(device_token == null || device_token.isEmpty()) {
|
||||
try {
|
||||
device_token = FirebaseInstanceId.getInstance().token
|
||||
if(device_token == null || device_token.isEmpty()) {
|
||||
log.e("getInstallId: missing device token.")
|
||||
return null
|
||||
} else {
|
||||
prefDevice.edit().putString(PrefDevice.KEY_DEVICE_TOKEN, device_token)
|
||||
.apply()
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.e("getInstallId: could not get device token.")
|
||||
log.trace(ex)
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$APP_SERVER/counter")
|
||||
.build()
|
||||
|
||||
val call = App1.ok_http_client.newCall(request)
|
||||
if(job != null) {
|
||||
job.current_call = call
|
||||
}
|
||||
|
||||
val response = call.execute()
|
||||
val body = response.body()?.string()
|
||||
|
||||
if(! response.isSuccessful || body?.isEmpty() != false) {
|
||||
log.e(
|
||||
TootApiClient.formatResponse(
|
||||
response,
|
||||
"getInstallId: get /counter failed."
|
||||
)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
sv = (device_token + UUID.randomUUID() + body).digestSHA256()
|
||||
prefDevice.edit().putString(PrefDevice.KEY_INSTALL_ID, sv).apply()
|
||||
|
||||
return sv
|
||||
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// タスクの管理
|
||||
|
||||
@ -364,7 +450,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
worker.start()
|
||||
}
|
||||
|
||||
internal inner class Worker : WorkerBase() {
|
||||
inner class Worker : WorkerBase() {
|
||||
|
||||
val bThreadCancelled = AtomicBoolean(false)
|
||||
|
||||
@ -551,7 +637,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
|
||||
internal class JobCancelledException : RuntimeException("job is cancelled.")
|
||||
|
||||
internal inner class JobItem {
|
||||
inner class JobItem {
|
||||
val jobId : Int
|
||||
private val refJobService : WeakReference<JobService>?
|
||||
private val jobParams : JobParameters?
|
||||
@ -781,10 +867,21 @@ class PollingWorker private constructor(c : Context) {
|
||||
var bDone = false
|
||||
val tag = taskData.parseString(EXTRA_TAG)
|
||||
if(tag != null) {
|
||||
for(sa in SavedAccount.loadByTag(context, tag)) {
|
||||
NotificationTracking.resetLastLoad(sa.db_id)
|
||||
process_db_id = sa.db_id
|
||||
bDone = true
|
||||
if(tag.startsWith("acct<>")) {
|
||||
val acct = tag.substring(6)
|
||||
val sa = SavedAccount.loadAccountByAcct(context, acct)
|
||||
if(sa != null) {
|
||||
NotificationTracking.resetLastLoad(sa.db_id)
|
||||
process_db_id = sa.db_id
|
||||
bDone = true
|
||||
}
|
||||
}
|
||||
if(! bDone) {
|
||||
for(sa in SavedAccount.loadByTag(context, tag)) {
|
||||
NotificationTracking.resetLastLoad(sa.db_id)
|
||||
process_db_id = sa.db_id
|
||||
bDone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if(! bDone) {
|
||||
@ -833,7 +930,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
// インストールID生成時にSavedAccountテーブルを操作することがあるので
|
||||
// アカウントリストの取得より先に行う
|
||||
if(job.install_id == null) {
|
||||
job.install_id = prepareInstallId()
|
||||
job.install_id = prepareInstallId(context, job)
|
||||
}
|
||||
|
||||
// アカウント別に処理スレッドを作る
|
||||
@ -875,66 +972,6 @@ class PollingWorker private constructor(c : Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// インストールIDを生成する前に、各データの通知登録キャッシュをクリアする
|
||||
// トークンがまだ生成されていない場合、このメソッドは null を返します。
|
||||
private fun prepareInstallId() : String? {
|
||||
val prefDevice = PrefDevice.prefDevice(context)
|
||||
|
||||
var sv = prefDevice.getString(PrefDevice.KEY_INSTALL_ID, null)
|
||||
if(sv?.isNotEmpty() == true) return sv
|
||||
SavedAccount.clearRegistrationCache()
|
||||
|
||||
try {
|
||||
var device_token = prefDevice.getString(PrefDevice.KEY_DEVICE_TOKEN, null)
|
||||
if(device_token == null || device_token.isEmpty()) {
|
||||
try {
|
||||
device_token = FirebaseInstanceId.getInstance().token
|
||||
if(device_token == null || device_token.isEmpty()) {
|
||||
log.e("getInstallId: missing device token.")
|
||||
return null
|
||||
} else {
|
||||
prefDevice.edit().putString(PrefDevice.KEY_DEVICE_TOKEN, device_token)
|
||||
.apply()
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.e("getInstallId: could not get device token.")
|
||||
log.trace(ex)
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$APP_SERVER/counter")
|
||||
.build()
|
||||
|
||||
val call = App1.ok_http_client.newCall(request)
|
||||
job.current_call = call
|
||||
|
||||
val response = call.execute()
|
||||
val body = response.body()?.string()
|
||||
|
||||
if(! response.isSuccessful || body?.isEmpty() != false) {
|
||||
log.e(
|
||||
TootApiClient.formatResponse(
|
||||
response,
|
||||
"getInstallId: get /counter failed."
|
||||
)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
sv = (device_token + UUID.randomUUID() + body).digestSHA256()
|
||||
prefDevice.edit().putString(PrefDevice.KEY_INSTALL_ID, sv).apply()
|
||||
|
||||
return sv
|
||||
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun createErrorNotification(error_instance : ArrayList<String>) {
|
||||
if(error_instance.isEmpty()) {
|
||||
return
|
||||
@ -1014,8 +1051,9 @@ class PollingWorker private constructor(c : Context) {
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AccountThread(val account : SavedAccount) : Thread(),
|
||||
CurrentCallCallback {
|
||||
internal inner class AccountThread(
|
||||
val account : SavedAccount
|
||||
) : Thread(), CurrentCallCallback {
|
||||
|
||||
private var current_call : Call? = null
|
||||
|
||||
@ -1057,19 +1095,26 @@ class PollingWorker private constructor(c : Context) {
|
||||
// 疑似アカウントはチェック対象外
|
||||
if(account.isPseudo) return
|
||||
|
||||
// アカウントの通知設定が全てオフ
|
||||
if(! account.notification_mention
|
||||
&& ! account.notification_boost
|
||||
&& ! account.notification_favourite
|
||||
&& ! account.notification_follow
|
||||
) {
|
||||
unregisterDeviceToken()
|
||||
return
|
||||
client.account = account
|
||||
|
||||
val wps = WebPushSubscription(context)
|
||||
wps.updateSubscription(client, account)
|
||||
val wps_log = wps.log
|
||||
if(wps_log.isNotEmpty()) {
|
||||
log.d("WebPushSubscription: ${account.acct} $wps_log")
|
||||
}
|
||||
|
||||
if(job.isJobCancelled) return
|
||||
|
||||
registerDeviceToken()
|
||||
if(wps.flags == 0) {
|
||||
unregisterDeviceToken()
|
||||
return
|
||||
}
|
||||
|
||||
if(! wps.subscribed) {
|
||||
registerDeviceToken()
|
||||
}
|
||||
|
||||
|
||||
if(job.isJobCancelled) return
|
||||
|
||||
@ -1278,7 +1323,7 @@ class PollingWorker private constructor(c : Context) {
|
||||
if(now - nr.last_load >= 60000L * 2) {
|
||||
nr.last_load = now
|
||||
|
||||
client.account = account
|
||||
|
||||
|
||||
for(nTry in 0 .. 3) {
|
||||
if(job.isJobCancelled) return
|
||||
|
@ -845,16 +845,31 @@ class TootApiClient(
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// JSONデータ以外を扱うリクエスト
|
||||
|
||||
// 疑似アカウントでステータスURLからステータスIDを取得するためにHTMLを取得する
|
||||
fun getHttp(url : String) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(url)
|
||||
fun http(req:Request) : TootApiResult? {
|
||||
val result = TootApiResult.makeWithCaption(req.url().host())
|
||||
if(result.error != null) return result
|
||||
|
||||
if(! sendRequest(result, progressPath = url) {
|
||||
Request.Builder().url(url).build()
|
||||
}) return result
|
||||
return parseString(result)
|
||||
|
||||
sendRequest(result, progressPath = null) { req }
|
||||
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? {
|
||||
val result = http(Request.Builder().url(url).build())
|
||||
if(result !=null && result.error == null){
|
||||
parseString(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getHttpBytes(url : String) : TootApiResult? {
|
||||
|
@ -11,6 +11,7 @@ import android.os.Looper
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.Base64
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -25,15 +26,15 @@ import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.security.MessageDigest
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Locale
|
||||
import java.util.LinkedList
|
||||
|
||||
|
||||
object Utils {
|
||||
|
||||
@ -337,25 +338,25 @@ private fun ByteArray.encodeHex() : String {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
//private fun ByteArray.encodeSHA256() : ByteArray? {
|
||||
// return try {
|
||||
// val digest = MessageDigest.getInstance("SHA-256")
|
||||
// digest.reset()
|
||||
// digest.digest(this)
|
||||
// } catch(e1 : NoSuchAlgorithmException) {
|
||||
// null
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//private fun ByteArray?.encodeBase64Safe() : String? {
|
||||
// this ?: return null
|
||||
// return try {
|
||||
// Base64.encodeToString(this, Base64.URL_SAFE)
|
||||
// } catch(ex : Throwable) {
|
||||
// null
|
||||
// }
|
||||
//}
|
||||
fun ByteArray.digestSHA256() : ByteArray? {
|
||||
return try {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.reset()
|
||||
digest.digest(this)
|
||||
} catch(e1 : NoSuchAlgorithmException) {
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun ByteArray?.encodeBase64Safe() : String? {
|
||||
this ?: return null
|
||||
return try {
|
||||
Base64 .encodeToString(this, Base64.URL_SAFE)
|
||||
} catch(ex : Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// CharSequence
|
||||
|
@ -0,0 +1,204 @@
|
||||
package jp.juggler.subwaytooter.util
|
||||
|
||||
import android.content.Context
|
||||
import jp.juggler.subwaytooter.PollingWorker
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.entity.TootInstance
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import org.json.JSONObject
|
||||
|
||||
class WebPushSubscription(val context : Context,val verbose: Boolean = false) {
|
||||
|
||||
var enabled : Boolean = false
|
||||
var subscribed : Boolean = false
|
||||
var flags = 0
|
||||
|
||||
val log : String
|
||||
get() = sb.toString()
|
||||
|
||||
private val sb = StringBuilder()
|
||||
|
||||
private fun addLog(s : String) {
|
||||
if(sb.isNotEmpty()) sb.append('\n')
|
||||
sb.append(s)
|
||||
}
|
||||
|
||||
private fun updateSubscription_sub(client : TootApiClient, account : SavedAccount) : TootApiResult? {
|
||||
try {
|
||||
|
||||
if(account.notification_boost) flags += 1
|
||||
if(account.notification_favourite) flags += 2
|
||||
if(account.notification_follow) flags += 4
|
||||
if(account.notification_mention) flags += 8
|
||||
|
||||
// 疑似アカウントの確認
|
||||
if(account.isPseudo) {
|
||||
return TootApiResult(error = context.getString(R.string.pseudo_account_not_supported))
|
||||
}
|
||||
|
||||
// インスタンスバージョンの確認
|
||||
var r = client.getInstanceInformation2()
|
||||
val ti = r?.data as? TootInstance ?: return r
|
||||
if(! ti.isEnoughVersion(TootInstance.VERSION_2_4)) {
|
||||
return TootApiResult(error = context.getString(R.string.instance_does_not_support_push_api,ti.version))
|
||||
}
|
||||
|
||||
// FCMのデバイスIDを取得
|
||||
val device_id = PollingWorker.getDeviceId(context)
|
||||
?: return TootApiResult(error = context.getString(R.string.missing_fcm_device_id))
|
||||
|
||||
// インストールIDを取得
|
||||
val install_id = PollingWorker.prepareInstallId(context)
|
||||
?: return TootApiResult(error = context.getString(R.string.missing_install_id))
|
||||
|
||||
// アクセストークンの優先権を取得
|
||||
r = client.http(
|
||||
Request.Builder()
|
||||
.url("${PollingWorker.APP_SERVER}/webpushtokencheck")
|
||||
.post(
|
||||
RequestBody.create(
|
||||
TootApiClient.MEDIA_TYPE_JSON,
|
||||
JSONObject().also {
|
||||
it.put("token_digest", account.getAccessToken()?.digestSHA256())
|
||||
it.put("install_id", install_id)
|
||||
}.toString()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
var res = r?.response ?: return r
|
||||
|
||||
if(res.code() != 200) {
|
||||
return TootApiResult(error = context.getString(R.string.token_exported))
|
||||
}
|
||||
|
||||
enabled = true
|
||||
|
||||
|
||||
// TODO 現在の購読状態を取得できれば良いのに…
|
||||
|
||||
if(flags == 0) {
|
||||
// delete subscription
|
||||
r = client.request(
|
||||
"/api/v1/push/subscription",
|
||||
Request.Builder().delete()
|
||||
)
|
||||
|
||||
res = r?.response ?: return r
|
||||
|
||||
when(res.code()) {
|
||||
200 -> {
|
||||
addLog(context.getString(R.string.push_subscription_deleted))
|
||||
return r
|
||||
}
|
||||
|
||||
404 -> {
|
||||
enabled = false
|
||||
return if(verbose){
|
||||
addLog(context.getString(R.string.missing_push_api))
|
||||
r
|
||||
}else{
|
||||
// バックグラウンド実行時は別にコレでも構わないので正常終了扱いとする
|
||||
TootApiResult()
|
||||
}
|
||||
}
|
||||
|
||||
403 -> {
|
||||
enabled = false
|
||||
return if(verbose){
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
r
|
||||
}else{
|
||||
// バックグラウンド実行時は別にコレでも構わないので正常終了扱いとする
|
||||
TootApiResult()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
addLog("${res.request()}")
|
||||
addLog("${res.code()} ${res.message()}")
|
||||
val json = r?.jsonObject
|
||||
if(json != null) {
|
||||
addLog(json.toString())
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// FCM経由での配信に必要なパラメータ
|
||||
val endpoint =
|
||||
"${PollingWorker.APP_SERVER}/webpushcallback/${device_id.encodePercent()}/${account.acct.encodePercent()}/$flags"
|
||||
|
||||
// プッシュ通知の登録
|
||||
var json : JSONObject? = JSONObject().also {
|
||||
it.put("subscription", JSONObject().also {
|
||||
it.put("endpoint", endpoint )
|
||||
it.put("keys", JSONObject().also {
|
||||
it.put(
|
||||
"p256dh",
|
||||
"BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE"
|
||||
)
|
||||
it.put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
|
||||
})
|
||||
})
|
||||
it.put("data", JSONObject().also {
|
||||
it.put("alerts", JSONObject().also {
|
||||
it.put("follow", account.notification_follow)
|
||||
it.put("favourite", account.notification_favourite)
|
||||
it.put("reblog", account.notification_boost)
|
||||
it.put("mention", account.notification_mention)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
r = client.request(
|
||||
"/api/v1/push/subscription",
|
||||
Request.Builder().post(
|
||||
RequestBody.create(
|
||||
TootApiClient.MEDIA_TYPE_JSON,
|
||||
json.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
res = r?.response ?: return r
|
||||
|
||||
when(res.code()) {
|
||||
404 -> {
|
||||
addLog(context.getString(R.string.missing_push_api))
|
||||
return r
|
||||
}
|
||||
|
||||
403 -> {
|
||||
addLog(context.getString(R.string.missing_push_scope))
|
||||
return r
|
||||
}
|
||||
|
||||
200 -> {
|
||||
subscribed = true
|
||||
addLog(context.getString(R.string.push_subscription_updated))
|
||||
return r
|
||||
}
|
||||
}
|
||||
json = r?.jsonObject
|
||||
if(json != null) {
|
||||
addLog(json.toString())
|
||||
}
|
||||
|
||||
return r
|
||||
} catch(ex : Throwable) {
|
||||
return TootApiResult(error = ex.withCaption("error."))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscription(client : TootApiClient, account : SavedAccount) : TootApiResult? {
|
||||
val result = updateSubscription_sub(client, account)
|
||||
val e = result?.error
|
||||
if(e != null) addLog(e)
|
||||
return result
|
||||
}
|
||||
}
|
@ -359,17 +359,7 @@
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPushTest"
|
||||
style="@style/setting_horizontal_stretch"
|
||||
android:ellipsize="start"
|
||||
android:text="@string/push_notification_test"
|
||||
android:textAllCaps="false"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
<Button
|
||||
@ -582,6 +572,18 @@
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPushSubscription"
|
||||
style="@style/setting_horizontal_stretch"
|
||||
android:ellipsize="start"
|
||||
android:text="@string/update_push_subscription"
|
||||
android:textAllCaps="false"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View style="@style/setting_divider"/>
|
||||
|
||||
<TextView
|
||||
|
@ -659,9 +659,18 @@
|
||||
<string name="available_mastodon_2_4_later">(available in Mastodon 2.4 or later)</string>
|
||||
<string name="confirm_boost_private_from">Boost private status from %1$s ? It\'s shown to all followers.</string>
|
||||
<string name="boost_private_toot_not_allowed">You can\'t boost private toot by another person.</string>
|
||||
<string name="push_notification_test">Push Notification Test (Mastodon 2.4 or later)</string>
|
||||
<string name="update_push_subscription">Update push subscription (Mastodon 2.4 or later)</string>
|
||||
<string name="pseudo_account_not_supported">Pseudo account can\'t use notifications.</string>
|
||||
<string name="instance_does_not_support_push_api">Instance version %1$s too old. that does not support Push API.</string>
|
||||
<string name="missing_fcm_device_id">Can\'t prepare FCM device ID. please retry later.</string>
|
||||
<string name="missing_install_id">Can\'t prepare install ID. please retry later.</string>
|
||||
<string name="token_exported">Due to exporting application data or restoring from backup, this access token may be used by other devices as well. Because access tokens that are not used by other devices are required to use push notifications, we recommend updating the access token.</string>
|
||||
<string name="push_subscription_deleted">Push subscription deleted.</string>
|
||||
<string name="missing_push_api">The instance has no Push API endpoint.</string>
|
||||
<string name="missing_push_scope">Your access token does not contains push scope. updating access token is recommended.</string>
|
||||
<string name="push_subscription_updated">Push subscription updated.</string>
|
||||
|
||||
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
|
||||
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
|
||||
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->
|
||||
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
|
||||
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->
|
||||
|
@ -938,6 +938,15 @@
|
||||
<string name="available_mastodon_2_4_later">(マストドン2.4以降で利用可能)</string>
|
||||
<string name="confirm_boost_private_from">非公開トゥートを %1$s からブーストしますか? 全てのフォロワーに公開されます</string>
|
||||
<string name="boost_private_toot_not_allowed">非公開トゥートをブーストできるのは本人だけです</string>
|
||||
<string name="push_notification_test">Push Notification Test (Mastodon 2.4 or later)</string>
|
||||
<string name="update_push_subscription">プッシュ通知購読の更新 (Mastodon2.4以降)</string>
|
||||
<string name="pseudo_account_not_supported">疑似アカウントでは通知を利用できません。</string>
|
||||
<string name="instance_does_not_support_push_api">タンスのバージョン %1$s は古くてプッシュAPIを利用できません</string>
|
||||
<string name="missing_fcm_device_id">FCMのデバイスIDを準備できません。しばらく後に試してください。</string>
|
||||
<string name="missing_install_id">インストールIDを準備できません。しばらく後に試してください。</string>
|
||||
<string name="token_exported">アプリデータのエクスポート、インポート、バックアップからの復元などでアクセストークンが他のデバイスでも使われている可能性があります。アクセストークンの更新をおすすめします。</string>
|
||||
<string name="push_subscription_deleted">プッシュ通知の購読を取り消しました。</string>
|
||||
<string name="missing_push_api">このタンスはなぜかプッシュAPIがありません。</string>
|
||||
<string name="missing_push_scope">このアクセストークンは古いのでプッシュAPIを利用する権限がありません。アクセストークンの更新をおすすめします。</string>
|
||||
<string name="push_subscription_updated">プッシュ通知の購読を更新しました。</string>
|
||||
|
||||
</resources>
|
||||
|
@ -645,5 +645,14 @@
|
||||
<string name="available_mastodon_2_4_later">(available in Mastodon 2.4 or later)</string>
|
||||
<string name="confirm_boost_private_from">Boost private status from %1$s ? It\'s shown to all followers.</string>
|
||||
<string name="boost_private_toot_not_allowed">You can\'t boost private toot by another person.</string>
|
||||
<string name="push_notification_test">Push Notification Test (Mastodon 2.4 or later)</string>
|
||||
<string name="update_push_subscription">Update push subscription (Mastodon 2.4 or later)</string>
|
||||
<string name="pseudo_account_not_supported">Pseudo account can\'t use notifications.</string>
|
||||
<string name="instance_does_not_support_push_api">Instance version %1$s too old. that does not support Push API.</string>
|
||||
<string name="missing_fcm_device_id">Can\'t prepare FCM device ID. please retry later.</string>
|
||||
<string name="missing_install_id">Can\'t prepare install ID. please retry later.</string>
|
||||
<string name="token_exported">Due to exporting application data or restoring from backup, this access token may be used by other devices as well. Because access tokens that are not used by other devices are required to use push notifications, we recommend updating the access token.</string>
|
||||
<string name="push_subscription_deleted">Push subscription deleted.</string>
|
||||
<string name="missing_push_api">The instance has no Push API endpoint.</string>
|
||||
<string name="missing_push_scope">Your access token does not contains push scope. updating access token is recommended.</string>
|
||||
<string name="push_subscription_updated">Push subscription updated.</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user