Skip to content

Commit

Permalink
fixup! feat(notifications_push_repository): Init
Browse files Browse the repository at this point in the history
  • Loading branch information
provokateurin committed Sep 23, 2024
1 parent 1f4d8f9 commit 4d72c86
Show file tree
Hide file tree
Showing 8 changed files with 510 additions and 157 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export 'src/models/models.dart' show PushNotification;
export 'src/notifications_push_repository.dart';
export 'src/notifications_push_storage.dart';
export 'src/utils/encryption.dart' show parseEncryptedPushNotifications;
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ abstract class PushNotification implements Built<PushNotification, PushNotificat
}

final Serializers _serializers = (Serializers().toBuilder()
..add(notifications.DecryptedSubject.serializer)
..add(PushNotification.serializer)
..add(notifications.DecryptedSubject.serializer)
..addPlugin(StandardJsonPlugin()))
.build();
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ abstract class PushSubscription implements Built<PushSubscription, PushSubscript
}

final Serializers _serializers = (Serializers().toBuilder()
..add(notifications.PushDevice.serializer)
..add(PushSubscription.serializer)
..add(notifications.PushDevice.serializer)
..addPlugin(StandardJsonPlugin()))
.build();
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:account_repository/account_repository.dart';
import 'package:built_collection/built_collection.dart';
import 'package:collection/collection.dart';
import 'package:crypton/crypton.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:neon_framework/storage.dart';
import 'package:nextcloud/notifications.dart' as notifications;
import 'package:notifications_push_repository/src/models/models.dart';
import 'package:notifications_push_repository/src/notifications_push_storage.dart';
import 'package:notifications_push_repository/src/utils/encryption.dart';
import 'package:unifiedpush/unifiedpush.dart';

part 'notifications_push_storage.dart';

/// Signature of the callback triggered by UnifiedPush when a new push notification is received.
typedef OnMessageCallback = void Function(Uint8List message, String accountID);

