Chepuhagram/lib/domain/services/api_service.dart

695 lines
22 KiB
Dart
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void> _ensureCacheReady() async {
if (!_isCacheInitialized) {
await cache.initialize();
_isCacheInitialized = true;
}
}
/// Получает данные пользователя (включая его публичный ключ E2EE) по username
Future<Contact?> 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<String?> 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<String?> uploadFile(
List<int> 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<File?> 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<String?> uploadFileStream(
Stream<List<int>> 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<bool> 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<String?> 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<bool> 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<bool> 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<Map<String, dynamic>> 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<String, dynamic>;
}
throw Exception('Не удалось получить данные пользователя');
}
Future<bool> 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<bool> 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<List<dynamic>> getChatHistory(
int contactId, {
bool forceRefresh = false,
}) async {
final token = await getAccessToken();
await _ensureCacheReady();
final client = CachedHttpClient(
cache: cache,
defaultCachePolicy: CachePolicy.networkFirst,
);
final Map<String, String> 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<dynamic>;
}
Future<Uint8List?> 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 = <int>[];
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<Map<String, dynamic>> 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<String, dynamic>;
}
throw Exception(
(decoded is Map && decoded['detail'] != null)
? decoded['detail']
: 'Failed to update profile',
);
}
Future<Map<String, dynamic>> 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<String, dynamic>;
}
throw Exception('Не удалось получить информацию о пользователе');
}
Future<bool> 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<Map<String, dynamic>> 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<String, dynamic>;
}
throw Exception('Не удалось получить настройки конфиденциальности');
}
Future<bool> 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<Map<String, dynamic>> 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<String, dynamic>;
}
throw Exception(
(jsonDecode(response.body) as Map<String, dynamic>)['detail'] ??
'Failed to enable TOTP',
);
}
Future<bool> 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<bool> 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<bool> 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;
}
}