feat: работа с вложениями

This commit is contained in:
Jganenok
2026-06-10 00:58:19 +07:00
parent 5d9a70f1b4
commit ea08c96726
11 changed files with 2134 additions and 191 deletions
+67
View File
@@ -241,6 +241,73 @@ class FileUploader {
}
}
Future<String?> uploadPhoto(
Uri uri,
File file, {
String filename = 'photo.jpg',
void Function(int sent, int total)? onProgress,
Duration progressThrottle = const Duration(milliseconds: 16),
}) async {
Socket? socket;
try {
final fileLength = await file.length();
socket = await _openSocket(uri);
final boundary =
'----KometBoundary${DateTime.now().microsecondsSinceEpoch}';
final preamble = utf8.encode(
'--$boundary\r\n'
'Content-Disposition: form-data; name="file"; filename="$filename"\r\n'
'Content-Type: ${_contentTypeForFilename(filename)}\r\n'
'\r\n',
);
final epilogue = utf8.encode('\r\n--$boundary--\r\n');
_writeImageHeaders(
socket,
uri,
preamble.length + fileLength + epilogue.length,
boundary: boundary,
);
socket.add(preamble);
final stopwatch = Stopwatch()..start();
var sent = 0;
final body = file.openRead().map((chunk) {
sent += chunk.length;
if (onProgress != null && stopwatch.elapsed >= progressThrottle) {
onProgress(sent, fileLength);
stopwatch.reset();
}
return chunk;
});
await socket.addStream(body);
socket.add(epilogue);
await socket.flush();
onProgress?.call(fileLength, fileLength);
final response = await _readFullResponse(
socket,
timeout: const Duration(minutes: 2),
);
try {
socket.destroy();
} catch (_) {}
if (response == null) return null;
final (status, responseBody) = response;
if (status != 200) {
logger.w('uploadPhoto: status=$status');
return null;
}
return _parsePhotoToken(responseBody);
} catch (e) {
logger.w('uploadPhoto: $e');
try {
socket?.destroy();
} catch (_) {}
return null;
}
}
void _writeImageHeaders(Socket socket, Uri uri, int total, {required String boundary}) {
final path = '${uri.path}${uri.hasQuery ? "?${uri.query}" : ""}';
final headers = StringBuffer()
+45
View File
@@ -562,6 +562,51 @@ class MessagesModule {
return false;
}
Future<String?> requestPhotoUploadUrl() async {
final response = await _api.sendRequest(Opcode.photoUpload, {'count': 1});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
return data['url'] as String?;
}
Future<Map<String, dynamic>?> sendPhotoMessage(
int chatId,
List<String> photoTokens, {
String? caption,
bool notify = true,
int maxAttempts = 20,
Duration retryDelay = const Duration(seconds: 1),
}) async {
final message = <String, dynamic>{
'cid': DateTime.now().millisecondsSinceEpoch * -1,
'attaches': [
for (final token in photoTokens)
{'_type': 'PHOTO', 'photoToken': token},
],
};
if (caption != null && caption.isNotEmpty) message['text'] = caption;
final payload = {'chatId': chatId, 'message': message, 'notify': notify};
for (var attempt = 0; attempt < maxAttempts; attempt++) {
try {
final response = await _api.sendRequest(Opcode.msgSend, payload);
if (!response.isOk) return null;
final data = response.payload;
if (data is Map) {
final msg = data['message'];
if (msg is Map) return Map<String, dynamic>.from(msg);
}
return null;
} on PacketError catch (e) {
if (e.errorKey != 'attachment.not.ready') rethrow;
if (attempt == maxAttempts - 1) return null;
await Future.delayed(retryDelay);
}
}
return null;
}
Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {