This commit is contained in:
otsmr 2025-05-28 22:51:28 +02:00
parent 87bc1ed824
commit 8cb5e77686
5 changed files with 136 additions and 120 deletions

View file

@ -8,16 +8,13 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:http/http.dart' as http;
// import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/providers/api/media_send.dart';
import 'dart:typed_data';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:logging/logging.dart';
import 'package:twonly/app.dart';
import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart'
as client;
import 'package:twonly/src/model/protobuf/api/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/server_to_client.pbserver.dart';
import 'package:twonly/src/utils/storage.dart';
Map<int, DateTime> downloadStartedForMediaReceived = {};
@ -91,7 +88,6 @@ Future startDownloadMedia(Message message, bool force) async {
final content =
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
if (content is! MediaMessageContent) return;
if (content.downloadToken == null) return;
@ -123,100 +119,70 @@ Future startDownloadMedia(Message message, bool force) async {
downloadState: Value(DownloadState.downloading),
),
);
int offset = 0;
Uint8List? bytes = await readMediaFile(media.messageId, "encrypted");
if (bytes != null && bytes.isNotEmpty) {
offset = bytes.length;
}
// int offset = 0;
// Uint8List? bytes = await readMediaFile(media.messageId, "encrypted");
// if (bytes != null && bytes.isNotEmpty) {
// offset = bytes.length;
downloadStartedForMediaReceived[message.messageId] = DateTime.now();
Result res =
await apiProvider.triggerDownload(content.downloadToken!, offset);
if (res.isError) {
if (res.error == ErrorCode.InvalidDownloadToken) {
// TODO: notfy the sender about this issue
String downloadToken = uint8ListToHex(content.downloadToken!);
String apiUrl =
"http${apiProvider.apiSecure}://${apiProvider.apiHost}/api/download/$downloadToken";
var httpClient = http.Client();
var request = http.Request('GET', Uri.parse(apiUrl));
var response = httpClient.send(request);
List<List<int>> chunks = [];
int downloaded = 0;
response.asStream().listen((http.StreamedResponse r) {
r.stream.listen((List<int> chunk) {
// Display percentage of completion
print('downloadPercentage: ${downloaded / (r.contentLength ?? 0) * 100}');
chunks.add(chunk);
downloaded += chunk.length;
}, onDone: () async {
if (r.statusCode != 200) {
Logger("media_received.dart").shout("Download error: $r");
await twonlyDatabase.messagesDao.updateMessageByMessageId(
media.messageId,
message.messageId,
MessagesCompanion(
errorWhileSending: Value(true),
),
);
}
}
}
return;
}
Future<client.Response> handleDownloadData(DownloadData data) async {
if (globalIsAppInBackground) {
// download should only be done when the app is open
return client.Response()..error = ErrorCode.InternalError;
// Display percentage of completion
print('downloadPercentage: ${downloaded / (r.contentLength ?? 0) * 100}');
// Save the file
final Uint8List bytes = Uint8List(r.contentLength ?? 0);
int offset = 0;
for (List<int> chunk in chunks) {
bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
await writeMediaFile(message.messageId, "encrypted", bytes);
handleEncryptedFile(message, encryptedBytesTmp: bytes);
return;
});
});
}
Logger("server_messages")
.info("downloading: ${data.downloadToken} ${data.fin}");
Future handleEncryptedFile(Message msg, {Uint8List? encryptedBytesTmp}) async {
Uint8List? encryptedBytes =
encryptedBytesTmp ?? await readMediaFile(msg.messageId, "encrypted");
final media = await twonlyDatabase.mediaDownloadsDao
.getMediaDownloadByDownloadToken(data.downloadToken)
.getSingleOrNull();
if (media == null) {
Logger("server_messages")
.shout("download data received, but unknown messageID");
// answers with ok, so the server will delete the message
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
if (data.fin && data.offset == 3_980_938_213 && data.data.isEmpty) {
Logger("media_received.dart").shout("Image already deleted by the server!");
// media file was deleted by the server. remove the media from device
await twonlyDatabase.messagesDao.updateMessageByMessageId(
media.messageId,
MessagesCompanion(
errorWhileSending: Value(true),
),
);
await deleteMediaFile(media.messageId, "encrypted");
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
Uint8List? buffered = await readMediaFile(media.messageId, "encrypted");
Uint8List downloadedBytes;
if (buffered != null) {
if (data.offset != buffered.length) {
if (encryptedBytes == null) {
Logger("media_received.dart")
.shout("server send wrong offset: ${data.offset} ${buffered.length}");
return client.Response()..error = ErrorCode.InvalidOffset;
}
var b = BytesBuilder();
b.add(buffered);
b.add(data.data);
downloadedBytes = b.takeBytes();
} else {
downloadedBytes = Uint8List.fromList(data.data);
}
await writeMediaFile(media.messageId, "encrypted", downloadedBytes);
if (!data.fin) {
// download not finished, so waiting for more data...
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
}
Message? msg = await twonlyDatabase.messagesDao
.getMessageByMessageId(media.messageId)
.getSingleOrNull();
if (msg == null) {
await deleteMediaFile(media.messageId, "encrypted");
Logger("media_received.dart")
.info("messageId not found in database. Ignoring download");
// answers with ok, so the server will delete the message
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
.shout("encrypted bytes are not found for ${msg.messageId}");
}
MediaMessageContent content =
@ -226,7 +192,7 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!);
SecretBox secretBox = SecretBox(
downloadedBytes,
encryptedBytes!,
nonce: content.encryptionNonce!,
mac: Mac(content.encryptionMac!),
);
@ -239,14 +205,14 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
if (content.isVideo) {
final splited = extractUint8Lists(imageBytes);
imageBytes = splited[0];
await writeMediaFile(media.messageId, "mp4", splited[1]);
await writeMediaFile(msg.messageId, "mp4", splited[1]);
}
await writeMediaFile(media.messageId, "png", imageBytes);
await writeMediaFile(msg.messageId, "png", imageBytes);
} catch (e) {
Logger("media_received.dart").info("Decryption error: $e");
await twonlyDatabase.messagesDao.updateMessageByMessageId(
media.messageId,
msg.messageId,
MessagesCompanion(
errorWhileSending: Value(true),
),
@ -257,14 +223,13 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
}
await twonlyDatabase.messagesDao.updateMessageByMessageId(
media.messageId,
msg.messageId,
MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
);
await deleteMediaFile(media.messageId, "encrypted");
await deleteMediaFile(msg.messageId, "encrypted");
var ok = client.Response_Ok()..none = true;
return client.Response()..ok = ok;
apiProvider.downloadDone(content.downloadToken!);
}
Future<Uint8List?> getImageBytes(int mediaId) async {

View file

@ -102,7 +102,7 @@ Future<int?> initMediaUpload() async {
Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
await videoFilePath.rename("$basePath.original.mp4");
await videoFilePath.copy("$basePath.original.mp4");
return await compressVideoIfExists(mediaUploadId);
}
@ -499,7 +499,7 @@ Future<bool> handleNotifyReceiver(MediaUpload media) async {
Future<bool> compressVideoIfExists(int mediaUploadId) async {
String basePath = await getMediaFilePath(mediaUploadId, "send");
File videoOriginalFile = File("$basePath.orginal.mp4");
File videoOriginalFile = File("$basePath.original.mp4");
File videoCompressedFile = File("$basePath.mp4");
if (videoCompressedFile.existsSync()) {

View file

@ -35,8 +35,6 @@ Future handleServerMessage(server.ServerToClient msg) async {
Uint8List body = Uint8List.fromList(msg.v0.newMessage.body);
int fromUserId = msg.v0.newMessage.fromUserId.toInt();
response = await handleNewMessage(fromUserId, body);
} else if (msg.v0.hasDownloaddata()) {
response = await handleDownloadData(msg.v0.downloaddata);
} else {
Logger("handleServerMessage")
.shout("Got a new message from the server: $msg");

View file

@ -22,6 +22,7 @@ import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/providers/api/media_received.dart';
import 'package:twonly/src/providers/api/media_send.dart';
import 'package:twonly/src/providers/api/server_messages.dart';
import 'package:twonly/src/providers/hive.dart';
import 'package:twonly/src/services/fcm_service.dart';
import 'package:twonly/src/services/flame_service.dart';
import 'package:twonly/src/utils/misc.dart';
@ -77,6 +78,7 @@ class ApiProvider {
globalCallbackConnectionState(true);
if (!globalIsAppInBackground) {
retransmitRawBytes();
tryTransmitMessages();
retryMediaUpload();
tryDownloadAllMediaFiles();
@ -148,6 +150,7 @@ class ApiProvider {
try {
final msg = server.ServerToClient.fromBuffer(msgBuffer);
if (msg.v0.hasResponse()) {
removeFromRetransmissionBuffer(msg.v0.seq);
messagesV0[msg.v0.seq] = msg;
} else {
await handleServerMessage(msg);
@ -160,7 +163,7 @@ class ApiProvider {
Future<server.ServerToClient?> _waitForResponse(Int64 seq) async {
final startTime = DateTime.now();
final timeout = Duration(seconds: 10);
final timeout = Duration(seconds: 20);
while (true) {
if (messagesV0[seq] != null) {
@ -182,18 +185,51 @@ class ApiProvider {
}
}
Future<Result> sendRequestSync(ClientToServer request,
{bool authenticated = true}) async {
if (_channel == null) {
log.shout("sending request, but api is not connected.");
if (!await connect()) {
return Result.error(ErrorCode.InternalError);
}
}
if (_channel == null) {
return Result.error(ErrorCode.InternalError);
Future<Map<String, dynamic>> getRetransmission() async {
final box = await getMediaStorage();
Map<String, dynamic>? retransmit = box.get("rawbytes-to-retransmit");
// Map<String, dynamic> retransmit = {};
// if (retransmitJson != null) {
// try {
// retransmit = jsonDecode(retransmitJson);
// } catch (e) {
// Logger("api.dart").shout("Could not decode the rawbytes messages: $e");
// await box.delete("rawbytes-to-retransmit");
// }
// }
return retransmit ?? {};
}
Future retransmitRawBytes() async {
var retransmit = await getRetransmission();
Logger("api_provider.dart")
.info("Retransmit: ${retransmit.keys.length} messages");
for (final seq in retransmit.keys) {
try {
_channel!.sink.add(base64Decode(retransmit[seq]));
} catch (e) {
Logger("api_provider.dart").shout("$e");
}
}
}
Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async {
var retransmit = await getRetransmission();
retransmit[seq.toString()] = base64Encode(bytes);
final box = await getMediaStorage();
box.put("rawbytes-to-retransmit", retransmit);
}
Future removeFromRetransmissionBuffer(Int64 seq) async {
var retransmit = await getRetransmission();
if (retransmit.isEmpty) return;
retransmit.remove(seq.toString());
final box = await getMediaStorage();
box.put("rawbytes-to-retransmit", retransmit);
}
Future<Result> sendRequestSync(ClientToServer request,
{bool authenticated = true, bool ensureRetransmission = false}) async {
var seq = Int64(Random().nextInt(4294967296));
while (messagesV0.containsKey(seq)) {
seq = Int64(Random().nextInt(4294967296));
@ -201,6 +237,21 @@ class ApiProvider {
request.v0.seq = seq;
final requestBytes = request.writeToBuffer();
if (ensureRetransmission) {
addToRetransmissionBuffer(seq, requestBytes);
}
if (_channel == null) {
log.shout("sending request, but api is not connected.");
if (!await connect()) {
return Result.error(ErrorCode.InternalError);
}
if (_channel == null) {
return Result.error(ErrorCode.InternalError);
}
}
_channel!.sink.add(requestBytes);
Result res = asResult(await _waitForResponse(seq));
@ -372,6 +423,13 @@ class ApiProvider {
return await sendRequestSync(req);
}
Future<Result> downloadDone(List<int> token) async {
var get = ApplicationData_DownloadDone()..downloadToken = token;
var appData = ApplicationData()..downloaddone = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req, ensureRetransmission: true);
}
Future<Result> getCurrentLocation() async {
var get = ApplicationData_GetLocation();
var appData = ApplicationData()..getlocation = get;
@ -379,15 +437,6 @@ class ApiProvider {
return await sendRequestSync(req);
}
Future<Result> triggerDownload(List<int> token, int offset) async {
var get = ApplicationData_DownloadData()
..downloadToken = token
..offset = offset;
var appData = ApplicationData()..downloaddata = get;
var req = createClientToServerFromApplicationData(appData);
return await sendRequestSync(req);
}
Future<Result> uploadData(List<int> uploadToken, Uint8List data, int offset,
List<int>? checksum) async {
var get = ApplicationData_UploadData()

View file

@ -290,13 +290,17 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (isFront) {
return;
}
if (controller == null) return;
if (!controller!.value.isInitialized) return;
scaleFactor = (baseScaleFactor + (basePanY - details.localPosition.dy) / 30)
.clamp(1, _maxAvailableZoom);
await controller!.setZoomLevel(scaleFactor);
if (mounted) {
setState(() {});
}
}
Future pickImageFromGallery() async {
setState(() {