Expand All @@ -35,9 +37,7 @@ final class NotificationsPushRepository {
var _initialized = false;
StreamSubscription<BuiltList<Account>>? _accountsListener;
late final RSAPrivateKey _privateKey;
late BuiltList<Account> _accounts;
String? _selectedDistributor;
String? _previousDistributor;

/// Returns all available distributors.
Future<BuiltList<String>> get distributors async {
Expand All @@ -61,7 +61,7 @@ final class NotificationsPushRepository {
}

_selectedDistributor = distributor;
await _updateSubscriptions();
await _updateSubscriptions(distributorChanged: true);
}

/// Initializes the repository.
Expand All @@ -70,20 +70,17 @@ final class NotificationsPushRepository {
return;
}

_accounts = (await _accountRepository.accounts.first).accounts;
_accountsListener = _accountRepository.accounts.skip(1).map((e) => e.accounts).listen((accounts) async {
_accounts = accounts;
await _updateSubscriptions();
});

_selectedDistributor = await UnifiedPush.getDistributor();
_previousDistributor = _selectedDistributor;

_privateKey = await getDevicePrivateKey(_storage);

await UnifiedPush.initialize(
onNewEndpoint: (endpoint, accountID) async {
final account = _accounts.firstWhereOrNull((account) => account.id == accountID);
final account = _accountRepository.accountByID(accountID);
if (account == null) {
_log.finer('Account $accountID not found'); // coverage:ignore-line
return;
Expand All @@ -97,12 +94,25 @@ final class NotificationsPushRepository {
}
subscription = subscription.rebuild((b) => b.endpoint = endpoint);

subscription = await _unregisterNextcloud(accountID, account, subscription);
subscription = await _registerNextcloud(account, endpoint, subscription);
await _saveUpdatedSubscription(subscriptions, account.id, subscription);
var pushDevice = subscription.pushDevice;
if (pushDevice != null) {
await _unregisterNextcloud(accountID, account, pushDevice);
subscription = subscription.rebuild((b) => b.pushDevice = null);
}

pushDevice = await _registerNextcloud(account, endpoint);
subscription = subscription.rebuild((b) {
if (pushDevice == null) {
b.pushDevice = null;
} else {
b.pushDevice.replace(pushDevice);
}
});

await _storage.updateSubscription(account.id, subscription);
},
onUnregistered: (accountID) async {
final account = _accounts.firstWhereOrNull((account) => account.id == accountID);
final account = _accountRepository.accountByID(accountID);
if (account == null) {
_log.finer('Account $accountID not found'); // coverage:ignore-line
return;
Expand All @@ -116,9 +126,13 @@ final class NotificationsPushRepository {
}

subscription = subscription.rebuild((b) => b.endpoint = null);
subscription = await _unregisterNextcloud(accountID, account, subscription);
final pushDevice = subscription.pushDevice;
if (pushDevice != null) {
await _unregisterNextcloud(accountID, account, pushDevice);
subscription = subscription.rebuild((b) => b.pushDevice = null);
}

await _saveUpdatedSubscription(subscriptions, accountID, subscription);
await _storage.updateSubscription(accountID, subscription);
},
onMessage: _onMessage,
);
Expand All @@ -128,16 +142,15 @@ final class NotificationsPushRepository {
_initialized = true;
}

Future<void> _updateSubscriptions() async {
Future<void> _updateSubscriptions({bool distributorChanged = false}) async {
if (_selectedDistributor == null) {
_log.fine('Push notifications disabled, removing all subscriptions');

await _unregisterUnifiedPush();
return;
}

if (_selectedDistributor != _previousDistributor) {
_previousDistributor = _selectedDistributor;
if (distributorChanged) {
_log.finer('UnifiedPush distributor changed to $_selectedDistributor');

await _unregisterUnifiedPush();
Expand All @@ -149,7 +162,8 @@ final class NotificationsPushRepository {

Future<void> _registerUnifiedPush() async {
// Notifications will only work on accounts with app password
for (final account in _accounts.where((a) => a.credentials.appPassword != null)) {
final accounts = (await _accountRepository.accounts.first).accounts;
for (final account in accounts.where((a) => a.credentials.appPassword != null)) {
_log.finer('Registering ${account.id} for UnifiedPush');

await UnifiedPush.registerApp(account.id);
Expand All @@ -160,24 +174,28 @@ final class NotificationsPushRepository {
final subscriptions = await _storage.readSubscriptions();
for (final entry in subscriptions.entries) {
final accountID = entry.key;
final account = _accounts.firstWhereOrNull((account) => account.id == accountID);
final account = _accountRepository.accountByID(accountID);
var subscription = entry.value;

subscription = await _unregisterNextcloud(accountID, account, subscription);
final pushDevice = subscription.pushDevice;
if (pushDevice != null) {
await _unregisterNextcloud(accountID, account, pushDevice);
subscription = subscription.rebuild((b) => b.pushDevice = null);
}

if (subscription.endpoint != null) {
_log.finer('Unregistering $accountID from UnifiedPush');

subscription = subscription.rebuild((b) => b.endpoint = null);
await _saveUpdatedSubscription(subscriptions, accountID, subscription);

await UnifiedPush.unregister(accountID);
} else {
await _saveUpdatedSubscription(subscriptions, accountID, subscription);

subscription = subscription.rebuild((b) => b.endpoint = null);
}

await _storage.updateSubscription(accountID, subscription);
}
}

Future<PushSubscription> _registerNextcloud(Account account, String endpoint, PushSubscription subscription) async {
Future<notifications.PushDevice?> _registerNextcloud(Account account, String endpoint) async {
_log.finer('Registering ${account.id} at Nextcloud');

try {
Expand All @@ -190,54 +208,25 @@ final class NotificationsPushRepository {
),
);

return subscription.rebuild(
(b) => b
..endpoint = endpoint
..pushDevice.replace(response.body.ocs.data),
);
} on DynamiteApiException catch (error) {
return response.body.ocs.data;
} on http.ClientException catch (error) {
_log.warning('Failed to register ${account.id} at Nextcloud', error);
}

return subscription;
return null;
}

Future<PushSubscription> _unregisterNextcloud(
Future<void> _unregisterNextcloud(
String accountID,
Account? account,
PushSubscription subscription,
notifications.PushDevice pushDevice,
) async {
if (subscription.pushDevice != null) {
_log.finer('Unregistering $accountID from Nextcloud');
_log.finer('Unregistering $accountID from Nextcloud');

try {
await account?.client.notifications.push.removeDevice();
} on DynamiteApiException catch (error) {
_log.warning('Failed to unregister $accountID at Nextcloud', error);
}
try {
await account?.client.notifications.push.removeDevice();
} on http.ClientException catch (error) {
_log.warning('Failed to unregister $accountID at Nextcloud', error);
}

// Remove the push device either way, as we need to register for a new one later
return subscription.rebuild((b) => b.pushDevice = null);
}

Future<void> _saveUpdatedSubscription(
BuiltMap<String, PushSubscription> subscriptions,
String accountID,
PushSubscription subscription,
) async {
await _storage.saveSubscriptions(
subscriptions.rebuild(
(b) {
if (subscription.endpoint == null && subscription.pushDevice == null) {
_log.finer('Removing subscription for $accountID');
b.remove(accountID);
} else {
_log.finer('Saving subscription for $accountID');
b[accountID] = subscription;
}
},
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:crypton/crypton.dart';
import 'package:neon_framework/storage.dart';
import 'package:notifications_push_repository/src/models/models.dart';
part of 'notifications_push_repository.dart';

/// A storage for push subscriptions and the device private key.
class NotificationsPushStorage {
Expand Down Expand Up @@ -45,19 +40,14 @@ class NotificationsPushStorage {
return builder.build();
}

/// Saves the updated [subscriptions].
/// Updates a [subscription].
///
/// Removes all stored subscriptions that are no longer present in the new [subscriptions].
Future<void> saveSubscriptions(BuiltMap<String, PushSubscription> subscriptions) async {
for (final entry in subscriptions.entries) {
_pushSubscriptionsPersistence.setValue(entry.key, json.encode(entry.value.toJson()));
}

final keys = await _pushSubscriptionsPersistence.keys();
for (final key in keys) {
if (!subscriptions.containsKey(key)) {
await _pushSubscriptionsPersistence.remove(key);
}
/// If [PushSubscription.endpoint] and [PushSubscription.pushDevice] are `null` the subscription will be removed.
Future<void> updateSubscription(String key, PushSubscription subscription) async {
if (subscription.endpoint == null && subscription.pushDevice == null) {
_pushSubscriptionsPersistence.remove(key);
} else {
_pushSubscriptionsPersistence.setValue(key, json.encode(subscription.toJson()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies:
built_value: ^8.9.2
collection: ^1.0.0
crypton: ^2.0.0
http: ^1.0.0
logging: ^1.0.0
meta: ^1.0.0
neon_framework:
Expand All @@ -34,7 +35,6 @@ dev_dependencies:
# https://github.com/invertase/melos/issues/755
flutter:
sdk: flutter
http: ^1.2.2
mocktail: ^1.0.4
neon_lints:
git:
Expand Down
Loading

0 comments on commit 4d72c86

Please sign in to comment.