import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/models/api_error.dart'; import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/sender_name_statistics.dart'; import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; enum ChannelSelector { owned(apiKey: 'owned'), // Return all channels of the user subscribedAny(apiKey: 'subscribed_any'), // Return all channels that the user is subscribing to (even unconfirmed) allAny(apiKey: 'all_any'), // Return channels that the user owns or is subscribing (even unconfirmed) subscribed(apiKey: 'subscribed'), // Return all channels that the user is subscribing to all(apiKey: 'all'); // Return channels that the user owns or is subscribing const ChannelSelector({required this.apiKey}); final String apiKey; } class MessageFilter { List? channelIDs; List? searchFilter; List? senderNames; List? usedKeys; List? priority; DateTime? timeBefore; DateTime? timeAfter; bool? hasSenderName; MessageFilter({ this.channelIDs, this.searchFilter, this.senderNames, this.usedKeys, this.priority, this.timeBefore, this.timeAfter, }); } class APIClient { static const String _base = 'https://simplecloudnotifier.de/api/v2'; static Future _request({ required String name, required String method, required String relURL, Map>? query, required T Function(Map json)? fn, dynamic jsonBody, String? authToken, Map? header, }) async { final t0 = DateTime.now(); final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {}); final req = http.Request(method, uri); print('[REQUEST|RUN] [${method}] ${name} | ${uri.toString()}'); if (jsonBody != null) { req.body = jsonEncode(jsonBody); req.headers['Content-Type'] = 'application/json'; } if (authToken != null) { req.headers['Authorization'] = 'SCN ${authToken}'; } req.headers['User-Agent'] = 'simplecloudnotifier/flutter/${Globals().platform.replaceAll(' ', '_')} ${Globals().version}+${Globals().buildNumber}'; if (header != null && !header.isEmpty) { req.headers.addAll(header); } int responseStatusCode = 0; String responseBody = ''; Map responseHeaders = {}; try { final response = await req.send(); responseBody = await response.stream.bytesToString(); responseStatusCode = response.statusCode; responseHeaders = response.headers; } catch (exc, trace) { RequestLog.addRequestException(name, t0, method, uri, req.body, req.headers, exc, trace); Toaster.error("Error", 'Request "${name}" failed'); ApplicationLog.error('Request "${name}" failed: ' + exc.toString(), trace: trace); rethrow; } if (responseStatusCode != 200) { APIError apierr; try { apierr = APIError.fromJson(jsonDecode(responseBody) as Map); } catch (exc, trace) { ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: trace); RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); Toaster.error("Error", 'Request "${name}" failed'); throw Exception('API request failed with status code ${responseStatusCode}'); } RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr); Toaster.error("Error", apierr.message); throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message); } try { final data = jsonDecode(responseBody); if (fn != null) { final result = fn(data as Map); RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); print('[REQUEST|FIN] [${method}] ${name}'); return result; } else { RequestLog.addRequestSuccess(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders); print('[REQUEST|FIN] [${method}] ${name}'); return null as T; } } catch (exc, trace) { RequestLog.addRequestDecodeError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, exc, trace); Toaster.error("Error", 'Request "${name}" failed'); ApplicationLog.error('Failed to decode response: ' + exc.toString(), additional: "\nBody:\n" + responseBody, trace: trace); rethrow; } } // ========================================================================================================================================================== static Future getUser(TokenSource auth, String uid) async { return await _request( name: 'getUser', method: 'GET', relURL: 'users/$uid', fn: User.fromJson, authToken: auth.getToken(), ); } static Future getUserPreview(TokenSource auth, String uid) async { return await _request( name: 'getUserPreview', method: 'GET', relURL: 'preview/users/$uid', fn: UserPreview.fromJson, authToken: auth.getToken(), ); } static Future addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async { return await _request( name: 'addClient', method: 'POST', relURL: 'users/${auth.getUserID()}/clients', jsonBody: { 'fcm_token': fcmToken, 'agent_model': agentModel, 'agent_version': agentVersion, 'client_type': clientType, 'name': name, }, fn: Client.fromJson, authToken: auth.getToken(), ); } static Future updateClient(TokenSource auth, String clientID, String fcmToken, String agentModel, String? name, String agentVersion) async { return await _request( name: 'updateClient', method: 'PUT', relURL: 'users/${auth.getUserID()}/clients/$clientID', jsonBody: { 'fcm_token': fcmToken, 'agent_model': agentModel, 'agent_version': agentVersion, 'name': name, }, fn: Client.fromJson, authToken: auth.getToken(), ); } static Future getClient(TokenSource auth, String cid) async { return await _request( name: 'getClient', method: 'GET', relURL: 'users/${auth.getUserID()}/clients/$cid', fn: Client.fromJson, authToken: auth.getToken(), ); } static Future> getChannelList(TokenSource auth, ChannelSelector sel) async { return await _request( name: 'getChannelList', method: 'GET', relURL: 'users/${auth.getUserID()}/channels', query: { 'selector': [sel.apiKey] }, fn: (json) => ChannelWithSubscription.fromJsonArray(json['channels'] as List), authToken: auth.getToken(), ); } static Future getChannel(TokenSource auth, String cid) async { return await _request( name: 'getChannel', method: 'GET', relURL: 'users/${auth.getUserID()}/channels/${cid}', fn: ChannelWithSubscription.fromJson, authToken: auth.getToken(), ); } static Future getChannelPreview(TokenSource auth, String cid) async { return await _request( name: 'getChannelPreview', method: 'GET', relURL: 'preview/channels/${cid}', fn: ChannelPreview.fromJson, authToken: auth.getToken(), ); } static Future updateChannel(AppAuth auth, String cid, {String? displayName, String? descriptionName}) async { return await _request( name: 'updateChannel', method: 'PATCH', relURL: 'users/${auth.getUserID()}/channels/${cid}', jsonBody: { if (displayName != null) 'display_name': displayName, if (descriptionName != null) 'description_name': descriptionName, }, fn: ChannelWithSubscription.fromJson, authToken: auth.getToken(), ); } static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter}) async { return await _request( name: 'getMessageList', method: 'GET', relURL: 'messages', query: { 'next_page_token': [pageToken], if (pageSize != null) 'page_size': [pageSize.toString()], if (filter?.searchFilter != null) 'search': filter!.searchFilter!, if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!, if (filter?.senderNames != null) 'sender': filter!.senderNames!, if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()], if (filter?.timeBefore != null) 'before': [filter!.timeBefore!.toIso8601String()], if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()], if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(), if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!, }, fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), authToken: auth.getToken(), ); } static Future getMessage(TokenSource auth, String msgid) async { return await _request( name: 'getMessage', method: 'GET', relURL: 'messages/$msgid', fn: SCNMessage.fromJson, authToken: auth.getToken(), ); } static Future<(String, List)> getChannelMessageList(TokenSource auth, String cid, String pageToken, {int? pageSize}) async { return await _request( name: 'getChannelMessageList', method: 'GET', relURL: 'users/${auth.getUserID()}/channels/${cid}/messages', query: { 'next_page_token': [pageToken], if (pageSize != null) 'page_size': [pageSize.toString()], }, fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), authToken: auth.getToken(), ); } static Future> getSubscriptionList(TokenSource auth) async { return await _request( name: 'getSubscriptionList', method: 'GET', relURL: 'users/${auth.getUserID()}/subscriptions', fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List), authToken: auth.getToken(), ); } static Future> getChannelSubscriptions(TokenSource auth, String cid) async { return await _request( name: 'getChannelSubscriptions', method: 'GET', relURL: 'users/${auth.getUserID()}/channels/${cid}/subscriptions', fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List), authToken: auth.getToken(), ); } static Future> getClientList(TokenSource auth) async { return await _request( name: 'getClientList', method: 'GET', relURL: 'users/${auth.getUserID()}/clients', fn: (json) => Client.fromJsonArray(json['clients'] as List), authToken: auth.getToken(), ); } static Future> getKeyTokenList(TokenSource auth) async { return await _request( name: 'getKeyTokenList', method: 'GET', relURL: 'users/${auth.getUserID()}/keys', fn: (json) => KeyToken.fromJsonArray(json['keys'] as List), authToken: auth.getToken(), ); } static Future createUserWithClient(String? username, String clientFcmToken, String clientAgentModel, String clientAgentVersion, String? clientName, String clientType) async { return await _request( name: 'createUserWithClient', method: 'POST', relURL: 'users', jsonBody: { 'username': username, 'fcm_token': clientFcmToken, 'agent_model': clientAgentModel, 'agent_version': clientAgentVersion, 'client_name': clientName, 'client_type': clientType, 'no_client': false, }, fn: UserWithClientsAndKeys.fromJson, ); } static Future getKeyToken(TokenSource auth, String kid) async { return await _request( name: 'getKeyToken', method: 'GET', relURL: 'users/${auth.getUserID()}/keys/$kid', fn: KeyToken.fromJson, authToken: auth.getToken(), ); } static Future getKeyTokenPreview(TokenSource auth, String kid) async { return await _request( name: 'getKeyTokenPreview', method: 'GET', relURL: 'preview/keys/$kid', fn: KeyTokenPreview.fromJson, authToken: auth.getToken(), ); } static Future getKeyTokenByToken(String userid, String token) async { return await _request( name: 'getCurrentKeyToken', method: 'GET', relURL: 'users/${userid}/keys/current', fn: KeyToken.fromJson, authToken: token, ); } static Future> getSenderNameList(TokenSource auth) async { return await _request( name: 'getSenderNameList', method: 'GET', relURL: 'users/${auth.getUserID()}/sender-names', fn: (json) => SenderNameStatistics.fromJsonArray(json['sender_names'] as List), authToken: auth.getToken(), ); } static Future subscribeToChannelbyID(TokenSource auth, String channelID) async { return await _request( name: 'subscribeToChannelbyID', method: 'POST', relURL: 'users/${auth.getUserID()}/subscriptions', jsonBody: { 'channel_id': channelID, }, fn: Subscription.fromJson, authToken: auth.getToken(), ); } static Future deleteSubscription(TokenSource auth, String channelID, String subID) async { return await _request( name: 'deleteSubscription', method: 'DELETE', relURL: 'users/${auth.getUserID()}/subscriptions/${subID}', fn: Subscription.fromJson, authToken: auth.getToken(), ); } static Future confirmSubscription(TokenSource auth, String channelID, String subID) async { return await _request( name: 'confirmSubscription', method: 'PATCH', relURL: 'users/${auth.getUserID()}/subscriptions/${subID}', jsonBody: { 'confirmed': true, }, fn: Subscription.fromJson, authToken: auth.getToken(), ); } static Future unconfirmSubscription(TokenSource auth, String channelID, String subID) async { return await _request( name: 'unconfirmSubscription', method: 'PATCH', relURL: 'users/${auth.getUserID()}/subscriptions/${subID}', jsonBody: { 'confirmed': false, }, fn: Subscription.fromJson, authToken: auth.getToken(), ); } }