// ignore_for_file: public_member_api_docs import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart'; import 'package:shelf_router/shelf_router.dart'; /// Implements the listening part of a Nextcloud push proxy class NextcloudPushProxy { HttpServer? _server; late StreamController _onNewDeviceController; late StreamController _onNewNotificationController; Stream? _onNewDeviceStream; Stream? _onNewNotificationStream; /// Listens for new devices Stream get onNewDevice { if (_onNewDeviceStream == null) { throw Exception('Server not created'); } return _onNewDeviceStream!; } /// Listens for new notifications Stream get onNewNotification { if (_onNewNotificationStream == null) { throw Exception('Server not created'); } return _onNewNotificationStream!; } late final _router = Router() ..post('/devices', _devicesHandler) ..post('/notifications', _notificationsHandler) ..get('/health', (final _) async => Response.ok('')); Future _devicesHandler(final Request request) async { final data = Uri(query: await request.readAsString()).queryParameters; _onNewDeviceController.add( PushProxyDevice( pushToken: data['pushToken']!, deviceIdentifier: data['deviceIdentifier']!, deviceIdentifierSignature: data['deviceIdentifierSignature']!, userPublicKey: data['userPublicKey']!, ), ); return Response.ok(''); } Future _notificationsHandler(final Request request) async { final data = Uri(query: await request.readAsString()).queryParameters; for (final notification in data.values) { final notificationData = json.decode(notification) as Map; _onNewNotificationController.add( PushProxyNotification( deviceIdentifier: notificationData['deviceIdentifier']! as String, pushTokenHash: notificationData['pushTokenHash']! as String, subject: notificationData['subject']! as String, signature: notificationData['signature']! as String, priority: notificationData['priority']! as String, type: notificationData['type']! as String, ), ); } return Response.ok(''); } /// Creates a server listening on the [port] Future create({ final bool logging = true, final int port = 8080, }) async { if (_server != null) { throw Exception('Server already created'); } _onNewDeviceController = StreamController(); _onNewNotificationController = StreamController(); _onNewDeviceStream = _onNewDeviceController.stream.asBroadcastStream(); _onNewNotificationStream = _onNewNotificationController.stream.asBroadcastStream(); var handler = Cascade().add(_router.call).handler; if (logging) { handler = logRequests().addHandler(handler); } final server = await serve( handler, InternetAddress.anyIPv4, port, ); server.autoCompress = true; _server = server; } /// Closes the server Future close() async { if (_server != null) { await _server!.close(); _server = null; await _onNewDeviceController.close(); await _onNewNotificationController.close(); } } } class PushProxyDevice { PushProxyDevice({ required this.pushToken, required this.deviceIdentifier, required this.deviceIdentifierSignature, required this.userPublicKey, }); factory PushProxyDevice.fromJson(final Map data) => PushProxyDevice( pushToken: data['pushToken'] as String, deviceIdentifier: data['deviceIdentifier'] as String, deviceIdentifierSignature: data['deviceIdentifierSignature'] as String, userPublicKey: data['userPublicKey'] as String, ); Map toJson() => { 'pushToken': pushToken, 'deviceIdentifier': deviceIdentifier, 'deviceIdentifierSignature': deviceIdentifierSignature, 'userPublicKey': userPublicKey, }; final String pushToken; final String deviceIdentifier; final String deviceIdentifierSignature; final String userPublicKey; } class PushProxyNotification { PushProxyNotification({ required this.deviceIdentifier, required this.pushTokenHash, required this.subject, required this.signature, required this.priority, required this.type, }); final String deviceIdentifier; final String pushTokenHash; final String subject; final String signature; final String priority; final String type; Map toPushNotificationData() => { 'subject': subject, 'signature': signature, 'priority': priority, 'type': type, }; }