import 'dart:convert'; import 'package:http/http.dart' as http; import '../api/marianumcloud/nextcloud_ocs.dart'; /// Result of a successful Nextcloud push-v2 registration. class NextcloudPushRegistration { /// Per-user server public key (PEM). Forwarded to MarianumConnect as /// `userPublicKey` and used device-side to verify push signatures. final String publicKey; /// Opaque device identifier assigned by Nextcloud. final String deviceIdentifier; /// Signature over [deviceIdentifier], forwarded as `deviceIdentifierSignature`. final String signature; /// True when Nextcloud created a new subscription (HTTP 201); false when the /// existing one was already up to date (HTTP 200). final bool created; const NextcloudPushRegistration({ required this.publicKey, required this.deviceIdentifier, required this.signature, required this.created, }); } /// Thin client for Nextcloud's push-v2 device endpoints under /// `/ocs/v2.php/apps/notifications/api/v2/push`. Uses the shared OCS headers, /// which authenticate with the app password once available (the push /// registration binds to it). class NextcloudPushApi { static const _path = 'apps/notifications/api/v2/push'; final http.Client _client; NextcloudPushApi({http.Client? client}) : _client = client ?? http.Client(); /// Registers (or refreshes) a device subscription. [devicePublicKeyPem] must /// be the 64-column SPKI PEM. [proxyServer] is the MarianumConnect push-proxy /// base URL (with trailing slash). /// /// [authorizationHeader] overrides the Basic auth (e.g. the Talk app /// password — NC binds the subscription to the authenticated session token). /// [userAgent] overrides the UA; a Talk-pattern UA makes NC classify the /// subscription as apptype `talk` and route Talk pushes to it. Future register({ required String pushTokenHash, required String devicePublicKeyPem, required String proxyServer, String? authorizationHeader, String? userAgent, }) async { final response = await _client.post( NextcloudOcs.uri(_path), headers: _headers(authorizationHeader, userAgent), body: { 'pushTokenHash': pushTokenHash, 'devicePublicKey': devicePublicKeyPem, 'proxyServer': proxyServer, }, ); if (response.statusCode < 200 || response.statusCode >= 300) { throw Exception('NC push register HTTP ${response.statusCode}'); } final json = jsonDecode(utf8.decode(response.bodyBytes)); final data = (json as Map)['ocs']?['data']; if (data is! Map) throw Exception('NC push register: malformed response'); final publicKey = data['publicKey'] as String?; final deviceIdentifier = data['deviceIdentifier'] as String?; final signature = data['signature'] as String?; if (publicKey == null || deviceIdentifier == null || signature == null) { throw Exception('NC push register: missing fields'); } return NextcloudPushRegistration( publicKey: publicKey, deviceIdentifier: deviceIdentifier, signature: signature, created: response.statusCode == 201, ); } /// Unregisters one device subscription — the DELETE is per session token, so /// each registration is removed with its own app password via /// [authorizationHeader]. Returns true when the server responded 202, /// meaning the proxy subscription should also be removed. Future unregister({String? authorizationHeader}) async { final response = await _client.delete( NextcloudOcs.uri(_path), headers: _headers(authorizationHeader, null), ); return response.statusCode == 202; } Map _headers( String? authorizationHeader, String? userAgent, ) => { ...NextcloudOcs.headers(), 'Authorization': ?authorizationHeader, 'User-Agent': ?userAgent, }; }