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,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<AuthLoginResponse> run({
required String username,
required String password,
required String tokenName,
}) async {
try {
final response = await _dio.post<Map<String, dynamic>>(
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<Map<String, dynamic>>(
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;
});
}
@@ -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<void> run() async {
try {
await _dio.post<void>(MarianumConnectEndpoint.resolve('auth/logout'));
await dio.post<void>(endpoint('auth/logout'));
} on DioException catch (_) {
// ignore — local clear below still happens
} finally {
@@ -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<void>(
MarianumConnectEndpoint.resolve('auth/verify'),
return guard(() async {
await dio.post<void>(
endpoint('auth/verify'),
data: {'username': username, 'password': password},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
});
}
}
@@ -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<GetBreakersResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('breaker'),
);
return GetBreakersResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<GetBreakersResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(endpoint('breaker'));
return GetBreakersResponse.fromJson(response.data!);
});
}
@@ -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<CapabilitiesResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('me/capabilities'),
);
return CapabilitiesResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<CapabilitiesResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
endpoint('me/capabilities'),
);
return CapabilitiesResponse.fromJson(response.data!);
});
}
@@ -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<Uint8List> run() async {
try {
final response = await _dio.get<List<int>>(
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<Uint8List> run() => guard(() async {
final response = await dio.get<List<int>>(
endpoint('newsletter/${Uri.encodeComponent(id)}/file'),
options: Options(responseType: ResponseType.bytes),
);
return Uint8List.fromList(response.data!);
});
}
@@ -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<TickerResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('ticker'),
);
return TickerResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<TickerResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(endpoint('ticker'));
return TickerResponse.fromJson(response.data!);
});
}
@@ -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<TickerNavResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('ticker/pages'),
);
return TickerNavResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<TickerNavResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
endpoint('ticker/pages'),
);
return TickerNavResponse.fromJson(response.data!);
});
}
@@ -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<TickerPageResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve(
'ticker/pages/${Uri.encodeComponent(slug)}',
),
final response = await dio.get<Map<String, dynamic>>(
endpoint('ticker/pages/${Uri.encodeComponent(slug)}'),
);
return TickerPageResponse.fromJson(response.data!);
} on DioException catch (e) {
@@ -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<Uint8List> run() async {
try {
final response = await _dio.get<List<int>>(
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<Uint8List> run() => guard(() async {
final response = await dio.get<List<int>>(
endpoint('ticker/pages/${Uri.encodeComponent(slug)}/file'),
options: Options(responseType: ResponseType.bytes),
);
return Uint8List.fromList(response.data!);
});
}
@@ -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<void> run({
required String deviceIdentifier,
@@ -21,24 +15,20 @@ class PushDeviceRegister {
required String platform,
required String registrationType,
String? appVersion,
}) async {
try {
await _dio.put<void>(
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<void>(
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,
},
);
});
}
@@ -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<int> run() async {
try {
final response = await _dio.post<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('me/push-device/test'),
);
return (response.data?['devices'] as num?)?.toInt() ?? 0;
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<int> run() => guard(() async {
final response = await dio.post<Map<String, dynamic>>(
endpoint('me/push-device/test'),
);
return (response.data?['devices'] as num?)?.toInt() ?? 0;
});
}
@@ -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<void> run({required String deviceIdentifier}) async {
try {
await _dio.delete<void>(
MarianumConnectEndpoint.resolve('me/push-device'),
queryParameters: {'deviceIdentifier': deviceIdentifier},
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<void> run({required String deviceIdentifier}) => guard(() async {
await dio.delete<void>(
endpoint('me/push-device'),
queryParameters: {'deviceIdentifier': deviceIdentifier},
);
});
}
@@ -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<void> run({
required String errorType,
@@ -21,22 +15,18 @@ class ReportClientError {
String? platform,
String? appVersion,
String? deviceModel,
}) async {
try {
await _dio.post<void>(
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<void>(
endpoint('client-errors'),
data: {
'errorType': errorType,
'message': ?message,
'stacktrace': ?stacktrace,
'context': ?context,
'platform': ?platform,
'appVersion': ?appVersion,
'deviceModel': ?deviceModel,
},
);
});
}
@@ -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<void> 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<void>(
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<void>(
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';
@@ -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<void> 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<void> 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 = <String, dynamic>{};
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<void>(
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 = <String, dynamic>{};
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<void>(
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),
},
);
});
}
@@ -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<void> run(CustomTimetableEvent event) async {
try {
await _dio.post<void>(
MarianumConnectEndpoint.resolve('timetable/custom-events'),
data: event.toJson(),
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<void> run(CustomTimetableEvent event) => guard(() async {
await dio.post<void>(
endpoint('timetable/custom-events'),
data: event.toJson(),
);
});
}
@@ -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<GetCustomTimetableEventResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('timetable/custom-events'),
);
return GetCustomTimetableEventResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<GetCustomTimetableEventResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
endpoint('timetable/custom-events'),
);
return GetCustomTimetableEventResponse.fromJson(response.data!);
});
}
@@ -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<void> run(String id) async {
try {
await _dio.delete<void>(
MarianumConnectEndpoint.resolve('timetable/custom-events/$id'),
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<void> run(String id) => guard(() async {
await dio.delete<void>(endpoint('timetable/custom-events/$id'));
});
}
@@ -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<void> run(String id, CustomTimetableEvent event) async {
try {
await _dio.put<void>(
MarianumConnectEndpoint.resolve('timetable/custom-events/$id'),
data: event.toJson(),
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<void> run(String id, CustomTimetableEvent event) => guard(() async {
await dio.put<void>(
endpoint('timetable/custom-events/$id'),
data: event.toJson(),
);
});
}
@@ -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<TimetableGetWeekResponse> run({
required TimetableElementType type,
required int id,
required DateTime from,
required DateTime until,
}) async {
try {
final response = await _dio.get<Map<String, dynamic>>(
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<Map<String, dynamic>>(
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')}';
@@ -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<TimetableGetSchoolyearResponse> run() async {
try {
final response = await _dio.get<Map<String, dynamic>>(
MarianumConnectEndpoint.resolve('timetable/schoolyear'),
);
return TimetableGetSchoolyearResponse.fromJson(response.data!);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<TimetableGetSchoolyearResponse> run() => guard(() async {
final response = await dio.get<Map<String, dynamic>>(
endpoint('timetable/schoolyear'),
);
return TimetableGetSchoolyearResponse.fromJson(response.data!);
});
}
@@ -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<TimetableGetWeekResponse> run({
required DateTime from,
required DateTime until,
}) async {
try {
final response = await _dio.get<Map<String, dynamic>>(
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<Map<String, dynamic>>(
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')}';
@@ -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<void> run(String subjectShort) async {
try {
await _dio.delete<void>(
MarianumConnectEndpoint.resolve('timetable/subject-colors'),
queryParameters: {'subject': subjectShort},
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<void> run(String subjectShort) => guard(() async {
await dio.delete<void>(
endpoint('timetable/subject-colors'),
queryParameters: {'subject': subjectShort},
);
});
}
@@ -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<void> run(String subjectShort, String color) async {
try {
await _dio.put<void>(
MarianumConnectEndpoint.resolve('timetable/subject-colors'),
data: {'subject': subjectShort, 'color': color},
);
} on DioException catch (e) {
throw mapMarianumConnectError(e);
}
}
Future<void> run(String subjectShort, String color) => guard(() async {
await dio.put<void>(
endpoint('timetable/subject-colors'),
data: {'subject': subjectShort, 'color': color},
);
});
}