Files
fuckKomet/lib/api/api_service_media.dart
2025-12-10 17:28:52 +03:00

568 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

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.

// hey
part of 'api_service.dart';
extension ApiServiceMedia on ApiService {
/// Обновляет имя/фамилию/описание профиля через сервер (opcode 16)
/// и возвращает обновленный профиль из ответа.
Future<Profile?> updateProfileText(
String firstName,
String lastName,
String description,
) async {
try {
await waitUntilOnline();
final Map<String, dynamic> payload = {
"firstName": firstName,
"lastName": lastName,
};
if (description.isNotEmpty) {
payload["description"] = description;
}
final int seq = _sendMessage(16, payload);
_log('➡️ SEND: opcode=16, payload=$payload');
// Ждем ответ именно на этот seq с opcode 16
final response = await messages.firstWhere(
(msg) => msg['seq'] == seq && msg['opcode'] == 16,
);
final Map<String, dynamic>? respPayload =
response['payload'] as Map<String, dynamic>?;
if (respPayload == null) {
throw Exception('Пустой ответ сервера на изменение профиля');
}
// Обработка ошибок вида { error, localizedMessage, message, title }
if (respPayload.containsKey('error')) {
final humanMessage = respPayload['localizedMessage'] ??
respPayload['message'] ??
respPayload['title'] ??
respPayload['error'];
throw Exception(humanMessage.toString());
}
final profileJson = respPayload['profile'];
if (profileJson is Map<String, dynamic>) {
// Обновляем глобальный снапшот чатов/профиля,
// чтобы все экраны сразу видели новые данные.
_lastChatsPayload ??= {
'chats': <dynamic>[],
'contacts': <dynamic>[],
'profile': null,
'presence': null,
'config': null,
};
_lastChatsPayload!['profile'] = profileJson;
return Profile.fromJson(profileJson);
}
} catch (e) {
_log('❌ Ошибка при обновлении профиля через opcode 16: $e');
}
return null;
}
/// Загружает фото и привязывает его к профилю через opcode 80 + 16.
/// Возвращает обновленный профиль из ответа opcode 16.
Future<Profile?> updateProfilePhoto(String firstName, String lastName) async {
try {
final picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image == null) return null;
print("Запрашиваем URL для загрузки фото...");
final int seq = _sendMessage(80, {"count": 1});
final response = await messages.firstWhere((msg) => msg['seq'] == seq);
final String uploadUrl = response['payload']['url'];
print("URL получен: $uploadUrl");
print("Загружаем фото на сервер...");
var request = http.MultipartRequest('POST', Uri.parse(uploadUrl));
request.files.add(await http.MultipartFile.fromPath('file', image.path));
var streamedResponse = await request.send();
var httpResponse = await http.Response.fromStream(streamedResponse);
if (httpResponse.statusCode != 200) {
throw Exception("Ошибка загрузки фото: ${httpResponse.body}");
}
final uploadResult = jsonDecode(httpResponse.body);
final String photoToken = uploadResult['photos'].values.first['token'];
print("Фото загружено, получен токен: $photoToken");
print("Привязываем фото к профилю...");
final payload = {
"firstName": firstName,
"lastName": lastName,
"photoToken": photoToken,
"avatarType": "USER_AVATAR",
};
final int seq16 = _sendMessage(16, payload);
print("Запрос на смену аватара отправлен.");
// Ждем ответ opcode 16 с обновленным профилем
final resp16 = await messages.firstWhere(
(msg) => msg['seq'] == seq16 && msg['opcode'] == 16,
);
final Map<String, dynamic>? respPayload16 =
resp16['payload'] as Map<String, dynamic>?;
if (respPayload16 == null) {
throw Exception('Пустой ответ сервера на смену аватара');
}
if (respPayload16.containsKey('error')) {
final humanMessage = respPayload16['localizedMessage'] ??
respPayload16['message'] ??
respPayload16['title'] ??
respPayload16['error'];
throw Exception(humanMessage.toString());
}
final profileJson = respPayload16['profile'];
if (profileJson is Map<String, dynamic>) {
_lastChatsPayload ??= {
'chats': <dynamic>[],
'contacts': <dynamic>[],
'profile': null,
'presence': null,
'config': null,
};
_lastChatsPayload!['profile'] = profileJson;
final profile = Profile.fromJson(profileJson);
await ProfileCacheService().syncWithServerProfile(profile);
return profile;
}
} catch (e) {
print("!!! Ошибка в процессе смены аватара: $e");
}
return null;
}
/// Загружает список заготовленных аватаров (opcode 25).
/// Возвращает payload вида:
/// { currentPresetId: int, presetAvatars: [ { name, avatars: [ {url,id}, ...] }, ... ] }
Future<Map<String, dynamic>> fetchPresetAvatars() async {
await waitUntilOnline();
final int seq = _sendMessage(25, {});
_log('➡️ SEND: opcode=25, payload={}');
final resp = await messages.firstWhere(
(msg) => msg['seq'] == seq && msg['opcode'] == 25,
);
final payload = resp['payload'] as Map<String, dynamic>?;
return payload ?? <String, dynamic>{};
}
/// Выбирает один из заготовленных аватаров (PRESET_AVATAR) через opcode 16.
/// firstName / lastName текущие значения профиля (как в примерах сервера).
Future<Profile?> setPresetAvatar({
required String firstName,
required String lastName,
required int photoId,
}) async {
try {
await waitUntilOnline();
final payload = {
"firstName": firstName,
"lastName": lastName,
"photoId": photoId,
"avatarType": "PRESET_AVATAR",
};
final int seq16 = _sendMessage(16, payload);
_log('➡️ SEND: opcode=16 (PRESET_AVATAR), payload=$payload');
final resp16 = await messages.firstWhere(
(msg) => msg['seq'] == seq16 && msg['opcode'] == 16,
);
final Map<String, dynamic>? respPayload16 =
resp16['payload'] as Map<String, dynamic>?;
if (respPayload16 == null) {
throw Exception('Пустой ответ сервера на установку пресет‑аватара');
}
if (respPayload16.containsKey('error')) {
final humanMessage = respPayload16['localizedMessage'] ??
respPayload16['message'] ??
respPayload16['title'] ??
respPayload16['error'];
throw Exception(humanMessage.toString());
}
final profileJson = respPayload16['profile'];
if (profileJson is Map<String, dynamic>) {
_lastChatsPayload ??= {
'chats': <dynamic>[],
'contacts': <dynamic>[],
'profile': null,
'presence': null,
'config': null,
};
_lastChatsPayload!['profile'] = profileJson;
return Profile.fromJson(profileJson);
}
} catch (e) {
_log('❌ Ошибка при установке пресет‑аватара: $e');
}
return null;
}
Future<void> sendPhotoMessage(
int chatId, {
String? localPath,
String? caption,
int? cidOverride,
int? senderId,
}) async {
try {
XFile? image;
if (localPath != null) {
image = XFile(localPath);
} else {
final picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
if (image == null) return;
}
await waitUntilOnline();
final int seq80 = _sendMessage(80, {"count": 1});
final resp80 = await messages.firstWhere((m) => m['seq'] == seq80);
final String uploadUrl = resp80['payload']['url'];
var request = http.MultipartRequest('POST', Uri.parse(uploadUrl));
request.files.add(await http.MultipartFile.fromPath('file', image.path));
var streamed = await request.send();
var httpResp = await http.Response.fromStream(streamed);
if (httpResp.statusCode != 200) {
throw Exception(
'Ошибка загрузки фото: ${httpResp.statusCode} ${httpResp.body}',
);
}
final uploadJson = jsonDecode(httpResp.body) as Map<String, dynamic>;
final Map photos = uploadJson['photos'] as Map;
if (photos.isEmpty) throw Exception('Не получен токен фото');
final String photoToken = (photos.values.first as Map)['token'];
final int cid = cidOverride ?? DateTime.now().millisecondsSinceEpoch;
final payload = {
"chatId": chatId,
"message": {
"text": caption?.trim() ?? "",
"cid": cid,
"elements": [],
"attaches": [
{"_type": "PHOTO", "photoToken": photoToken},
],
},
"notify": true,
};
clearChatsCache();
if (localPath != null) {
_emitLocal({
'ver': 11,
'cmd': 1,
'seq': -1,
'opcode': 128,
'payload': {
'chatId': chatId,
'message': {
'id': 'local_$cid',
'sender': senderId ?? 0,
'time': DateTime.now().millisecondsSinceEpoch,
'text': caption?.trim() ?? '',
'type': 'USER',
'cid': cid,
'attaches': [
{'_type': 'PHOTO', 'url': 'file://$localPath'},
],
},
},
});
}
_sendMessage(64, payload);
} catch (e) {
print('Ошибка отправки фото-сообщения: $e');
}
}
Future<void> sendPhotoMessages(
int chatId, {
required List<String> localPaths,
String? caption,
int? senderId,
}) async {
if (localPaths.isEmpty) return;
try {
await waitUntilOnline();
final int cid = DateTime.now().millisecondsSinceEpoch;
_emitLocal({
'ver': 11,
'cmd': 1,
'seq': -1,
'opcode': 128,
'payload': {
'chatId': chatId,
'message': {
'id': 'local_$cid',
'sender': senderId ?? 0,
'time': DateTime.now().millisecondsSinceEpoch,
'text': caption?.trim() ?? '',
'type': 'USER',
'cid': cid,
'attaches': [
for (final p in localPaths)
{'_type': 'PHOTO', 'url': 'file://$p'},
],
},
},
});
final List<Map<String, String>> photoTokens = [];
for (final path in localPaths) {
final int seq80 = _sendMessage(80, {"count": 1});
final resp80 = await messages.firstWhere((m) => m['seq'] == seq80);
final String uploadUrl = resp80['payload']['url'];
var request = http.MultipartRequest('POST', Uri.parse(uploadUrl));
request.files.add(await http.MultipartFile.fromPath('file', path));
var streamed = await request.send();
var httpResp = await http.Response.fromStream(streamed);
if (httpResp.statusCode != 200) {
throw Exception(
'Ошибка загрузки фото: ${httpResp.statusCode} ${httpResp.body}',
);
}
final uploadJson = jsonDecode(httpResp.body) as Map<String, dynamic>;
final Map photos = uploadJson['photos'] as Map;
if (photos.isEmpty) throw Exception('Не получен токен фото');
final String photoToken = (photos.values.first as Map)['token'];
photoTokens.add({"token": photoToken});
}
final payload = {
"chatId": chatId,
"message": {
"text": caption?.trim() ?? "",
"cid": cid,
"elements": [],
"attaches": [
for (final t in photoTokens)
{"_type": "PHOTO", "photoToken": t["token"]},
],
},
"notify": true,
};
clearChatsCache();
_sendMessage(64, payload);
} catch (e) {
print('Ошибка отправки фото-сообщений: $e');
}
}
Future<void> sendFileMessage(
int chatId, {
String? caption,
int? senderId,
}) async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.any,
);
if (result == null || result.files.single.path == null) {
print("Выбор файла отменен");
return;
}
final String filePath = result.files.single.path!;
final String fileName = result.files.single.name;
final int fileSize = result.files.single.size;
await waitUntilOnline();
// Показываем локальное сообщение с файлом сразу, как отправляется
final int cid = DateTime.now().millisecondsSinceEpoch;
_emitLocal({
'ver': 11,
'cmd': 1,
'seq': -1,
'opcode': 128,
'payload': {
'chatId': chatId,
'message': {
'id': 'local_$cid',
'sender': senderId ?? 0,
'time': DateTime.now().millisecondsSinceEpoch,
'text': caption?.trim() ?? '',
'type': 'USER',
'cid': cid,
'attaches': [
{
'_type': 'FILE',
'name': fileName,
'size': fileSize,
'url': 'file://$filePath',
},
],
},
},
});
// Запрашиваем URL для загрузки файла
final int seq87 = _sendMessage(87, {"count": 1});
final resp87 = await messages.firstWhere((m) => m['seq'] == seq87);
if (resp87['payload'] == null ||
resp87['payload']['info'] == null ||
(resp87['payload']['info'] as List).isEmpty) {
throw Exception('Неверный ответ на Opcode 87: отсутствует "info"');
}
final uploadInfo = (resp87['payload']['info'] as List).first;
final String uploadUrl = uploadInfo['url'];
final int fileId = uploadInfo['fileId'];
final String token = uploadInfo['token'];
print('Получен fileId: $fileId, token: $token и URL: $uploadUrl');
// Начинаем heartbeat каждые 5 секунд
Timer? heartbeatTimer;
heartbeatTimer = Timer.periodic(const Duration(seconds: 5), (_) {
_sendMessage(65, {"chatId": chatId, "type": "FILE"});
print('Heartbeat отправлен для загрузки файла');
});
try {
// Загружаем файл
var request = http.MultipartRequest('POST', Uri.parse(uploadUrl));
request.files.add(await http.MultipartFile.fromPath('file', filePath));
var streamed = await request.send();
var httpResp = await http.Response.fromStream(streamed);
if (httpResp.statusCode != 200) {
throw Exception(
'Ошибка загрузки файла: ${httpResp.statusCode} ${httpResp.body}',
);
}
print('Файл успешно загружен на сервер. Ожидаем подтверждение...');
// Ждем уведомления о завершении загрузки (opcode 136)
final uploadCompleteMsg = await messages
.timeout(const Duration(seconds: 30))
.firstWhere(
(msg) =>
msg['opcode'] == 136 && msg['payload']['fileId'] == fileId,
);
print(
'Получено подтверждение загрузки файла: ${uploadCompleteMsg['payload']}',
);
// Останавливаем heartbeat
heartbeatTimer.cancel();
final payload = {
"chatId": chatId,
"message": {
"text": caption?.trim() ?? "",
"cid": cid,
"elements": [],
"attaches": [
{"_type": "FILE", "fileId": fileId},
],
},
"notify": true,
};
clearChatsCache();
_sendMessage(64, payload);
print('Сообщение о файле (Opcode 64) отправлено.');
} finally {
// Гарантированно останавливаем heartbeat в случае ошибки
heartbeatTimer.cancel();
}
} catch (e) {
print('Ошибка отправки файла: $e');
}
}
Future<String> getVideoUrl(int videoId, int chatId, String messageId) async {
await waitUntilOnline();
final payload = {
"videoId": videoId,
"chatId": chatId,
"messageId": messageId,
};
final int seq = _sendMessage(83, payload);
print('Запрашиваем URL для videoId: $videoId (seq: $seq)');
try {
final response = await messages
.firstWhere((msg) => msg['seq'] == seq && msg['opcode'] == 83)
.timeout(const Duration(seconds: 15));
if (response['cmd'] == 3) {
throw Exception(
'Ошибка получения URL видео: ${response['payload']?['message']}',
);
}
final videoPayload = response['payload'] as Map<String, dynamic>?;
if (videoPayload == null) {
throw Exception('Получен пустой payload для видео');
}
String? videoUrl =
videoPayload['MP4_720'] as String? ??
videoPayload['MP4_480'] as String? ??
videoPayload['MP4_1080'] as String? ??
videoPayload['MP4_360'] as String?;
if (videoUrl == null) {
final mp4Key = videoPayload.keys.firstWhere(
(k) => k.startsWith('MP4_'),
orElse: () => '',
);
if (mp4Key.isNotEmpty) {
videoUrl = videoPayload[mp4Key] as String?;
}
}
if (videoUrl != null) {
print('URL для videoId: $videoId успешно получен.');
return videoUrl;
} else {
throw Exception('Не найден ни один MP4 URL в ответе');
}
} on TimeoutException {
print('Таймаут ожидания URL для videoId: $videoId');
throw Exception('Сервер не ответил на запрос видео вовремя');
} catch (e) {
print('Ошибка в getVideoUrl: $e');
rethrow;
}
}
}