Push notifications support via UnifiedPush (#2303)
Fixes #793. This is an implementation for push notifications based on UnifiedPush for Tusky. No push gateway (other than UP itself) is needed, since UnifiedPush is simple enough such that it can act as a catch-all endpoint for WebPush messages. When a UnifiedPush distributor is present on-device, we will by default register Tusky as a receiver; if no UnifiedPush distributor is available, then pull notifications are used as a fallback mechanism. Because WebPush messages are encrypted, and Mastodon does not send the keys and IV needed for decryption in the request body, for now the push handler simply acts as a trigger for the pre-existing NotificationWorker which is also used for pull notifications. Nevertheless, I have implemented proper key generation and storage, just in case we would like to implement full decryption support in the future when Mastodon upgrades to the latest WebPush encryption scheme that includes all information in the request body. For users with existing accounts, push notifications will not be enabled until all of the accounts have been re-logged in to grant the new push OAuth scope. A small prompt will be shown (until dismissed) as a Snackbar to explain to the user about this, and an option is added in Account Preferences to facilitate re-login without deleting local drafts and cache.
This commit is contained in:
parent
20f3ec921f
commit
9ec5d6e3b0
20 changed files with 1490 additions and 24 deletions
|
|
@ -64,7 +64,15 @@ data class AccountEntity(
|
|||
var activeNotifications: String = "[]",
|
||||
var emojis: List<Emoji> = emptyList(),
|
||||
var tabPreferences: List<TabData> = defaultTabs(),
|
||||
var notificationsFilter: String = "[\"follow_request\"]"
|
||||
var notificationsFilter: String = "[\"follow_request\"]",
|
||||
// Scope cannot be changed without re-login, so store it in case
|
||||
// the scope needs to be changed in the future
|
||||
var oauthScopes: String = "",
|
||||
var unifiedPushUrl: String = "",
|
||||
var pushPubKey: String = "",
|
||||
var pushPrivKey: String = "",
|
||||
var pushAuth: String = "",
|
||||
var pushServerKey: String = "",
|
||||
) {
|
||||
|
||||
val identifier: String
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
* @param accessToken the access token for the new account
|
||||
* @param domain the domain of the accounts Mastodon instance
|
||||
*/
|
||||
fun addAccount(accessToken: String, domain: String) {
|
||||
fun addAccount(accessToken: String, domain: String, oauthScopes: String) {
|
||||
|
||||
activeAccount?.let {
|
||||
it.isActive = false
|
||||
|
|
@ -65,7 +65,10 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
|
||||
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
||||
val newAccountId = maxAccountId + 1
|
||||
activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true)
|
||||
activeAccount = AccountEntity(
|
||||
id = newAccountId, domain = domain.lowercase(Locale.ROOT),
|
||||
accessToken = accessToken, oauthScopes = oauthScopes, isActive = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -189,4 +192,15 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
id == accountId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an account by its string identifier
|
||||
* @param identifier the string identifier of the account
|
||||
* @return the requested account or null if it was not found
|
||||
*/
|
||||
fun getAccountByIdentifier(identifier: String): AccountEntity? {
|
||||
return accounts.find {
|
||||
identifier == it.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
|||
*/
|
||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 35)
|
||||
}, version = 36)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract AccountDao accountDao();
|
||||
|
|
@ -541,4 +541,16 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_35_36 = new Migration(35, 36) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `oauthScopes` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `unifiedPushUrl` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushPubKey` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushPrivKey` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushAuth` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushServerKey` TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue