import 'dart:math'; import 'package:chepuhagram/data/models/contact_model.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'dart:convert'; import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:chepuhagram/core/constants.dart'; import 'package:flutter_http_cache/flutter_http_cache.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'dart:io'; class ApiService extends ChangeNotifier { final _client = http.Client(); final _storage = const FlutterSecureStorage(); bool _isRefreshing = false; bool _isCacheInitialized = false; final cache = HttpCache( config: const CacheConfig( maxMemorySize: 100 * 1024 * 1024, // 100MB maxDiskSize: 500 * 1024 * 1024, // 500MB ), ); Future _ensureCacheReady() async { if (!_isCacheInitialized) { await cache.initialize(); _isCacheInitialized = true; } } /// Получает данные пользователя (включая его публичный ключ E2EE) по username Future getUserByUsername(String username) async { try { // Подставляй свой эндпоинт, например: /users/by-username/ final response = await Dio().get('/users/by-username/$username'); if (response.statusCode == 200 && response.data != null) { // Парсим полученные данные в модель контакта. // Убедись, что метод Contact.fromJson или Contact.fromMap корректно обрабатывает поле public_key return Contact.fromJson(response.data); } return null; } catch (e) { print("[ApiService] Ошибка при получении пользователя по username: $e"); return null; } } Future copyMediaOnServer(String fileId, int receiverId) async { try { final token = await getAccessToken(); final response = await Dio().post( '${AppConstants.baseUrl}/media/copy', data: FormData.fromMap({'file_id': fileId, 'receiver_id': receiverId}), options: Options(headers: {'Authorization': 'Bearer $token'}), ); if (response.statusCode == 200) { return response.data['new_file_id']; } } catch (e) { print("Ошибка копирования на сервере: $e"); } return null; } Future uploadFile( List bytes, { String purpose = 'media', Function(double)? onProgress, }) async { final token = await getAccessToken(); final dio = Dio(); final formData = FormData.fromMap({ 'file': MultipartFile.fromBytes(bytes, filename: 'media.enc'), 'purpose': purpose, }); final response = await dio.post( '${AppConstants.baseUrl}/media/v2/upload', data: formData, onSendProgress: (sent, total) { onProgress?.call(sent / total); }, options: Options(headers: {'Authorization': 'Bearer $token'}), ); return response.data['file_id']; } Future<(int?, String?)> getRemoteFileSizeAndName(String fileId) async { try { final token = await getAccessToken(); final url = '${AppConstants.baseUrl}/media/size/$fileId'; // Скорректируй путь согласно роутеру final response = await Dio().get( url, options: Options( headers: { 'Authorization': 'Bearer $token', // Твой токен, если требуется }, ), ); if (response.statusCode == 200 && response.data != null) { // Извлекаем размер из JSON: {"file_id": "...", "size": 123456} final intSize = response.data['size'] as int?; String? fileName = response.data['file_name'] as String?; if (fileName != null) { fileName = Uri.decodeComponent(fileName); print("Имя файла, полученное от сервера: $fileName"); } debugPrint( 'Успешно получен размер файла через API-size: $intSize байт', ); return (intSize, fileName); } } catch (e) { debugPrint('Ошибка при получении размера файла через API-size: $e'); } return (null, null); } Future<(http.ByteStream, String)> downloadFileAsStream(String fileId) async { final token = await getAccessToken(); // Получаем JWT токен авторизации final url = Uri.parse( '${AppConstants.baseUrl}/media/$fileId', ); // Замените на ваш эндпоинт скачивания final request = http.Request('GET', url); request.headers.addAll({'Authorization': 'Bearer $token'}); // Отправляем запрос final http.StreamedResponse response = await _client.send(request); final contentDisposition = response.headers['content-disposition']; String serverFileName = 'media.enc'; if (contentDisposition != null) { final match = RegExp( r"filename\*=UTF-8''(.+)", ).firstMatch(contentDisposition); if (match != null) { serverFileName = Uri.decodeComponent(match.group(1)!); print("Имя файла, полученное от сервера: $serverFileName"); } } if (response.statusCode == 200) { return (response.stream, serverFileName); } else { throw Exception( 'Ошибка скачивания файла: сервер вернул статус ${response.statusCode}', ); } } Future downloadFile(String fileId, String filePath) async { final token = await getAccessToken(); try { final response = await Dio().download( '${AppConstants.baseUrl}/media/$fileId', filePath, options: Options( headers: {'Authorization': 'Bearer $token'}, validateStatus: (status) => status == 200, ), ); if (response.statusCode == 200) { return File(filePath); } } catch (e) { debugPrint('Ошибка при скачивании: $e'); final file = File(filePath); if (await file.exists()) await file.delete(); } return null; } Future uploadFileStream( Stream> stream, int sourceLength, { String purpose = 'media', void Function(int processed, int total)? onProgress, String? fileName, }) async { try { final token = await getAccessToken(); final dio = Dio(); print( '[DEBUG] uploadFileStream: работаем через поточное чтение. Размер=$sourceLength bytes, purpose=$purpose', ); // БЕЗОПАСНО ДЛЯ RAM: Передаем стрим напрямую в Dio. Память не забивается! final formData = FormData.fromMap({ 'file': MultipartFile.fromStream( () => stream, sourceLength, // Передаем точную длину стрима, это важно для прогресса! filename: fileName, ), 'purpose': purpose, }); final response = await dio.post( '${AppConstants.baseUrl}/media/v2/upload', data: formData, onSendProgress: (sent, total) { // Твой print(sent) теперь будет вызываться динамически по мере ухода байт в сеть! if (total > 0) { onProgress?.call(sent, total); } }, options: Options( headers: {'Authorization': 'Bearer $token'}, validateStatus: (status) => status != null && status < 500, ), ); print('[DEBUG] uploadFileStream response status=${response.statusCode}'); print('[DEBUG] uploadFileStream response data=${response.data}'); if (response.statusCode == 200 || response.statusCode == 201) { final fileId = response.data['file_id']?.toString(); print('[DEBUG] uploadFileStream: успешно загружен, file_id=$fileId'); return fileId; } else { print( '[ERROR] uploadFileStream: ошибка ${response.statusCode} - ${response.data}', ); return null; } } catch (e) { print('[ERROR] uploadFileStream exception: $e'); return null; } } Future refreshToken() async { if (_isRefreshing) { // Already refreshing, wait for completion or return true assuming it will succeed return true; } _isRefreshing = true; notifyListeners(); try { final refreshToken = await _storage.read(key: 'refresh_token'); final response = await _client .post( Uri.parse('${AppConstants.baseUrl}/auth/refresh'), body: jsonEncode({'refresh_token': refreshToken}), headers: {'Content-Type': 'application/json'}, ) .timeout(Duration(seconds: 30)); final decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map; if (response.statusCode == 200) { await _storage.write( key: 'access_token', value: decodedResponse['access_token'], ); await _storage.write( key: 'refresh_token', value: decodedResponse['refresh_token'], ); notifyListeners(); return true; } else { notifyListeners(); return false; } } catch (e) { notifyListeners(); return false; } finally { _isRefreshing = false; } } Future getAccessToken() async { String? token; try { token = await _storage.read(key: 'access_token'); } catch (_) { throw Exception( 'Критическая ошибка инициализации внутренних библиотек.\n Приложение не может продолжить работу. \n Обратитесь к разработчику. \n Код ошибки: _apis_gat_1', ); } if (token != null) { bool isExpiredSoon = JwtDecoder.isExpired(token) || JwtDecoder.getRemainingTime(token).inMinutes < 2; if (isExpiredSoon) { bool refreshed = await refreshToken(); if (refreshed) { token = await _storage.read(key: 'access_token'); } else { return null; } } } return token; } Future updateFcmToken(String fcmtoken) async { notifyListeners(); try { final token = await getAccessToken(); if (token == null) return false; // Нет токена — прерываем выполнение final response = await _client .post( Uri.parse( '${AppConstants.baseUrl}/auth/update-fcm?token=$fcmtoken', ), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ) .timeout(const Duration(seconds: 10)); // Ограничиваем время ожидания if (response.statusCode == 200) { return true; } else { print("Ошибка установки FCM ключа: ${response.statusCode}"); return false; } } catch (e) { print("ℹ️ Не удалось обновить FCM токен (нет сети): $e"); return false; // Возвращаем false вместо падения приложения } finally { notifyListeners(); } } Future setPublicKey(String publicKey) async { notifyListeners(); try { final token = await getAccessToken(); final response = await _client.post( Uri.parse('${AppConstants.baseUrl}/auth/set-public-key'), body: jsonEncode({'public_key': publicKey}), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); if (response.statusCode == 200) { return true; } else { print("Ошибка установки ключа: ${response.statusCode}"); return false; } } catch (e) { rethrow; } finally { notifyListeners(); } } Future> getMe() async { final token = await getAccessToken(); await cache.initialize(); final client = CachedHttpClient( cache: cache, defaultCachePolicy: CachePolicy.networkFirst, ); final response = await client.get( Uri.parse('${AppConstants.baseUrl}/users/me'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); if (response.statusCode == 200) { return jsonDecode(utf8.decode(response.bodyBytes)) as Map; } throw Exception('Не удалось получить данные пользователя'); } Future updateEncryptedPrivateKey(String encryptedPrivateKey) async { final token = await getAccessToken(); final response = await _client.put( Uri.parse('${AppConstants.baseUrl}/users/me/encryption-key'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: jsonEncode({'encrypted_private_key': encryptedPrivateKey}), ); return response.statusCode == 200; } Future changePassword( String currentPassword, String newPassword, ) async { final token = await getAccessToken(); final response = await _client.put( Uri.parse('${AppConstants.baseUrl}/users/me/password'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: jsonEncode({ 'current_password': currentPassword, 'new_password': newPassword, }), ); return response.statusCode == 200; } Future> getChatHistory( int contactId, { bool forceRefresh = false, }) async { final token = await getAccessToken(); await _ensureCacheReady(); final client = CachedHttpClient( cache: cache, defaultCachePolicy: CachePolicy.networkFirst, ); final Map requestHeaders = { 'Authorization': 'Bearer $token', }; if (forceRefresh) { requestHeaders['Cache-Control'] = 'no-cache'; } final response = await client.get( Uri.parse( '${AppConstants.baseUrl}/messages/history/${contactId.toString()}', ), headers: { 'Content-Type': 'application/json', "Authorization": "Bearer $token", }, ); return jsonDecode(utf8.decode(response.bodyBytes)) as List; } Future downloadMedia( String fileId, { void Function(int received, int total)? onProgress, }) async { try { final token = await getAccessToken(); await _ensureCacheReady(); final client = CachedHttpClient( cache: cache, defaultCachePolicy: CachePolicy.networkFirst, ); final uri = Uri.parse('${AppConstants.baseUrl}/media/$fileId'); if (onProgress == null) { final response = await client.get( uri, headers: {'Authorization': 'Bearer $token'}, ); if (response.statusCode == 200) { return response.bodyBytes; } print('?????? ???????? ?????: ${response.statusCode}'); return null; } final req = http.Request('GET', uri); req.headers['Authorization'] = 'Bearer $token'; final streamed = await client.send(req); final total = int.tryParse(streamed.headers['content-length'] ?? '') ?? -1; int received = 0; final bytes = []; await for (final chunk in streamed.stream) { bytes.addAll(chunk); received += chunk.length; onProgress(received, total); } if (streamed.statusCode == 200) { return Uint8List.fromList(bytes); } print('?????? ???????? ?????: ${streamed.statusCode}'); return null; } catch (e) { print('?????? downloadMedia: $e'); return null; } } Future> updateMe({ required String username, required String firstName, required String lastName, String? phone, String? email, String? about, }) async { final token = await getAccessToken(); final response = await _client.put( Uri.parse('${AppConstants.baseUrl}/users/me'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: jsonEncode({ 'username': username, 'first_name': firstName, 'last_name': lastName, 'phone': (phone == null || phone.trim().isEmpty) ? null : phone.trim(), 'email': (email == null || email.trim().isEmpty) ? null : email.trim(), 'about': (about == null || about.trim().isEmpty) ? null : about.trim(), }), ); final decoded = jsonDecode(utf8.decode(response.bodyBytes)); if (response.statusCode == 200) { return decoded as Map; } throw Exception( (decoded is Map && decoded['detail'] != null) ? decoded['detail'] : 'Failed to update profile', ); } Future> getUserById(int userId) async { final token = await getAccessToken(); await _ensureCacheReady(); final client = CachedHttpClient( cache: cache, defaultCachePolicy: CachePolicy.networkFirst, ); final response = await client.get( Uri.parse('${AppConstants.baseUrl}/users/$userId'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); if (response.statusCode == 200) { return jsonDecode(utf8.decode(response.bodyBytes)) as Map; } throw Exception('Не удалось получить информацию о пользователе'); } Future updatePrivacySettings({ bool? showEmail, bool? showPhone, bool? showAvatar, bool? showAbout, bool? showUsername, bool? showLastOnline, }) async { final token = await getAccessToken(); final response = await _client.put( Uri.parse('${AppConstants.baseUrl}/users/me/privacy'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: jsonEncode({ if (showEmail != null) 'show_email': showEmail, if (showPhone != null) 'show_phone': showPhone, if (showAvatar != null) 'show_avatar': showAvatar, if (showAbout != null) 'show_about': showAbout, if (showUsername != null) 'show_username': showUsername, if (showLastOnline != null) 'show_last_online': showLastOnline, }), ); return response.statusCode == 200; } Future> getPrivacySettings() async { final token = await getAccessToken(); await _ensureCacheReady(); final client = CachedHttpClient( cache: cache, defaultCachePolicy: CachePolicy.networkFirst, ); final response = await client.get( Uri.parse('${AppConstants.baseUrl}/users/me/privacy'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); if (response.statusCode == 200) { return jsonDecode(utf8.decode(response.bodyBytes)) as Map; } throw Exception('Не удалось получить настройки конфиденциальности'); } Future updateAvatar(String fileId) async { final token = await getAccessToken(); final response = await _client.put( Uri.parse('${AppConstants.baseUrl}/users/me/avatar'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: jsonEncode({'avatar_file_id': fileId}), ); return response.statusCode == 200; } Future> enableTotp() async { final token = await getAccessToken(); final response = await _client.post( Uri.parse('${AppConstants.baseUrl}/auth/totp/enable'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); if (response.statusCode == 200) { return jsonDecode(utf8.decode(response.bodyBytes)) as Map; } throw Exception( (jsonDecode(response.body) as Map)['detail'] ?? 'Failed to enable TOTP', ); } Future verifyTotp(String code) async { final token = await getAccessToken(); final response = await _client.post( Uri.parse('${AppConstants.baseUrl}/auth/totp/verify'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, body: jsonEncode({'code': code}), ); return response.statusCode == 200; } Future disableTotp() async { final token = await getAccessToken(); final response = await _client.post( Uri.parse('${AppConstants.baseUrl}/auth/totp/disable'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); return response.statusCode == 200; } Future deleteAllMessages() async { final token = await getAccessToken(); final response = await _client.delete( Uri.parse('${AppConstants.baseUrl}/messages/all'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); return response.statusCode == 200; } }