Files
Client/lib/api/marianumcloud/talk/talk_api.dart
T
MineTecandClaude Fable 5.1 279b973e89 hardened API decoding against server fields the models did not expect
The Talk room list crashed for a new account because the server omits
lastMessage for rooms without a visible message and the model required it.
Beyond that single field, a review of the Talk models against the OpenAPI
spec of the installed version (23.0.8) found the same crash class in several
places: closed enums over open server values (inCall bitmask, rich-object
types, actor and message types), a deleted parent sent as {id, deleted},
and the autocomplete status arriving as an object.

- json_serializable `checked: true`: decode errors now name class and field
- lists (rooms, messages, participants, autocomplete) decode per entry;
  a malformed entry is skipped and reported via ClientErrorReporter
- unknownEnumValue fallbacks for all response enums of Talk and breakers
- lastMessage nullable; inCall as bitmask; deleted parent placeholder;
  autocomplete status object/string; fixed shareWithDisplayNameUnique key
- GetRoomParams no longer sends null query parameters

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HrepDTL4qDe35TuS29sLW3
2026-09-29 16:35:41 +02:00

92 lines
3.2 KiB
Dart

import 'dart:developer';
import 'package:http/http.dart' as http;
import '../../api_params.dart';
import '../../api_response.dart';
import '../../errors/network_exception.dart';
import '../../errors/parse_exception.dart';
import '../../errors/talk_exception.dart';
import '../../http_errors.dart';
import '../../json_lenient.dart';
import '../../marianumconnect/queries/report_client_error/client_error_reporter.dart';
import '../nextcloud_ocs.dart';
import 'talk_error.dart';
abstract class TalkApi<T extends ApiResponse?> {
// package:http has no timeout of its own; a request stuck on a dead
// connection (e.g. one started right before a suspend) would spin forever.
static const Duration _timeout = Duration(seconds: 30);
String path;
ApiParams? body;
Map<String, String>? headers;
Map<String, dynamic>? getParameters;
TalkApi(this.path, this.body, {this.headers, this.getParameters});
/// Defaults to a plain GET; endpoints with a body or another verb override it.
Future<http.Response>? request(
Uri uri,
ApiParams? body,
Map<String, String>? headers,
) => http.get(uri, headers: headers);
T assemble(String raw);
/// Override to parse large payloads off the UI isolate (e.g. via `compute`
/// with a top-level parser); defaults to the synchronous [assemble].
Future<T> assembleAsync(String raw) async => assemble(raw);
Future<T> run() async {
final endpoint = NextcloudOcs.uri(
'apps/spreed/api/$path',
queryParameters: getParameters,
);
final mergedHeaders = {...NextcloudOcs.headers(), ...?headers};
final data = await sendGuarded(
'Talk $endpoint',
() => request(endpoint, body, mergedHeaders)?.timeout(_timeout),
);
if (data == null) {
throw const NetworkException(
userMessage: 'Keine Antwort vom Talk-Server erhalten.',
technicalDetails: 'Talk request returned null',
);
}
final status = data.statusCode;
if (status < 200 || status >= 300) {
// Talk's OCS errors carry the real reason in the body (expired session,
// removed participant, ...); include a trimmed preview so the dialog and
// logs surface the cause instead of just the bare status code.
final detail = httpErrorDetail('Talk $endpoint', data.body, status);
log(detail);
// Talk's own codes need their own wording: a 400/412/413 here is a
// rejected request, not a struggling server, and "try again later"
// would send the user in circles. 401/403/404 keep the shared mapping
// so the auth handling stays in one place.
if (status == 400 || status == 412 || status == 413 || status == 429) {
throw TalkException(
TalkError('failure', status, ''),
technicalDetails: detail,
);
}
throwForStatus(status, detail);
}
final T assembled;
try {
assembled = await assembleAsync(data.body);
} catch (e) {
throw ParseException(technicalDetails: 'Talk $endpoint assemble: $e');
}
assembled?.headers = data.headers;
ClientErrorReporter.reportDecodeWarnings('Talk $path', [
...?assembled?.parseWarnings,
...drainDecodeWarnings(),
]);
return assembled;
}
}