import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import '../friendica_client/paged_response.dart'; import '../globals.dart'; import '../models/auth/profile.dart'; import '../models/exec_error.dart'; final _logger = Logger('NetworkUtils'); http.Response requestTimeout() => http.Response('Client side timeout', 408); enum _RequestType { get, } const _expireDuration = Duration(seconds: 2); class _CachedResponse { final _RequestType requestType; final Uri requestUri; final Map requestBody; final Map headers; final http.Response response; final DateTime requestTime; _CachedResponse( {required this.requestType, required this.requestUri, required this.requestBody, required this.headers, required this.response, required this.requestTime}); factory _CachedResponse.requestStub(_RequestType type, Uri uri, Map? headers, Map? body) => _CachedResponse( requestType: type, requestUri: uri, requestBody: body ?? {}, headers: headers ?? {}, response: http.Response('', 555), requestTime: DateTime(0), ); factory _CachedResponse.response( _RequestType type, Uri uri, Map? headers, Map? body, http.Response response, ) => _CachedResponse( requestType: type, requestUri: uri, requestBody: body ?? {}, headers: headers ?? {}, response: response, requestTime: DateTime.now(), ); @override bool operator ==(Object other) => identical(this, other) || other is _CachedResponse && runtimeType == other.runtimeType && requestType == other.requestType && requestUri == other.requestUri; @override int get hashCode => requestType.hashCode ^ requestUri.hashCode; } class _ExpiringRequestCache { final _responses = <_CachedResponse, _CachedResponse>{}; void _cleanupCache() { final expireTime = DateTime.now().subtract(_expireDuration); _logger .finest('Cleaning up request cache with ${_responses.length} entries'); _responses.removeWhere((key, value) { final expired = key.requestTime.isBefore(expireTime); if (expired) { _logger.finest( 'Expiring request: ${value.requestType} => ${value.requestUri}'); } return expired; }); _logger.finest('Cleaned up request cache has ${_responses.length} entries'); } Future getRequestOrExecute({ required Uri url, Map? headers, Map? body, Duration? timeout, }) async { _cleanupCache(); const type = _RequestType.get; final requestStub = _CachedResponse.requestStub( type, url, headers, null, ); late final http.Response response; if (_responses.containsKey(requestStub)) { print('Returning cached response for $type => $url'); response = _responses[requestStub]?.response ?? http.Response('', 555); } else { final request = http.get( url, headers: headers, ); response = await request.timeout( timeout ?? apiCallTimeout, onTimeout: requestTimeout, ); final cacheEntry = _CachedResponse.response( type, url, headers ?? {}, body ?? {}, response, ); print('Adding cached response for $type => $url'); _responses[cacheEntry] = cacheEntry; } return response; } } final _cache = _ExpiringRequestCache(); FutureResult, ExecError> getUrl( Uri url, { Map? headers, Duration? timeout, }) async { _logger.fine('GET: $url'); final requestHeaders = headers ?? {}; if (usePhpDebugging) { requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/'; } try { final response = await _cache.getRequestOrExecute( url: url, headers: headers, timeout: timeout ?? apiCallTimeout, ); if (response.statusCode != 200) { return Result.error(ExecError( type: ErrorType.authentication, message: '${response.statusCode}: ${response.reasonPhrase}')); } return PagedResponse.fromLinkHeader( response.headers['link'], utf8.decode(response.bodyBytes), ); } catch (e) { return Result.error( ExecError(type: ErrorType.localError, message: e.toString())); } } FutureResult postUrl( Uri url, Map body, { Map? headers, Duration? timeout, }) async { _logger.fine('POST: $url \n Body: $body'); final requestHeaders = headers ?? {}; if (usePhpDebugging) { requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/'; } try { final request = http.post( url, headers: requestHeaders, body: jsonEncode(body), ); final response = await request.timeout( timeout ?? apiCallTimeout, onTimeout: requestTimeout, ); if (response.statusCode != 200) { return Result.error(ExecError( type: ErrorType.authentication, message: '${response.statusCode}: ${response.reasonPhrase}')); } return Result.ok(utf8.decode(response.bodyBytes)); } catch (e) { return Result.error( ExecError(type: ErrorType.localError, message: e.toString())); } } FutureResult putUrl( Uri url, Map body, { Map? headers, Duration? timeout, }) async { _logger.fine('PUT: $url \n Body: $body'); try { final request = http.put( url, headers: headers, body: jsonEncode(body), ); final response = await request.timeout( timeout ?? apiCallTimeout, onTimeout: requestTimeout, ); if (response.statusCode != 200) { return Result.error(ExecError( type: ErrorType.authentication, message: '${response.statusCode}: ${response.reasonPhrase}')); } return Result.ok(utf8.decode(response.bodyBytes)); } catch (e) { return Result.error( ExecError(type: ErrorType.localError, message: e.toString())); } } FutureResult deleteUrl( Uri url, Map body, { Map? headers, Duration? timeout, }) async { _logger.fine('DELETE: $url'); try { final request = http.delete( url, headers: headers, body: jsonEncode(body), ); final response = await request.timeout( timeout ?? apiCallTimeout, onTimeout: requestTimeout, ); if (response.statusCode != 200) { return Result.error(ExecError( type: ErrorType.authentication, message: '${response.statusCode}: ${response.reasonPhrase}')); } return Result.ok(utf8.decode(response.bodyBytes)); } catch (e) { return Result.error( ExecError(type: ErrorType.localError, message: e.toString())); } } Uri generateTagUrlFromProfile(Profile profile, String tag) { return Uri.https(profile.serverName, '/search', {'tag': tag}); }