adopt MarianumConnectQuery base in remaining queries

This commit is contained in:
2026-07-12 23:28:51 +02:00
parent 0a2ff5c3fb
commit db329c7299
25 changed files with 309 additions and 544 deletions
@@ -1,29 +1,27 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../auth/token_storage.dart'; import '../../auth/token_storage.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_endpoint.dart';
import 'auth_login_response.dart'; import 'auth_login_response.dart';
/// Performs the Marianum-Connect bearer login. Used both by the foreground /// 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* /// 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 /// 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. /// 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 _connectTimeout = Duration(seconds: 10);
static const Duration _receiveTimeout = Duration(seconds: 15); static const Duration _receiveTimeout = Duration(seconds: 15);
final MarianumConnectTokenStorage _tokenStorage; final MarianumConnectTokenStorage _tokenStorage;
final Dio _dio;
AuthLogin({ AuthLogin({
MarianumConnectTokenStorage tokenStorage = MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(), const MarianumConnectTokenStorage(),
Dio? dio, Dio? dio,
}) : _tokenStorage = tokenStorage, }) : _tokenStorage = tokenStorage,
_dio = super(dio: dio ?? _buildDio());
dio ??
Dio( static Dio _buildDio() => Dio(
BaseOptions( BaseOptions(
connectTimeout: _connectTimeout, connectTimeout: _connectTimeout,
receiveTimeout: _receiveTimeout, receiveTimeout: _receiveTimeout,
@@ -37,10 +35,9 @@ class AuthLogin {
required String username, required String username,
required String password, required String password,
required String tokenName, required String tokenName,
}) async { }) => guard(() async {
try { final response = await dio.post<Map<String, dynamic>>(
final response = await _dio.post<Map<String, dynamic>>( endpoint('auth/login'),
MarianumConnectEndpoint.resolve('auth/login'),
data: { data: {
'username': username, 'username': username,
'password': password, 'password': password,
@@ -54,8 +51,5 @@ class AuthLogin {
expiresAt: payload.expiresAt, expiresAt: payload.expiresAt,
); );
return payload; return payload;
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,26 +1,23 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../auth/token_storage.dart'; import '../../auth/token_storage.dart';
import '../../marianumconnect_api.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_endpoint.dart';
/// Revokes the stored MC bearer token both server-side and locally. Best-effort /// 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 /// — a network error still clears the local token so the user isn't stuck with
/// an unusable session. /// an unusable session.
class AuthLogout { class AuthLogout extends MarianumConnectQuery {
final MarianumConnectTokenStorage _tokenStorage; final MarianumConnectTokenStorage _tokenStorage;
final Dio _dio;
AuthLogout({ AuthLogout({
MarianumConnectTokenStorage tokenStorage = MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(), const MarianumConnectTokenStorage(),
Dio? dio, super.dio,
}) : _tokenStorage = tokenStorage, }) : _tokenStorage = tokenStorage;
_dio = dio ?? MarianumConnectApi.dio();
Future<void> run() async { Future<void> run() async {
try { try {
await _dio.post<void>(MarianumConnectEndpoint.resolve('auth/logout')); await dio.post<void>(endpoint('auth/logout'));
} on DioException catch (_) { } on DioException catch (_) {
// ignore — local clear below still happens // ignore — local clear below still happens
} finally { } finally {
@@ -2,8 +2,7 @@ import 'package:dio/dio.dart';
import '../../../errors/auth_exception.dart'; import '../../../errors/auth_exception.dart';
import '../../auth/token_storage.dart'; import '../../auth/token_storage.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_endpoint.dart';
/// Probes that the stored bearer token still maps to the given credentials. /// 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 /// Server returns 200 only when the credentials belong to the user that the
@@ -12,21 +11,20 @@ import '../../marianumconnect_endpoint.dart';
/// ///
/// Bypasses the shared dio singleton so the auth interceptor doesn't kick in /// Bypasses the shared dio singleton so the auth interceptor doesn't kick in
/// and obscure a real 401 with a silent re-login. /// 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 _connectTimeout = Duration(seconds: 10);
static const Duration _receiveTimeout = Duration(seconds: 15); static const Duration _receiveTimeout = Duration(seconds: 15);
final MarianumConnectTokenStorage _tokenStorage; final MarianumConnectTokenStorage _tokenStorage;
final Dio _dio;
AuthVerify({ AuthVerify({
MarianumConnectTokenStorage tokenStorage = MarianumConnectTokenStorage tokenStorage =
const MarianumConnectTokenStorage(), const MarianumConnectTokenStorage(),
Dio? dio, Dio? dio,
}) : _tokenStorage = tokenStorage, }) : _tokenStorage = tokenStorage,
_dio = super(dio: dio ?? _buildDio());
dio ??
Dio( static Dio _buildDio() => Dio(
BaseOptions( BaseOptions(
connectTimeout: _connectTimeout, connectTimeout: _connectTimeout,
sendTimeout: _connectTimeout, sendTimeout: _connectTimeout,
@@ -49,14 +47,12 @@ class AuthVerify {
technicalDetails: 'AuthVerify: no bearer token in storage', technicalDetails: 'AuthVerify: no bearer token in storage',
); );
} }
try { return guard(() async {
await _dio.post<void>( await dio.post<void>(
MarianumConnectEndpoint.resolve('auth/verify'), endpoint('auth/verify'),
data: {'username': username, 'password': password}, data: {'username': username, 'password': password},
options: Options(headers: {'Authorization': 'Bearer $token'}), options: Options(headers: {'Authorization': 'Bearer $token'}),
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
} }
} }
@@ -1,26 +1,14 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'get_breakers_response.dart'; import 'get_breakers_response.dart';
/// Fetches the app maintenance breaker rules from `GET /api/mobile/v1/breaker`. /// 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 /// 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). /// required, so this also works before login (e.g. to block the whole app).
class GetBreakers { class GetBreakers extends MarianumConnectQuery {
final Dio _dio; GetBreakers({super.dio});
GetBreakers({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<GetBreakersResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(endpoint('breaker'));
Future<GetBreakersResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('breaker'),
);
return GetBreakersResponse.fromJson(response.data!); return GetBreakersResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,26 +1,16 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'get_capabilities_response.dart'; import 'get_capabilities_response.dart';
/// Fetches the current user's mobile capability flags from /// Fetches the current user's mobile capability flags from
/// `GET /api/mobile/v1/me/capabilities`. Goes through the shared dio singleton /// `GET /api/mobile/v1/me/capabilities`. Goes through the shared dio singleton
/// so the bearer token is attached automatically. /// so the bearer token is attached automatically.
class GetCapabilities { class GetCapabilities extends MarianumConnectQuery {
final Dio _dio; GetCapabilities({super.dio});
GetCapabilities({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<CapabilitiesResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
Future<CapabilitiesResponse> run() async { endpoint('me/capabilities'),
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('me/capabilities'),
); );
return CapabilitiesResponse.fromJson(response.data!); return CapabilitiesResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -2,9 +2,7 @@ import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Downloads the raw PDF bytes of a Marianum Message from /// Downloads the raw PDF bytes of a Marianum Message from
/// `GET /api/mobile/v1/newsletter/{id}/file`. /// `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; /// 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 /// the bytes are handed to `SfPdfViewer.memory` so no auth header has to be
/// plumbed into the viewer itself. /// plumbed into the viewer itself.
class GetNewsletterFile { class GetNewsletterFile extends MarianumConnectQuery {
final String id; final String id;
final Dio _dio;
GetNewsletterFile(this.id, {Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); GetNewsletterFile(this.id, {super.dio});
Future<Uint8List> run() async { Future<Uint8List> run() => guard(() async {
try { final response = await dio.get<List<int>>(
final response = await _dio.get<List<int>>( endpoint('newsletter/${Uri.encodeComponent(id)}/file'),
MarianumConnectEndpoint.resolve(
'newsletter/${Uri.encodeComponent(id)}/file',
),
options: Options(responseType: ResponseType.bytes), options: Options(responseType: ResponseType.bytes),
); );
return Uint8List.fromList(response.data!); return Uint8List.fromList(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,25 +1,13 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'get_ticker_response.dart'; import 'get_ticker_response.dart';
/// Fetches the current "Aktuelles" ticker post from /// Fetches the current "Aktuelles" ticker post from
/// `GET /api/mobile/v1/ticker`. Bearer token is attached by the shared dio. /// `GET /api/mobile/v1/ticker`. Bearer token is attached by the shared dio.
class GetTicker { class GetTicker extends MarianumConnectQuery {
final Dio _dio; GetTicker({super.dio});
GetTicker({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<TickerResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(endpoint('ticker'));
Future<TickerResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('ticker'),
);
return TickerResponse.fromJson(response.data!); return TickerResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,25 +1,15 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'get_ticker_nav_response.dart'; import 'get_ticker_nav_response.dart';
/// Fetches the filtered ticker page tree from /// Fetches the filtered ticker page tree from
/// `GET /api/mobile/v1/ticker/pages`. /// `GET /api/mobile/v1/ticker/pages`.
class GetTickerNav { class GetTickerNav extends MarianumConnectQuery {
final Dio _dio; GetTickerNav({super.dio});
GetTickerNav({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<TickerNavResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
Future<TickerNavResponse> run() async { endpoint('ticker/pages'),
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('ticker/pages'),
); );
return TickerNavResponse.fromJson(response.data!); return TickerNavResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -4,8 +4,7 @@ import 'package:dio/dio.dart';
import '../../../errors/ticker_content_unavailable_exception.dart'; import '../../../errors/ticker_content_unavailable_exception.dart';
import '../../errors/marianumconnect_error.dart'; import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_endpoint.dart';
import 'get_ticker_page_response.dart'; import 'get_ticker_page_response.dart';
/// Fetches a single ticker page from /// 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 /// 404 `{ "error": "CONTENT_UNAVAILABLE", "webUrl": ... }`; that case is mapped
/// to a dedicated [TickerContentUnavailableException] carrying the browser /// to a dedicated [TickerContentUnavailableException] carrying the browser
/// fallback URL, so the detail screen can offer "open in browser" instead of a /// fallback URL, so the detail screen can offer "open in browser" instead of a
/// generic error. /// generic error. The bespoke 404 handling is why this keeps its own try/catch
class GetTickerPage { /// instead of the base [guard].
class GetTickerPage extends MarianumConnectQuery {
final String slug; final String slug;
final Dio _dio;
GetTickerPage(this.slug, {Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); GetTickerPage(this.slug, {super.dio});
Future<TickerPageResponse> run() async { Future<TickerPageResponse> run() async {
try { try {
final response = await _dio.get<Map<String, dynamic>>( final response = await dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve( endpoint('ticker/pages/${Uri.encodeComponent(slug)}'),
'ticker/pages/${Uri.encodeComponent(slug)}',
),
); );
return TickerPageResponse.fromJson(response.data!); return TickerPageResponse.fromJson(response.data!);
} on DioException catch (e) { } on DioException catch (e) {
@@ -2,9 +2,7 @@ import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Downloads the raw bytes of a PROXIED_FILE ticker page from /// Downloads the raw bytes of a PROXIED_FILE ticker page from
/// `GET /api/mobile/v1/ticker/pages/{slug}/file`. /// `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 /// Goes through the shared MC dio so the bearer token is attached automatically
/// (server enforces page visibility); the bytes are handed to `SfPdfViewer.memory` /// (server enforces page visibility); the bytes are handed to `SfPdfViewer.memory`
/// so no auth header has to be plumbed into the viewer itself. /// so no auth header has to be plumbed into the viewer itself.
class GetTickerPageFile { class GetTickerPageFile extends MarianumConnectQuery {
final String slug; final String slug;
final Dio _dio;
GetTickerPageFile(this.slug, {Dio? dio}) GetTickerPageFile(this.slug, {super.dio});
: _dio = dio ?? MarianumConnectApi.dio();
Future<Uint8List> run() async { Future<Uint8List> run() => guard(() async {
try { final response = await dio.get<List<int>>(
final response = await _dio.get<List<int>>( endpoint('ticker/pages/${Uri.encodeComponent(slug)}/file'),
MarianumConnectEndpoint.resolve(
'ticker/pages/${Uri.encodeComponent(slug)}/file',
),
options: Options(responseType: ResponseType.bytes), options: Options(responseType: ResponseType.bytes),
); );
return Uint8List.fromList(response.data!); return Uint8List.fromList(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,17 +1,11 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Registers (upserts) this device's push subscription with MarianumConnect via /// Registers (upserts) this device's push subscription with MarianumConnect via
/// `PUT /api/mobile/v1/me/push-device`. The backend verifies the Nextcloud /// `PUT /api/mobile/v1/me/push-device`. The backend verifies the Nextcloud
/// device-identifier signature, stores the routing metadata and starts /// device-identifier signature, stores the routing metadata and starts
/// forwarding Nextcloud pushes to this device's FCM token. Responds 204. /// forwarding Nextcloud pushes to this device's FCM token. Responds 204.
class PushDeviceRegister { class PushDeviceRegister extends MarianumConnectQuery {
final Dio _dio; PushDeviceRegister({super.dio});
PushDeviceRegister({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<void> run({ Future<void> run({
required String deviceIdentifier, required String deviceIdentifier,
@@ -21,10 +15,9 @@ class PushDeviceRegister {
required String platform, required String platform,
required String registrationType, required String registrationType,
String? appVersion, String? appVersion,
}) async { }) => guard(() async {
try { await dio.put<void>(
await _dio.put<void>( endpoint('me/push-device'),
MarianumConnectEndpoint.resolve('me/push-device'),
data: { data: {
'deviceIdentifier': deviceIdentifier, 'deviceIdentifier': deviceIdentifier,
'deviceIdentifierSignature': deviceIdentifierSignature, 'deviceIdentifierSignature': deviceIdentifierSignature,
@@ -37,8 +30,5 @@ class PushDeviceRegister {
'appVersion': ?appVersion, 'appVersion': ?appVersion,
}, },
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,25 +1,15 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Triggers a test push to all of the current user's registered devices via /// 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 /// `POST /api/mobile/v1/me/push-device/test`. Returns the number of devices the
/// backend dispatched to (0 when none are registered). /// backend dispatched to (0 when none are registered).
class PushDeviceTest { class PushDeviceTest extends MarianumConnectQuery {
final Dio _dio; PushDeviceTest({super.dio});
PushDeviceTest({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<int> run() => guard(() async {
final response = await dio.post<Map<String, dynamic>>(
Future<int> run() async { endpoint('me/push-device/test'),
try {
final response = await _dio.post<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('me/push-device/test'),
); );
return (response.data?['devices'] as num?)?.toInt() ?? 0; return (response.data?['devices'] as num?)?.toInt() ?? 0;
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,25 +1,15 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Removes this device's push subscription from MarianumConnect via /// Removes this device's push subscription from MarianumConnect via
/// `DELETE /api/mobile/v1/me/push-device?deviceIdentifier=...`. Idempotent /// `DELETE /api/mobile/v1/me/push-device?deviceIdentifier=...`. Idempotent
/// (204 even when the row is already gone). /// (204 even when the row is already gone).
class PushDeviceUnregister { class PushDeviceUnregister extends MarianumConnectQuery {
final Dio _dio; PushDeviceUnregister({super.dio});
PushDeviceUnregister({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<void> run({required String deviceIdentifier}) => guard(() async {
await dio.delete<void>(
Future<void> run({required String deviceIdentifier}) async { endpoint('me/push-device'),
try {
await _dio.delete<void>(
MarianumConnectEndpoint.resolve('me/push-device'),
queryParameters: {'deviceIdentifier': deviceIdentifier}, queryParameters: {'deviceIdentifier': deviceIdentifier},
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,17 +1,11 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Sends a single client-side error report to MarianumConnect /// Sends a single client-side error report to MarianumConnect
/// (`POST client-errors`). The endpoint is public, so reports that happen /// (`POST client-errors`). The endpoint is public, so reports that happen
/// before login are still captured; when a bearer token is present the shared /// 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. /// dio interceptor attaches it and the server attributes the report to that user.
class ReportClientError { class ReportClientError extends MarianumConnectQuery {
final Dio _dio; ReportClientError({super.dio});
ReportClientError({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<void> run({ Future<void> run({
required String errorType, required String errorType,
@@ -21,10 +15,9 @@ class ReportClientError {
String? platform, String? platform,
String? appVersion, String? appVersion,
String? deviceModel, String? deviceModel,
}) async { }) => guard(() async {
try { await dio.post<void>(
await _dio.post<void>( endpoint('client-errors'),
MarianumConnectEndpoint.resolve('client-errors'),
data: { data: {
'errorType': errorType, 'errorType': errorType,
'message': ?message, 'message': ?message,
@@ -35,8 +28,5 @@ class ReportClientError {
'deviceModel': ?deviceModel, 'deviceModel': ?deviceModel,
}, },
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -3,32 +3,26 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Submits user feedback to MarianumConnect (`POST me/feedback`, bearer-auth). /// Submits user feedback to MarianumConnect (`POST me/feedback`, bearer-auth).
/// The optional screenshot is sent base64-encoded. Replaces the legacy mhsl.eu /// The optional screenshot is sent base64-encoded. Replaces the legacy mhsl.eu
/// `server/feedback` endpoint — the user no longer needs to be sent explicitly, /// `server/feedback` endpoint — the user no longer needs to be sent explicitly,
/// the bearer token identifies them. /// the bearer token identifies them.
class SubmitFeedback { class SubmitFeedback extends MarianumConnectQuery {
final Dio _dio; SubmitFeedback({super.dio});
SubmitFeedback({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<void> run({ Future<void> run({
required String message, required String message,
Uint8List? screenshot, Uint8List? screenshot,
String screenshotContentType = 'image/png', String screenshotContentType = 'image/png',
}) async { }) => guard(() async {
try {
final package = await PackageInfo.fromPlatform(); final package = await PackageInfo.fromPlatform();
final screenshotBase64 = screenshot != null ? base64Encode(screenshot) : null; final screenshotBase64 = screenshot != null ? base64Encode(screenshot) : null;
await _dio.post<void>( await dio.post<void>(
MarianumConnectEndpoint.resolve('me/feedback'), endpoint('me/feedback'),
data: { data: {
'message': message, 'message': message,
'screenshot': ?screenshotBase64, 'screenshot': ?screenshotBase64,
@@ -39,10 +33,7 @@ class SubmitFeedback {
'deviceModel': await _deviceModel(), 'deviceModel': await _deviceModel(),
}, },
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
static String? _platform() { static String? _platform() {
if (Platform.isAndroid) return 'android'; if (Platform.isAndroid) return 'android';
@@ -3,14 +3,11 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import '../../../../push/push_registration_store.dart'; import '../../../../push/push_registration_store.dart';
import '../../../../push/push_registration_type.dart'; import '../../../../push/push_registration_type.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'telemetry_device_id.dart'; import 'telemetry_device_id.dart';
/// Sends a telemetry heartbeat to MarianumConnect (`POST me/telemetry`) — /// 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). /// (so a fresh registration isn't under-reported until the next launch).
/// Bearer-authenticated via the shared dio interceptor. Replaces the legacy /// Bearer-authenticated via the shared dio interceptor. Replaces the legacy
/// mhsl.eu `server/userIndex/update` call. /// mhsl.eu `server/userIndex/update` call.
class TelemetryHeartbeat { class TelemetryHeartbeat extends MarianumConnectQuery {
final Dio _dio; TelemetryHeartbeat({super.dio});
TelemetryHeartbeat({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
/// Fire-and-forget: schedules a heartbeat and swallows any error, so a failed /// 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 /// send never disrupts app start. Used from the app shell's initState and
@@ -35,8 +30,7 @@ class TelemetryHeartbeat {
); );
} }
Future<void> send({required bool notificationsEnabled}) async { Future<void> send({required bool notificationsEnabled}) => guard(() async {
try {
final info = DeviceInfoPlugin(); final info = DeviceInfoPlugin();
final package = await PackageInfo.fromPlatform(); final package = await PackageInfo.fromPlatform();
final deviceIdentifier = await TelemetryDeviceId.resolve(); final deviceIdentifier = await TelemetryDeviceId.resolve();
@@ -61,8 +55,8 @@ class TelemetryHeartbeat {
raw = appleInfo.data; raw = appleInfo.data;
} }
await _dio.post<void>( await dio.post<void>(
MarianumConnectEndpoint.resolve('me/telemetry'), endpoint('me/telemetry'),
data: { data: {
'deviceIdentifier': deviceIdentifier, 'deviceIdentifier': deviceIdentifier,
// `pushDeviceIdentifier` reflects a *completed* registration and is // `pushDeviceIdentifier` reflects a *completed* registration and is
@@ -79,8 +73,5 @@ class TelemetryHeartbeat {
'deviceInfo': jsonEncode(raw), 'deviceInfo': jsonEncode(raw),
}, },
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,23 +1,13 @@
import 'package:dio/dio.dart';
import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart'; import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
class TimetableCustomEventsAdd { class TimetableCustomEventsAdd extends MarianumConnectQuery {
final Dio _dio; TimetableCustomEventsAdd({super.dio});
TimetableCustomEventsAdd({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<void> run(CustomTimetableEvent event) => guard(() async {
await dio.post<void>(
Future<void> run(CustomTimetableEvent event) async { endpoint('timetable/custom-events'),
try {
await _dio.post<void>(
MarianumConnectEndpoint.resolve('timetable/custom-events'),
data: event.toJson(), data: event.toJson(),
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,23 +1,13 @@
import 'package:dio/dio.dart';
import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart'; import '../../../mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
class TimetableCustomEventsGet { class TimetableCustomEventsGet extends MarianumConnectQuery {
final Dio _dio; TimetableCustomEventsGet({super.dio});
TimetableCustomEventsGet({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<GetCustomTimetableEventResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
Future<GetCustomTimetableEventResponse> run() async { endpoint('timetable/custom-events'),
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('timetable/custom-events'),
); );
return GetCustomTimetableEventResponse.fromJson(response.data!); return GetCustomTimetableEventResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,22 +1,9 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart'; class TimetableCustomEventsRemove extends MarianumConnectQuery {
import '../../marianumconnect_api.dart'; TimetableCustomEventsRemove({super.dio});
import '../../marianumconnect_endpoint.dart';
class TimetableCustomEventsRemove { Future<void> run(String id) => guard(() async {
final Dio _dio; await dio.delete<void>(endpoint('timetable/custom-events/$id'));
});
TimetableCustomEventsRemove({Dio? dio})
: _dio = dio ?? MarianumConnectApi.dio();
Future<void> run(String id) async {
try {
await _dio.delete<void>(
MarianumConnectEndpoint.resolve('timetable/custom-events/$id'),
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,24 +1,13 @@
import 'package:dio/dio.dart';
import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart'; import '../../../mhsl/custom_timetable_event/custom_timetable_event.dart';
import '../../errors/marianumconnect_error.dart'; import '../../marianumconnect_query.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
class TimetableCustomEventsUpdate { class TimetableCustomEventsUpdate extends MarianumConnectQuery {
final Dio _dio; TimetableCustomEventsUpdate({super.dio});
TimetableCustomEventsUpdate({Dio? dio}) Future<void> run(String id, CustomTimetableEvent event) => guard(() async {
: _dio = dio ?? MarianumConnectApi.dio(); await dio.put<void>(
endpoint('timetable/custom-events/$id'),
Future<void> run(String id, CustomTimetableEvent event) async {
try {
await _dio.put<void>(
MarianumConnectEndpoint.resolve('timetable/custom-events/$id'),
data: event.toJson(), data: event.toJson(),
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,35 +1,25 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import '../timetable_get_week/timetable_get_week_response.dart'; import '../timetable_get_week/timetable_get_week_response.dart';
import 'timetable_element_type.dart'; import 'timetable_element_type.dart';
/// Fetches a foreign element's weekly timetable from /// Fetches a foreign element's weekly timetable from
/// `timetable/{student|teacher|room|class}/{id}`. The response shape is /// `timetable/{student|teacher|room|class}/{id}`. The response shape is
/// identical to `timetable/me`, so [TimetableGetWeekResponse] is reused. /// identical to `timetable/me`, so [TimetableGetWeekResponse] is reused.
class TimetableGetElementWeek { class TimetableGetElementWeek extends MarianumConnectQuery {
final Dio _dio; TimetableGetElementWeek({super.dio});
TimetableGetElementWeek({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetWeekResponse> run({ Future<TimetableGetWeekResponse> run({
required TimetableElementType type, required TimetableElementType type,
required int id, required int id,
required DateTime from, required DateTime from,
required DateTime until, required DateTime until,
}) async { }) => guard(() async {
try { final response = await dio.get<Map<String, dynamic>>(
final response = await _dio.get<Map<String, dynamic>>( endpoint('timetable/${type.pathSegment}/$id'),
MarianumConnectEndpoint.resolve('timetable/${type.pathSegment}/$id'),
queryParameters: {'from': _format(from), 'until': _format(until)}, queryParameters: {'from': _format(from), 'until': _format(until)},
); );
return TimetableGetWeekResponse.fromJson(response.data!); return TimetableGetWeekResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
String _format(DateTime d) => String _format(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
@@ -1,23 +1,13 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_schoolyear_response.dart'; import 'timetable_get_schoolyear_response.dart';
class TimetableGetSchoolyear { class TimetableGetSchoolyear extends MarianumConnectQuery {
final Dio _dio; TimetableGetSchoolyear({super.dio});
TimetableGetSchoolyear({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<TimetableGetSchoolyearResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
Future<TimetableGetSchoolyearResponse> run() async { endpoint('timetable/schoolyear'),
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('timetable/schoolyear'),
); );
return TimetableGetSchoolyearResponse.fromJson(response.data!); return TimetableGetSchoolyearResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,32 +1,19 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
import 'timetable_get_week_response.dart'; import 'timetable_get_week_response.dart';
class TimetableGetWeek { class TimetableGetWeek extends MarianumConnectQuery {
final Dio _dio; TimetableGetWeek({super.dio});
TimetableGetWeek({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio();
Future<TimetableGetWeekResponse> run({ Future<TimetableGetWeekResponse> run({
required DateTime from, required DateTime from,
required DateTime until, required DateTime until,
}) async { }) => guard(() async {
try { final response = await dio.get<Map<String, dynamic>>(
final response = await _dio.get<Map<String, dynamic>>( endpoint('timetable/me'),
MarianumConnectEndpoint.resolve('timetable/me'), queryParameters: {'from': _format(from), 'until': _format(until)},
queryParameters: {
'from': _format(from),
'until': _format(until),
},
); );
return TimetableGetWeekResponse.fromJson(response.data!); return TimetableGetWeekResponse.fromJson(response.data!);
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
String _format(DateTime d) => String _format(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
@@ -1,25 +1,14 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Entfernt die persönliche Farbe eines Fachs (Fach fällt auf die Status-Farbe /// Entfernt die persönliche Farbe eines Fachs (Fach fällt auf die Status-Farbe
/// zurück). Schlüssel ist das Fach-Kürzel (`shortName`). /// zurück). Schlüssel ist das Fach-Kürzel (`shortName`).
class TimetableSubjectColorRemove { class TimetableSubjectColorRemove extends MarianumConnectQuery {
final Dio _dio; TimetableSubjectColorRemove({super.dio});
TimetableSubjectColorRemove({Dio? dio}) Future<void> run(String subjectShort) => guard(() async {
: _dio = dio ?? MarianumConnectApi.dio(); await dio.delete<void>(
endpoint('timetable/subject-colors'),
Future<void> run(String subjectShort) async {
try {
await _dio.delete<void>(
MarianumConnectEndpoint.resolve('timetable/subject-colors'),
queryParameters: {'subject': subjectShort}, queryParameters: {'subject': subjectShort},
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }
@@ -1,24 +1,14 @@
import 'package:dio/dio.dart'; import '../../marianumconnect_query.dart';
import '../../errors/marianumconnect_error.dart';
import '../../marianumconnect_api.dart';
import '../../marianumconnect_endpoint.dart';
/// Setzt (oder überschreibt) die persönliche Farbe eines Fachs. Der Schlüssel /// Setzt (oder überschreibt) die persönliche Farbe eines Fachs. Der Schlüssel
/// ist das Fach-Kürzel (`shortName`); die Farbe ist ein Palette-Name. /// ist das Fach-Kürzel (`shortName`); die Farbe ist ein Palette-Name.
class TimetableSubjectColorSet { class TimetableSubjectColorSet extends MarianumConnectQuery {
final Dio _dio; TimetableSubjectColorSet({super.dio});
TimetableSubjectColorSet({Dio? dio}) : _dio = dio ?? MarianumConnectApi.dio(); Future<void> run(String subjectShort, String color) => guard(() async {
await dio.put<void>(
Future<void> run(String subjectShort, String color) async { endpoint('timetable/subject-colors'),
try {
await _dio.put<void>(
MarianumConnectEndpoint.resolve('timetable/subject-colors'),
data: {'subject': subjectShort, 'color': color}, data: {'subject': subjectShort, 'color': color},
); );
} on DioException catch (e) { });
throw mapMarianumConnectError(e);
}
}
} }