diff --git a/lib/api/marianumconnect/queries/auth_login/auth_login.dart b/lib/api/marianumconnect/queries/auth_login/auth_login.dart index 7b37304..99f6779 100644 --- a/lib/api/marianumconnect/queries/auth_login/auth_login.dart +++ b/lib/api/marianumconnect/queries/auth_login/auth_login.dart @@ -1,61 +1,55 @@ import 'package:dio/dio.dart'; import '../../auth/token_storage.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'auth_login_response.dart'; /// Performs the Marianum-Connect bearer login. Used both by the foreground /// login flow and by the auth interceptor's silent re-auth on 401. Does *not* /// run through the shared dio instance — that one has the interceptor, which /// would attempt to re-auth us into a loop if our credentials are wrong. -class AuthLogin { +class AuthLogin extends MarianumConnectQuery { static const Duration _connectTimeout = Duration(seconds: 10); static const Duration _receiveTimeout = Duration(seconds: 15); final MarianumConnectTokenStorage _tokenStorage; - final Dio _dio; AuthLogin({ MarianumConnectTokenStorage tokenStorage = const MarianumConnectTokenStorage(), Dio? dio, }) : _tokenStorage = tokenStorage, - _dio = - dio ?? - Dio( - BaseOptions( - connectTimeout: _connectTimeout, - receiveTimeout: _receiveTimeout, - sendTimeout: _connectTimeout, - responseType: ResponseType.json, - contentType: 'application/json', - ), - ); + super(dio: dio ?? _buildDio()); + + static Dio _buildDio() => Dio( + BaseOptions( + connectTimeout: _connectTimeout, + receiveTimeout: _receiveTimeout, + sendTimeout: _connectTimeout, + responseType: ResponseType.json, + contentType: 'application/json', + ), + ); Future run({ required String username, required String password, required String tokenName, - }) async { - try { - final response = await _dio.post>( - MarianumConnectEndpoint.resolve('auth/login'), - data: { - 'username': username, - 'password': password, - 'tokenName': tokenName, - }, - ); - final payload = AuthLoginResponse.fromJson(response.data!); - await _tokenStorage.write( - token: payload.token, - tokenId: payload.tokenId, - expiresAt: payload.expiresAt, - ); - return payload; - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + }) => guard(() async { + final response = await dio.post>( + endpoint('auth/login'), + data: { + 'username': username, + 'password': password, + 'tokenName': tokenName, + }, + ); + final payload = AuthLoginResponse.fromJson(response.data!); + await _tokenStorage.write( + token: payload.token, + tokenId: payload.tokenId, + expiresAt: payload.expiresAt, + ); + return payload; + }); } diff --git a/lib/api/marianumconnect/queries/auth_logout/auth_logout.dart b/lib/api/marianumconnect/queries/auth_logout/auth_logout.dart index d5f9624..e96d581 100644 --- a/lib/api/marianumconnect/queries/auth_logout/auth_logout.dart +++ b/lib/api/marianumconnect/queries/auth_logout/auth_logout.dart @@ -1,26 +1,23 @@ import 'package:dio/dio.dart'; import '../../auth/token_storage.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Revokes the stored MC bearer token both server-side and locally. Best-effort /// — a network error still clears the local token so the user isn't stuck with /// an unusable session. -class AuthLogout { +class AuthLogout extends MarianumConnectQuery { final MarianumConnectTokenStorage _tokenStorage; - final Dio _dio; AuthLogout({ MarianumConnectTokenStorage tokenStorage = const MarianumConnectTokenStorage(), - Dio? dio, - }) : _tokenStorage = tokenStorage, - _dio = dio ?? MarianumConnectApi.dio(); + super.dio, + }) : _tokenStorage = tokenStorage; Future run() async { try { - await _dio.post(MarianumConnectEndpoint.resolve('auth/logout')); + await dio.post(endpoint('auth/logout')); } on DioException catch (_) { // ignore — local clear below still happens } finally { diff --git a/lib/api/marianumconnect/queries/auth_verify/auth_verify.dart b/lib/api/marianumconnect/queries/auth_verify/auth_verify.dart index e60e5e8..8e3f8a2 100644 --- a/lib/api/marianumconnect/queries/auth_verify/auth_verify.dart +++ b/lib/api/marianumconnect/queries/auth_verify/auth_verify.dart @@ -2,8 +2,7 @@ import 'package:dio/dio.dart'; import '../../../errors/auth_exception.dart'; import '../../auth/token_storage.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Probes that the stored bearer token still maps to the given credentials. /// Server returns 200 only when the credentials belong to the user that the @@ -12,29 +11,28 @@ import '../../marianumconnect_endpoint.dart'; /// /// Bypasses the shared dio singleton so the auth interceptor doesn't kick in /// and obscure a real 401 with a silent re-login. -class AuthVerify { +class AuthVerify extends MarianumConnectQuery { static const Duration _connectTimeout = Duration(seconds: 10); static const Duration _receiveTimeout = Duration(seconds: 15); final MarianumConnectTokenStorage _tokenStorage; - final Dio _dio; AuthVerify({ MarianumConnectTokenStorage tokenStorage = const MarianumConnectTokenStorage(), Dio? dio, }) : _tokenStorage = tokenStorage, - _dio = - dio ?? - Dio( - BaseOptions( - connectTimeout: _connectTimeout, - sendTimeout: _connectTimeout, - receiveTimeout: _receiveTimeout, - responseType: ResponseType.json, - contentType: 'application/json', - ), - ); + super(dio: dio ?? _buildDio()); + + static Dio _buildDio() => Dio( + BaseOptions( + connectTimeout: _connectTimeout, + sendTimeout: _connectTimeout, + receiveTimeout: _receiveTimeout, + responseType: ResponseType.json, + contentType: 'application/json', + ), + ); /// Throws [AuthException] on 401 (credentials no longer match the token's /// user, token missing, or token rejected), other [AppException]s on @@ -49,14 +47,12 @@ class AuthVerify { technicalDetails: 'AuthVerify: no bearer token in storage', ); } - try { - await _dio.post( - MarianumConnectEndpoint.resolve('auth/verify'), + return guard(() async { + await dio.post( + endpoint('auth/verify'), data: {'username': username, 'password': password}, options: Options(headers: {'Authorization': 'Bearer $token'}), ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } + }); } } diff --git a/lib/api/marianumconnect/queries/get_breakers/get_breakers.dart b/lib/api/marianumconnect/queries/get_breakers/get_breakers.dart index 1076ac4..ea14ab2 100644 --- a/lib/api/marianumconnect/queries/get_breakers/get_breakers.dart +++ b/lib/api/marianumconnect/queries/get_breakers/get_breakers.dart @@ -1,26 +1,14 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'get_breakers_response.dart'; /// Fetches the app maintenance breaker rules from `GET /api/mobile/v1/breaker`. /// The endpoint is public: the bearer token is attached if present but not /// required, so this also works before login (e.g. to block the whole app). -class GetBreakers { - final Dio _dio; +class GetBreakers extends MarianumConnectQuery { + GetBreakers({super.dio}); - GetBreakers({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('breaker'), - ); - return GetBreakersResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>(endpoint('breaker')); + return GetBreakersResponse.fromJson(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/get_capabilities/get_capabilities.dart b/lib/api/marianumconnect/queries/get_capabilities/get_capabilities.dart index 5e9485f..2dd3080 100644 --- a/lib/api/marianumconnect/queries/get_capabilities/get_capabilities.dart +++ b/lib/api/marianumconnect/queries/get_capabilities/get_capabilities.dart @@ -1,26 +1,16 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'get_capabilities_response.dart'; /// Fetches the current user's mobile capability flags from /// `GET /api/mobile/v1/me/capabilities`. Goes through the shared dio singleton /// so the bearer token is attached automatically. -class GetCapabilities { - final Dio _dio; +class GetCapabilities extends MarianumConnectQuery { + GetCapabilities({super.dio}); - GetCapabilities({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('me/capabilities'), - ); - return CapabilitiesResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>( + endpoint('me/capabilities'), + ); + return CapabilitiesResponse.fromJson(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart b/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart index 4180c93..ba530ff 100644 --- a/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart +++ b/lib/api/marianumconnect/queries/get_newsletter_file/get_newsletter_file.dart @@ -2,9 +2,7 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Downloads the raw PDF bytes of a Marianum Message from /// `GET /api/mobile/v1/newsletter/{id}/file`. @@ -12,23 +10,16 @@ import '../../marianumconnect_endpoint.dart'; /// Goes through the shared MC dio so the bearer token is attached automatically; /// the bytes are handed to `SfPdfViewer.memory` so no auth header has to be /// plumbed into the viewer itself. -class GetNewsletterFile { +class GetNewsletterFile extends MarianumConnectQuery { final String id; - final Dio _dio; - GetNewsletterFile(this.id, {Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); + GetNewsletterFile(this.id, {super.dio}); - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve( - 'newsletter/${Uri.encodeComponent(id)}/file', - ), - options: Options(responseType: ResponseType.bytes), - ); - return Uint8List.fromList(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>( + endpoint('newsletter/${Uri.encodeComponent(id)}/file'), + options: Options(responseType: ResponseType.bytes), + ); + return Uint8List.fromList(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/get_ticker/get_ticker.dart b/lib/api/marianumconnect/queries/get_ticker/get_ticker.dart index 73ecc27..c725114 100644 --- a/lib/api/marianumconnect/queries/get_ticker/get_ticker.dart +++ b/lib/api/marianumconnect/queries/get_ticker/get_ticker.dart @@ -1,25 +1,13 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'get_ticker_response.dart'; /// Fetches the current "Aktuelles" ticker post from /// `GET /api/mobile/v1/ticker`. Bearer token is attached by the shared dio. -class GetTicker { - final Dio _dio; +class GetTicker extends MarianumConnectQuery { + GetTicker({super.dio}); - GetTicker({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('ticker'), - ); - return TickerResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>(endpoint('ticker')); + return TickerResponse.fromJson(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/get_ticker_nav/get_ticker_nav.dart b/lib/api/marianumconnect/queries/get_ticker_nav/get_ticker_nav.dart index e6d6cf5..e4d8a84 100644 --- a/lib/api/marianumconnect/queries/get_ticker_nav/get_ticker_nav.dart +++ b/lib/api/marianumconnect/queries/get_ticker_nav/get_ticker_nav.dart @@ -1,25 +1,15 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'get_ticker_nav_response.dart'; /// Fetches the filtered ticker page tree from /// `GET /api/mobile/v1/ticker/pages`. -class GetTickerNav { - final Dio _dio; +class GetTickerNav extends MarianumConnectQuery { + GetTickerNav({super.dio}); - GetTickerNav({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('ticker/pages'), - ); - return TickerNavResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>( + endpoint('ticker/pages'), + ); + return TickerNavResponse.fromJson(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/get_ticker_page/get_ticker_page.dart b/lib/api/marianumconnect/queries/get_ticker_page/get_ticker_page.dart index 260cd98..60a5ece 100644 --- a/lib/api/marianumconnect/queries/get_ticker_page/get_ticker_page.dart +++ b/lib/api/marianumconnect/queries/get_ticker_page/get_ticker_page.dart @@ -4,8 +4,7 @@ import 'package:dio/dio.dart'; import '../../../errors/ticker_content_unavailable_exception.dart'; import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'get_ticker_page_response.dart'; /// Fetches a single ticker page from @@ -15,19 +14,17 @@ import 'get_ticker_page_response.dart'; /// 404 `{ "error": "CONTENT_UNAVAILABLE", "webUrl": ... }`; that case is mapped /// to a dedicated [TickerContentUnavailableException] carrying the browser /// fallback URL, so the detail screen can offer "open in browser" instead of a -/// generic error. -class GetTickerPage { +/// generic error. The bespoke 404 handling is why this keeps its own try/catch +/// instead of the base [guard]. +class GetTickerPage extends MarianumConnectQuery { final String slug; - final Dio _dio; - GetTickerPage(this.slug, {Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); + GetTickerPage(this.slug, {super.dio}); Future run() async { try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve( - 'ticker/pages/${Uri.encodeComponent(slug)}', - ), + final response = await dio.get>( + endpoint('ticker/pages/${Uri.encodeComponent(slug)}'), ); return TickerPageResponse.fromJson(response.data!); } on DioException catch (e) { diff --git a/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart b/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart index 1dec832..011db55 100644 --- a/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart +++ b/lib/api/marianumconnect/queries/get_ticker_page_file/get_ticker_page_file.dart @@ -2,9 +2,7 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Downloads the raw bytes of a PROXIED_FILE ticker page from /// `GET /api/mobile/v1/ticker/pages/{slug}/file`. @@ -12,24 +10,16 @@ import '../../marianumconnect_endpoint.dart'; /// Goes through the shared MC dio so the bearer token is attached automatically /// (server enforces page visibility); the bytes are handed to `SfPdfViewer.memory` /// so no auth header has to be plumbed into the viewer itself. -class GetTickerPageFile { +class GetTickerPageFile extends MarianumConnectQuery { final String slug; - final Dio _dio; - GetTickerPageFile(this.slug, {Dio? dio}) - : _dio = dio ?? MarianumConnectApi.dio(); + GetTickerPageFile(this.slug, {super.dio}); - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve( - 'ticker/pages/${Uri.encodeComponent(slug)}/file', - ), - options: Options(responseType: ResponseType.bytes), - ); - return Uint8List.fromList(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>( + endpoint('ticker/pages/${Uri.encodeComponent(slug)}/file'), + options: Options(responseType: ResponseType.bytes), + ); + return Uint8List.fromList(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/push_device_register/push_device_register.dart b/lib/api/marianumconnect/queries/push_device_register/push_device_register.dart index ec514c9..f094756 100644 --- a/lib/api/marianumconnect/queries/push_device_register/push_device_register.dart +++ b/lib/api/marianumconnect/queries/push_device_register/push_device_register.dart @@ -1,17 +1,11 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Registers (upserts) this device's push subscription with MarianumConnect via /// `PUT /api/mobile/v1/me/push-device`. The backend verifies the Nextcloud /// device-identifier signature, stores the routing metadata and starts /// forwarding Nextcloud pushes to this device's FCM token. Responds 204. -class PushDeviceRegister { - final Dio _dio; - - PushDeviceRegister({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); +class PushDeviceRegister extends MarianumConnectQuery { + PushDeviceRegister({super.dio}); Future run({ required String deviceIdentifier, @@ -21,24 +15,20 @@ class PushDeviceRegister { required String platform, required String registrationType, String? appVersion, - }) async { - try { - await _dio.put( - MarianumConnectEndpoint.resolve('me/push-device'), - data: { - 'deviceIdentifier': deviceIdentifier, - 'deviceIdentifierSignature': deviceIdentifierSignature, - 'userPublicKey': userPublicKey, - 'pushToken': pushToken, - 'platform': platform, - // 'general' | 'talk' — the backend derives the NC hash comparison - // value from it (general = sha512(token), talk = sha512(token+'#talk')). - 'registrationType': registrationType, - 'appVersion': ?appVersion, - }, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + }) => guard(() async { + await dio.put( + endpoint('me/push-device'), + data: { + 'deviceIdentifier': deviceIdentifier, + 'deviceIdentifierSignature': deviceIdentifierSignature, + 'userPublicKey': userPublicKey, + 'pushToken': pushToken, + 'platform': platform, + // 'general' | 'talk' — the backend derives the NC hash comparison + // value from it (general = sha512(token), talk = sha512(token+'#talk')). + 'registrationType': registrationType, + 'appVersion': ?appVersion, + }, + ); + }); } diff --git a/lib/api/marianumconnect/queries/push_device_test/push_device_test.dart b/lib/api/marianumconnect/queries/push_device_test/push_device_test.dart index d688008..e1b8077 100644 --- a/lib/api/marianumconnect/queries/push_device_test/push_device_test.dart +++ b/lib/api/marianumconnect/queries/push_device_test/push_device_test.dart @@ -1,25 +1,15 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Triggers a test push to all of the current user's registered devices via /// `POST /api/mobile/v1/me/push-device/test`. Returns the number of devices the /// backend dispatched to (0 when none are registered). -class PushDeviceTest { - final Dio _dio; +class PushDeviceTest extends MarianumConnectQuery { + PushDeviceTest({super.dio}); - PushDeviceTest({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.post>( - MarianumConnectEndpoint.resolve('me/push-device/test'), - ); - return (response.data?['devices'] as num?)?.toInt() ?? 0; - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.post>( + endpoint('me/push-device/test'), + ); + return (response.data?['devices'] as num?)?.toInt() ?? 0; + }); } diff --git a/lib/api/marianumconnect/queries/push_device_unregister/push_device_unregister.dart b/lib/api/marianumconnect/queries/push_device_unregister/push_device_unregister.dart index 6a06c2c..2f1fa4b 100644 --- a/lib/api/marianumconnect/queries/push_device_unregister/push_device_unregister.dart +++ b/lib/api/marianumconnect/queries/push_device_unregister/push_device_unregister.dart @@ -1,25 +1,15 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Removes this device's push subscription from MarianumConnect via /// `DELETE /api/mobile/v1/me/push-device?deviceIdentifier=...`. Idempotent /// (204 even when the row is already gone). -class PushDeviceUnregister { - final Dio _dio; +class PushDeviceUnregister extends MarianumConnectQuery { + PushDeviceUnregister({super.dio}); - PushDeviceUnregister({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run({required String deviceIdentifier}) async { - try { - await _dio.delete( - MarianumConnectEndpoint.resolve('me/push-device'), - queryParameters: {'deviceIdentifier': deviceIdentifier}, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run({required String deviceIdentifier}) => guard(() async { + await dio.delete( + endpoint('me/push-device'), + queryParameters: {'deviceIdentifier': deviceIdentifier}, + ); + }); } diff --git a/lib/api/marianumconnect/queries/report_client_error/report_client_error.dart b/lib/api/marianumconnect/queries/report_client_error/report_client_error.dart index d8ee4e4..65bc54e 100644 --- a/lib/api/marianumconnect/queries/report_client_error/report_client_error.dart +++ b/lib/api/marianumconnect/queries/report_client_error/report_client_error.dart @@ -1,17 +1,11 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Sends a single client-side error report to MarianumConnect /// (`POST client-errors`). The endpoint is public, so reports that happen /// before login are still captured; when a bearer token is present the shared /// dio interceptor attaches it and the server attributes the report to that user. -class ReportClientError { - final Dio _dio; - - ReportClientError({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); +class ReportClientError extends MarianumConnectQuery { + ReportClientError({super.dio}); Future run({ required String errorType, @@ -21,22 +15,18 @@ class ReportClientError { String? platform, String? appVersion, String? deviceModel, - }) async { - try { - await _dio.post( - MarianumConnectEndpoint.resolve('client-errors'), - data: { - 'errorType': errorType, - 'message': ?message, - 'stacktrace': ?stacktrace, - 'context': ?context, - 'platform': ?platform, - 'appVersion': ?appVersion, - 'deviceModel': ?deviceModel, - }, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + }) => guard(() async { + await dio.post( + endpoint('client-errors'), + data: { + 'errorType': errorType, + 'message': ?message, + 'stacktrace': ?stacktrace, + 'context': ?context, + 'platform': ?platform, + 'appVersion': ?appVersion, + 'deviceModel': ?deviceModel, + }, + ); + }); } diff --git a/lib/api/marianumconnect/queries/submit_feedback/submit_feedback.dart b/lib/api/marianumconnect/queries/submit_feedback/submit_feedback.dart index e433a21..2eeac7e 100644 --- a/lib/api/marianumconnect/queries/submit_feedback/submit_feedback.dart +++ b/lib/api/marianumconnect/queries/submit_feedback/submit_feedback.dart @@ -3,46 +3,37 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:dio/dio.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Submits user feedback to MarianumConnect (`POST me/feedback`, bearer-auth). /// The optional screenshot is sent base64-encoded. Replaces the legacy mhsl.eu /// `server/feedback` endpoint — the user no longer needs to be sent explicitly, /// the bearer token identifies them. -class SubmitFeedback { - final Dio _dio; - - SubmitFeedback({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); +class SubmitFeedback extends MarianumConnectQuery { + SubmitFeedback({super.dio}); Future run({ required String message, Uint8List? screenshot, String screenshotContentType = 'image/png', - }) async { - try { - final package = await PackageInfo.fromPlatform(); - final screenshotBase64 = screenshot != null ? base64Encode(screenshot) : null; - await _dio.post( - MarianumConnectEndpoint.resolve('me/feedback'), - data: { - 'message': message, - 'screenshot': ?screenshotBase64, - 'screenshotContentType': screenshot != null ? screenshotContentType : null, - 'platform': _platform(), - 'appVersion': package.version, - 'appBuild': int.tryParse(package.buildNumber), - 'deviceModel': await _deviceModel(), - }, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + }) => guard(() async { + final package = await PackageInfo.fromPlatform(); + final screenshotBase64 = screenshot != null ? base64Encode(screenshot) : null; + await dio.post( + endpoint('me/feedback'), + data: { + 'message': message, + 'screenshot': ?screenshotBase64, + 'screenshotContentType': screenshot != null ? screenshotContentType : null, + 'platform': _platform(), + 'appVersion': package.version, + 'appBuild': int.tryParse(package.buildNumber), + 'deviceModel': await _deviceModel(), + }, + ); + }); static String? _platform() { if (Platform.isAndroid) return 'android'; diff --git a/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart b/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart index f93fbc3..5fb367f 100644 --- a/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart +++ b/lib/api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart @@ -3,14 +3,11 @@ import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:dio/dio.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../../../push/push_registration_store.dart'; import '../../../../push/push_registration_type.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'telemetry_device_id.dart'; /// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) — @@ -19,10 +16,8 @@ import 'telemetry_device_id.dart'; /// (so a fresh registration isn't under-reported until the next launch). /// Bearer-authenticated via the shared dio interceptor. Replaces the legacy /// mhsl.eu `server/userIndex/update` call. -class TelemetryHeartbeat { - final Dio _dio; - - TelemetryHeartbeat({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); +class TelemetryHeartbeat extends MarianumConnectQuery { + TelemetryHeartbeat({super.dio}); /// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed /// send never disrupts app start. Used from the app shell's initState and @@ -35,52 +30,48 @@ class TelemetryHeartbeat { ); } - Future send({required bool notificationsEnabled}) async { - try { - final info = DeviceInfoPlugin(); - final package = await PackageInfo.fromPlatform(); - final deviceIdentifier = await TelemetryDeviceId.resolve(); - final pushDeviceIdentifier = await const PushRegistrationStore() - .deviceIdentifier(PushRegistrationType.general); + Future send({required bool notificationsEnabled}) => guard(() async { + final info = DeviceInfoPlugin(); + final package = await PackageInfo.fromPlatform(); + final deviceIdentifier = await TelemetryDeviceId.resolve(); + final pushDeviceIdentifier = await const PushRegistrationStore() + .deviceIdentifier(PushRegistrationType.general); - var platform = 'unknown'; - String? deviceModel; - String? osVersion; - var raw = {}; - if (Platform.isAndroid) { - platform = 'android'; - final androidInfo = await info.androidInfo; - deviceModel = androidInfo.model; - osVersion = androidInfo.version.release; - raw = androidInfo.data; - } else if (Platform.isIOS) { - platform = 'ios'; - final appleInfo = await info.iosInfo; - deviceModel = appleInfo.utsname.machine; - osVersion = appleInfo.systemVersion; - raw = appleInfo.data; - } - - await _dio.post( - MarianumConnectEndpoint.resolve('me/telemetry'), - data: { - 'deviceIdentifier': deviceIdentifier, - // `pushDeviceIdentifier` reflects a *completed* registration and is - // absent until it lands; `pushEnabled` carries the user's intent - // (the notification toggle) so the backend can tell "user wants push" - // apart from "registration not finished yet". - 'pushDeviceIdentifier': ?pushDeviceIdentifier, - 'pushEnabled': notificationsEnabled, - 'platform': platform, - 'appVersion': package.version, - 'appBuild': int.tryParse(package.buildNumber), - 'deviceModel': deviceModel, - 'osVersion': osVersion, - 'deviceInfo': jsonEncode(raw), - }, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); + var platform = 'unknown'; + String? deviceModel; + String? osVersion; + var raw = {}; + if (Platform.isAndroid) { + platform = 'android'; + final androidInfo = await info.androidInfo; + deviceModel = androidInfo.model; + osVersion = androidInfo.version.release; + raw = androidInfo.data; + } else if (Platform.isIOS) { + platform = 'ios'; + final appleInfo = await info.iosInfo; + deviceModel = appleInfo.utsname.machine; + osVersion = appleInfo.systemVersion; + raw = appleInfo.data; } - } + + await dio.post( + endpoint('me/telemetry'), + data: { + 'deviceIdentifier': deviceIdentifier, + // `pushDeviceIdentifier` reflects a *completed* registration and is + // absent until it lands; `pushEnabled` carries the user's intent + // (the notification toggle) so the backend can tell "user wants push" + // apart from "registration not finished yet". + 'pushDeviceIdentifier': ?pushDeviceIdentifier, + 'pushEnabled': notificationsEnabled, + 'platform': platform, + 'appVersion': package.version, + 'appBuild': int.tryParse(package.buildNumber), + 'deviceModel': deviceModel, + 'osVersion': osVersion, + 'deviceInfo': jsonEncode(raw), + }, + ); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_add.dart b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_add.dart index 67541b1..6c4b01d 100644 --- a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_add.dart +++ b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_add.dart @@ -1,23 +1,13 @@ -import 'package:dio/dio.dart'; - import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; -class TimetableCustomEventsAdd { - final Dio _dio; +class TimetableCustomEventsAdd extends MarianumConnectQuery { + TimetableCustomEventsAdd({super.dio}); - TimetableCustomEventsAdd({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run(CustomTimetableEvent event) async { - try { - await _dio.post( - MarianumConnectEndpoint.resolve('timetable/custom-events'), - data: event.toJson(), - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run(CustomTimetableEvent event) => guard(() async { + await dio.post( + endpoint('timetable/custom-events'), + data: event.toJson(), + ); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_get.dart b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_get.dart index 656e8c6..3acdc2c 100644 --- a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_get.dart +++ b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_get.dart @@ -1,23 +1,13 @@ -import 'package:dio/dio.dart'; - import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; -class TimetableCustomEventsGet { - final Dio _dio; +class TimetableCustomEventsGet extends MarianumConnectQuery { + TimetableCustomEventsGet({super.dio}); - TimetableCustomEventsGet({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('timetable/custom-events'), - ); - return GetCustomTimetableEventResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>( + endpoint('timetable/custom-events'), + ); + return GetCustomTimetableEventResponse.fromJson(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_remove.dart b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_remove.dart index 0ee0ef0..e70441e 100644 --- a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_remove.dart +++ b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_remove.dart @@ -1,22 +1,9 @@ -import 'package:dio/dio.dart'; +import '../../marianumconnect_query.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +class TimetableCustomEventsRemove extends MarianumConnectQuery { + TimetableCustomEventsRemove({super.dio}); -class TimetableCustomEventsRemove { - final Dio _dio; - - TimetableCustomEventsRemove({Dio? dio}) - : _dio = dio ?? MarianumConnectApi.dio(); - - Future run(String id) async { - try { - await _dio.delete( - MarianumConnectEndpoint.resolve('timetable/custom-events/$id'), - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run(String id) => guard(() async { + await dio.delete(endpoint('timetable/custom-events/$id')); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_update.dart b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_update.dart index b8a315a..5315cd1 100644 --- a/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_update.dart +++ b/lib/api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_update.dart @@ -1,24 +1,13 @@ -import 'package:dio/dio.dart'; - import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart'; -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; -class TimetableCustomEventsUpdate { - final Dio _dio; +class TimetableCustomEventsUpdate extends MarianumConnectQuery { + TimetableCustomEventsUpdate({super.dio}); - TimetableCustomEventsUpdate({Dio? dio}) - : _dio = dio ?? MarianumConnectApi.dio(); - - Future run(String id, CustomTimetableEvent event) async { - try { - await _dio.put( - MarianumConnectEndpoint.resolve('timetable/custom-events/$id'), - data: event.toJson(), - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run(String id, CustomTimetableEvent event) => guard(() async { + await dio.put( + endpoint('timetable/custom-events/$id'), + data: event.toJson(), + ); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart b/lib/api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart index c21e4a9..34c4bd8 100644 --- a/lib/api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart +++ b/lib/api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart @@ -1,35 +1,25 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import '../timetable_get_week/timetable_get_week_response.dart'; import 'timetable_element_type.dart'; /// Fetches a foreign element's weekly timetable from /// `timetable/{student|teacher|room|class}/{id}`. The response shape is /// identical to `timetable/me`, so [TimetableGetWeekResponse] is reused. -class TimetableGetElementWeek { - final Dio _dio; - - TimetableGetElementWeek({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); +class TimetableGetElementWeek extends MarianumConnectQuery { + TimetableGetElementWeek({super.dio}); Future run({ required TimetableElementType type, required int id, required DateTime from, required DateTime until, - }) async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('timetable/${type.pathSegment}/$id'), - queryParameters: {'from': _format(from), 'until': _format(until)}, - ); - return TimetableGetWeekResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + }) => guard(() async { + final response = await dio.get>( + endpoint('timetable/${type.pathSegment}/$id'), + queryParameters: {'from': _format(from), 'until': _format(until)}, + ); + return TimetableGetWeekResponse.fromJson(response.data!); + }); String _format(DateTime d) => '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; diff --git a/lib/api/marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear.dart b/lib/api/marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear.dart index 30d8ffa..9e95c36 100644 --- a/lib/api/marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear.dart +++ b/lib/api/marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear.dart @@ -1,23 +1,13 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'timetable_get_schoolyear_response.dart'; -class TimetableGetSchoolyear { - final Dio _dio; +class TimetableGetSchoolyear extends MarianumConnectQuery { + TimetableGetSchoolyear({super.dio}); - TimetableGetSchoolyear({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run() async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('timetable/schoolyear'), - ); - return TimetableGetSchoolyearResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run() => guard(() async { + final response = await dio.get>( + endpoint('timetable/schoolyear'), + ); + return TimetableGetSchoolyearResponse.fromJson(response.data!); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_get_week/timetable_get_week.dart b/lib/api/marianumconnect/queries/timetable_get_week/timetable_get_week.dart index 97cdfe7..62a4453 100644 --- a/lib/api/marianumconnect/queries/timetable_get_week/timetable_get_week.dart +++ b/lib/api/marianumconnect/queries/timetable_get_week/timetable_get_week.dart @@ -1,32 +1,19 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; import 'timetable_get_week_response.dart'; -class TimetableGetWeek { - final Dio _dio; - - TimetableGetWeek({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); +class TimetableGetWeek extends MarianumConnectQuery { + TimetableGetWeek({super.dio}); Future run({ required DateTime from, required DateTime until, - }) async { - try { - final response = await _dio.get>( - MarianumConnectEndpoint.resolve('timetable/me'), - queryParameters: { - 'from': _format(from), - 'until': _format(until), - }, - ); - return TimetableGetWeekResponse.fromJson(response.data!); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + }) => guard(() async { + final response = await dio.get>( + endpoint('timetable/me'), + queryParameters: {'from': _format(from), 'until': _format(until)}, + ); + return TimetableGetWeekResponse.fromJson(response.data!); + }); String _format(DateTime d) => '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; diff --git a/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_remove.dart b/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_remove.dart index 44b7631..77230ff 100644 --- a/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_remove.dart +++ b/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_remove.dart @@ -1,25 +1,14 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Entfernt die persönliche Farbe eines Fachs (Fach fällt auf die Status-Farbe /// zurück). Schlüssel ist das Fach-Kürzel (`shortName`). -class TimetableSubjectColorRemove { - final Dio _dio; +class TimetableSubjectColorRemove extends MarianumConnectQuery { + TimetableSubjectColorRemove({super.dio}); - TimetableSubjectColorRemove({Dio? dio}) - : _dio = dio ?? MarianumConnectApi.dio(); - - Future run(String subjectShort) async { - try { - await _dio.delete( - MarianumConnectEndpoint.resolve('timetable/subject-colors'), - queryParameters: {'subject': subjectShort}, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run(String subjectShort) => guard(() async { + await dio.delete( + endpoint('timetable/subject-colors'), + queryParameters: {'subject': subjectShort}, + ); + }); } diff --git a/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_set.dart b/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_set.dart index 674359a..b66e964 100644 --- a/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_set.dart +++ b/lib/api/marianumconnect/queries/timetable_subject_colors/timetable_subject_color_set.dart @@ -1,24 +1,14 @@ -import 'package:dio/dio.dart'; - -import '../../errors/marianumconnect_error.dart'; -import '../../marianumconnect_api.dart'; -import '../../marianumconnect_endpoint.dart'; +import '../../marianumconnect_query.dart'; /// Setzt (oder überschreibt) die persönliche Farbe eines Fachs. Der Schlüssel /// ist das Fach-Kürzel (`shortName`); die Farbe ist ein Palette-Name. -class TimetableSubjectColorSet { - final Dio _dio; +class TimetableSubjectColorSet extends MarianumConnectQuery { + TimetableSubjectColorSet({super.dio}); - TimetableSubjectColorSet({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); - - Future run(String subjectShort, String color) async { - try { - await _dio.put( - MarianumConnectEndpoint.resolve('timetable/subject-colors'), - data: {'subject': subjectShort, 'color': color}, - ); - } on DioException catch (e) { - throw mapMarianumConnectError(e); - } - } + Future run(String subjectShort, String color) => guard(() async { + await dio.put( + endpoint('timetable/subject-colors'), + data: {'subject': subjectShort, 'color': color}, + ); + }); }