Files
Client/lib/push/nextcloud_push_api.dart
T

107 lines
3.8 KiB
Dart

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<NextcloudPushRegistration> 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<bool> unregister({String? authorizationHeader}) async {
final response = await _client.delete(
NextcloudOcs.uri(_path),
headers: _headers(authorizationHeader, null),
);
return response.statusCode == 202;
}
Map<String, String> _headers(
String? authorizationHeader,
String? userAgent,
) => {
...NextcloudOcs.headers(),
'Authorization': ?authorizationHeader,
'User-Agent': ?userAgent,
};
}