2024-12-10 21:13:02 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:http/http.dart' as http;
|
2024-12-11 02:06:09 +00:00
|
|
|
import 'package:http/http.dart';
|
2024-12-10 21:13:02 +00:00
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:result_monad/result_monad.dart';
|
|
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
|
|
|
|
import '../../globals.dart';
|
|
|
|
import '../../models/exec_error.dart';
|
|
|
|
import '../../models/networking/paged_response.dart';
|
2024-12-17 00:11:11 +00:00
|
|
|
import '../globals_services.dart';
|
2024-12-10 21:13:02 +00:00
|
|
|
import '../rp_provider_extension.dart';
|
2024-12-11 02:26:04 +00:00
|
|
|
import '../settings_services.dart';
|
2024-12-10 21:13:02 +00:00
|
|
|
|
|
|
|
part 'network_services.g.dart';
|
|
|
|
|
|
|
|
http.Response requestTimeout() => http.Response('Client side timeout', 408);
|
|
|
|
|
|
|
|
class NetworkRequest {
|
|
|
|
final Uri url;
|
|
|
|
final Map<String, dynamic>? body;
|
|
|
|
final Map<String, String> headers;
|
|
|
|
final Duration? timeout;
|
|
|
|
|
|
|
|
NetworkRequest(
|
|
|
|
this.url, {
|
|
|
|
this.body,
|
|
|
|
this.headers = const {},
|
|
|
|
this.timeout,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() {
|
|
|
|
return 'NetworkRequest{url: $url, body: $body, headers: $headers, timeout: $timeout}';
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool operator ==(Object other) =>
|
|
|
|
identical(this, other) ||
|
|
|
|
other is NetworkRequest &&
|
|
|
|
runtimeType == other.runtimeType &&
|
|
|
|
url == other.url &&
|
|
|
|
mapEquals(body, other.body) &&
|
|
|
|
mapEquals(headers, other.headers) &&
|
|
|
|
timeout == other.timeout;
|
|
|
|
|
|
|
|
@override
|
|
|
|
int get hashCode =>
|
|
|
|
url.hashCode ^
|
|
|
|
Object.hashAll(body?.entries.toList() ?? []) ^
|
|
|
|
Object.hashAll(headers.entries.toList()) ^
|
|
|
|
timeout.hashCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
final _logger = Logger('NetworkServicesProviders');
|
|
|
|
|
|
|
|
extension _DebounceAndCancelExtension on Ref {
|
|
|
|
/// Wait for [duration] (defaults to 500ms), and then return a [http.Client]
|
|
|
|
/// which can be used to make a request.
|
|
|
|
///
|
|
|
|
/// That client will automatically be closed when the provider is disposed.
|
|
|
|
Future<http.Client> getDebouncedHttpClient([Duration? duration]) async {
|
|
|
|
// First, we handle debouncing.
|
|
|
|
var didDispose = false;
|
|
|
|
onDispose(() => didDispose = true);
|
|
|
|
|
|
|
|
// We delay multiple requests in a row within a specified interval or 0.5 s
|
|
|
|
await Future<void>.delayed(duration ?? const Duration(milliseconds: 500));
|
|
|
|
|
|
|
|
// If the provider was disposed during the delay, it means that the user
|
|
|
|
// refreshed again. We throw an exception to cancel the request.
|
|
|
|
// It is safe to use an exception here, as it will be caught by Riverpod.
|
|
|
|
if (didDispose) {
|
|
|
|
throw Exception('Cancelled');
|
|
|
|
}
|
|
|
|
|
|
|
|
// We now create the client and close it when the provider is disposed.
|
2024-12-17 00:11:11 +00:00
|
|
|
final client = http.Client();
|
2024-12-10 21:13:02 +00:00
|
|
|
onDispose(client.close);
|
|
|
|
|
|
|
|
// Finally, we return the client to allow our provider to make the request.
|
|
|
|
return client;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-17 00:11:11 +00:00
|
|
|
@riverpod
|
|
|
|
Map<String, String> userAgentHeader(Ref ref) {
|
|
|
|
final userAgent = ref.watch(userAgentProvider);
|
|
|
|
return {
|
|
|
|
'user-agent': userAgent,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
@riverpod
|
|
|
|
Future<Result<Uint8List, ExecError>> httpGetRawBytes(
|
|
|
|
Ref ref,
|
|
|
|
NetworkRequest request,
|
|
|
|
) async {
|
|
|
|
_logger.fine('GET Raw Bytes: ${request.url}');
|
|
|
|
//TODO determine if this timeout needs to be the timeout for the call or how long it sticks around for after it is first executed
|
|
|
|
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
|
|
|
|
final timeout = request.timeout ?? timeoutSetting;
|
|
|
|
ref.cacheFor(timeout);
|
|
|
|
final requestHeaders = Map<String, String>.from(request.headers);
|
|
|
|
if (usePhpDebugging) {
|
|
|
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final client = await ref.getDebouncedHttpClient();
|
|
|
|
final httpRequest = client.get(
|
|
|
|
request.url,
|
|
|
|
headers: requestHeaders,
|
|
|
|
);
|
|
|
|
|
|
|
|
final response = await httpRequest.timeout(
|
|
|
|
timeout,
|
|
|
|
onTimeout: requestTimeout,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
|
|
|
return Result.error(_buildErrorFromResponse(response));
|
|
|
|
}
|
|
|
|
|
|
|
|
return Result.ok(response.bodyBytes);
|
|
|
|
} catch (e) {
|
|
|
|
return Result.error(
|
|
|
|
ExecError(type: ErrorType.localError, message: e.toString()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-10 21:13:02 +00:00
|
|
|
@riverpod
|
|
|
|
Future<Result<PagedResponse<String>, ExecError>> httpGet(
|
|
|
|
Ref ref,
|
|
|
|
NetworkRequest request,
|
|
|
|
) async {
|
|
|
|
_logger.fine('GET: ${request.url}');
|
|
|
|
//TODO determine if this timeout needs to be the timeout for the call or how long it sticks around for after it is first executed
|
2024-12-11 02:26:04 +00:00
|
|
|
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
|
|
|
|
final timeout = request.timeout ?? timeoutSetting;
|
2024-12-10 21:13:02 +00:00
|
|
|
ref.cacheFor(timeout);
|
|
|
|
final requestHeaders = Map<String, String>.from(request.headers);
|
|
|
|
if (usePhpDebugging) {
|
|
|
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final client = await ref.getDebouncedHttpClient();
|
|
|
|
final httpRequest = client.get(
|
|
|
|
request.url,
|
|
|
|
headers: requestHeaders,
|
|
|
|
);
|
|
|
|
|
|
|
|
final response = await httpRequest.timeout(
|
|
|
|
timeout,
|
|
|
|
onTimeout: requestTimeout,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
2024-12-11 02:06:09 +00:00
|
|
|
return Result.error(_buildErrorFromResponse(response));
|
2024-12-10 21:13:02 +00:00
|
|
|
}
|
|
|
|
return PagedResponse.fromLinkHeader(
|
|
|
|
response.headers['link'],
|
|
|
|
utf8.decode(response.bodyBytes),
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
return Result.error(
|
|
|
|
ExecError(type: ErrorType.localError, message: e.toString()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@riverpod
|
|
|
|
Future<Result<String, ExecError>> httpPost(
|
|
|
|
Ref ref,
|
|
|
|
NetworkRequest request,
|
|
|
|
) async {
|
|
|
|
_logger.fine('POST: ${request.url} \n Body: ${request.body}');
|
2024-12-11 02:26:04 +00:00
|
|
|
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
|
|
|
|
final timeout = request.timeout ?? timeoutSetting;
|
2024-12-10 21:13:02 +00:00
|
|
|
ref.cacheFor(timeout);
|
|
|
|
final requestHeaders = Map<String, String>.from(request.headers);
|
|
|
|
if (usePhpDebugging) {
|
|
|
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final client = await ref.getDebouncedHttpClient();
|
|
|
|
|
|
|
|
final httpRequest = client.post(
|
|
|
|
request.url,
|
|
|
|
headers: requestHeaders,
|
|
|
|
body: jsonEncode(request.body),
|
|
|
|
);
|
|
|
|
|
|
|
|
final response = await httpRequest.timeout(
|
|
|
|
timeout,
|
|
|
|
onTimeout: requestTimeout,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
2024-12-11 02:06:09 +00:00
|
|
|
return Result.error(_buildErrorFromResponse(response));
|
2024-12-10 21:13:02 +00:00
|
|
|
}
|
|
|
|
return Result.ok(utf8.decode(response.bodyBytes));
|
|
|
|
} catch (e) {
|
|
|
|
return Result.error(
|
|
|
|
ExecError(type: ErrorType.localError, message: e.toString()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@riverpod
|
|
|
|
Future<Result<String, ExecError>> httpPut(
|
|
|
|
Ref ref,
|
|
|
|
NetworkRequest request,
|
|
|
|
) async {
|
|
|
|
_logger.fine('PUT: ${request.url} \n Body: ${request.body}');
|
2024-12-11 02:26:04 +00:00
|
|
|
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
|
|
|
|
final timeout = request.timeout ?? timeoutSetting;
|
2024-12-10 21:13:02 +00:00
|
|
|
ref.cacheFor(timeout);
|
|
|
|
final requestHeaders = Map<String, String>.from(request.headers);
|
|
|
|
if (usePhpDebugging) {
|
|
|
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final client = await ref.getDebouncedHttpClient();
|
|
|
|
|
|
|
|
final httpRequest = client.put(
|
|
|
|
request.url,
|
|
|
|
headers: requestHeaders,
|
|
|
|
body: jsonEncode(request.body),
|
|
|
|
);
|
|
|
|
|
|
|
|
final response = await httpRequest.timeout(
|
|
|
|
timeout,
|
|
|
|
onTimeout: requestTimeout,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
2024-12-11 02:06:09 +00:00
|
|
|
return Result.error(_buildErrorFromResponse(response));
|
2024-12-10 21:13:02 +00:00
|
|
|
}
|
|
|
|
return Result.ok(utf8.decode(response.bodyBytes));
|
|
|
|
} catch (e) {
|
|
|
|
return Result.error(
|
|
|
|
ExecError(type: ErrorType.localError, message: e.toString()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@riverpod
|
|
|
|
Future<Result<String, ExecError>> httpDelete(
|
|
|
|
Ref ref,
|
|
|
|
NetworkRequest request,
|
|
|
|
) async {
|
|
|
|
_logger.fine('DELETE: ${request.url}');
|
2024-12-11 02:26:04 +00:00
|
|
|
final timeoutSetting = ref.read(friendicaApiTimeoutSettingProvider);
|
|
|
|
final timeout = request.timeout ?? timeoutSetting;
|
2024-12-10 21:13:02 +00:00
|
|
|
ref.cacheFor(timeout);
|
|
|
|
final requestHeaders = Map<String, String>.from(request.headers);
|
|
|
|
if (usePhpDebugging) {
|
|
|
|
requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/';
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
final client = await ref.getDebouncedHttpClient();
|
|
|
|
|
|
|
|
final httpRequest = client.delete(
|
|
|
|
request.url,
|
|
|
|
headers: requestHeaders,
|
|
|
|
body: jsonEncode(request.body),
|
|
|
|
);
|
|
|
|
|
|
|
|
final response = await httpRequest.timeout(
|
|
|
|
timeout,
|
|
|
|
onTimeout: requestTimeout,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
2024-12-11 02:06:09 +00:00
|
|
|
return Result.error(_buildErrorFromResponse(response));
|
2024-12-10 21:13:02 +00:00
|
|
|
}
|
|
|
|
return Result.ok(utf8.decode(response.bodyBytes));
|
|
|
|
} catch (e) {
|
|
|
|
return Result.error(
|
|
|
|
ExecError(type: ErrorType.localError, message: e.toString()));
|
|
|
|
}
|
|
|
|
}
|
2024-12-11 02:06:09 +00:00
|
|
|
|
|
|
|
ExecError _buildErrorFromResponse(Response response) {
|
|
|
|
final type = switch (response.statusCode) {
|
|
|
|
401 => ErrorType.authentication,
|
|
|
|
404 => ErrorType.notFound,
|
|
|
|
408 => ErrorType.timeoutError,
|
|
|
|
_ => ErrorType.serverError,
|
|
|
|
};
|
|
|
|
final msg = '${response.statusCode}: ${response.reasonPhrase}';
|
|
|
|
return ExecError(type: type, message: msg);
|
|
|
|
}
|