From 57309ae7756fca9b43417681d033810d2917f3db Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 31 Jan 2025 00:39:02 +0100 Subject: [PATCH] implement retransmission for encrypted text messages --- .../components/message_send_state_icon.dart | 7 +- lib/src/model/messages_model.dart | 9 +++ lib/src/providers/api/api.dart | 79 ++++++++++++------- lib/src/providers/api_provider.dart | 27 ++++--- .../providers/download_change_provider.dart | 12 ++- .../providers/messages_change_provider.dart | 2 + lib/src/views/chat_item_details_view.dart | 23 ++++-- lib/src/views/chat_list_view.dart | 14 ++-- 8 files changed, 110 insertions(+), 63 deletions(-) diff --git a/lib/src/components/message_send_state_icon.dart b/lib/src/components/message_send_state_icon.dart index 825da60..90915ed 100644 --- a/lib/src/components/message_send_state_icon.dart +++ b/lib/src/components/message_send_state_icon.dart @@ -72,10 +72,9 @@ class MessageSendStateIcon extends StatelessWidget { bool isDownloading = false; if (message.messageContent != null && message.messageContent!.downloadToken != null) { - isDownloading = context - .watch() - .currentlyDownloading - .contains(message.messageContent!.downloadToken!); + final test = context.watch().currentlyDownloading; + isDownloading = + test.contains(message.messageContent!.downloadToken.toString()); } if (isDownloading) { diff --git a/lib/src/model/messages_model.dart b/lib/src/model/messages_model.dart index 98c8589..221f7b5 100644 --- a/lib/src/model/messages_model.dart +++ b/lib/src/model/messages_model.dart @@ -194,6 +194,15 @@ class DbMessages extends CvModelBase { return messages; } + static Future> getAllMessagesForRetransmitting() async { + var rows = await dbProvider.db!.query( + tableName, + where: "$columnMessageAcknowledgeByServer = 0", + ); + List messages = await convertToDbMessage(rows); + return messages; + } + static Future> getAllMessagesForUser(int otherUserId) async { var rows = await dbProvider.db!.query( tableName, diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 41a1dd7..1b5f78e 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -17,6 +17,33 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/signal.dart' as SignalHelper; // this functions ensures that the message is received by the server and in case of errors will try again later + +Future tryTransmitMessages() async { + List retransmit = + await DbMessages.getAllMessagesForRetransmitting(); + + debugPrint("tryTransmitMessages: ${retransmit.length}"); + + Box box = await getMediaStorage(); + for (int i = 0; i < retransmit.length; i++) { + int msgId = retransmit[i].messageId; + debugPrint("msgId=$msgId"); + Uint8List? bytes = box.get("retransmit-$msgId"); + debugPrint("bytes == null =${bytes == null}"); + if (bytes != null) { + Result resp = await apiProvider.sendTextMessage( + Int64(retransmit[i].otherUserId), bytes); + + if (resp.isSuccess) { + DbMessages.acknowledgeMessageByServer(msgId); + box.delete("retransmit-$msgId"); + } else { + // in case of error do nothing. As the message is not removed the app will try again when relaunched + } + } + } +} + Future encryptAndSendMessage(Int64 userId, Message msg) async { Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); @@ -25,18 +52,19 @@ Future encryptAndSendMessage(Int64 userId, Message msg) async { return Result.error(ErrorCode.InternalError); } - Logger("api.dart").shout( - "TODO: store encrypted message and send later again. STORE: userId, bytes and messageId"); + Box box = await getMediaStorage(); + if (msg.messageId != null) { + debugPrint("putting=${msg.messageId}"); + box.put("retransmit-${msg.messageId}", bytes); + } Result resp = await apiProvider.sendTextMessage(userId, bytes); if (resp.isSuccess) { if (msg.messageId != null) { DbMessages.acknowledgeMessageByServer(msg.messageId!); + box.delete("retransmit-${msg.messageId}"); } - // TODO: remove encrypted tmp file - } else { - // in case of error do nothing. As the message is not removed the app will try again when relaunched } return resp; @@ -65,22 +93,6 @@ Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async { await DbMessages.insertMyMessage(target.toInt(), MessageKind.image); if (messageId == null) return; - Result res = await apiProvider.getUploadToken(); - - if (res.isError || !res.value.hasUploadtoken()) { - Logger("api.dart").shout("Error getting upload token!"); - - // TODO store message for later and try again - - return null; - } - - List uploadToken = res.value.uploadtoken; - Logger("sendImageToSingleTarget").fine("Got token: $uploadToken"); - - MessageContent content = - MessageContent(text: null, downloadToken: uploadToken); - Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target); if (encryptBytes == null) { await DbMessages.deleteMessageById(messageId); @@ -88,14 +100,25 @@ Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async { return; } - List? imageToken = - await apiProvider.uploadData(uploadToken, encryptBytes); - if (imageToken == null) { - Logger("api.dart").shout("handle error uploading like saving..."); - return; + Result res = await apiProvider.getUploadToken(); + + if (res.isError || !res.value.hasUploadtoken()) { + print("store encryptBytes in box to retransmit without an upload token"); + Logger("api.dart").shout("Error getting upload token!"); + return null; } - print("TODO: insert into DB and then create this MESSAGE"); + List uploadToken = res.value.uploadtoken; + + MessageContent content = + MessageContent(text: null, downloadToken: uploadToken); + + print("fragmentate the data"); + + if (!await apiProvider.uploadData(uploadToken, encryptBytes, 0)) { + Logger("api.dart").shout("error while uploading image"); + return; + } Message msg = Message( kind: MessageKind.image, @@ -140,7 +163,7 @@ Future tryDownloadMedia(List mediaToken, {bool force = false}) async { if (media != null && media.isNotEmpty) { offset = media.length; } - globalCallBackOnDownloadChange(mediaToken, true); + //globalCallBackOnDownloadChange(mediaToken, true); apiProvider.triggerDownload(mediaToken, offset); } diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 45a8743..d12aab7 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -8,6 +8,7 @@ import 'package:twonly/src/app.dart'; import 'package:twonly/src/proto/api/client_to_server.pbserver.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; +import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/api/server_messages.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -52,6 +53,14 @@ class ApiProvider { } } + Future onConnected() async { + await authenticate(); + globalCallbackConnectionState(true); + _reconnectionDelay = 5; + + tryTransmitMessages(); + } + Future connect() async { if (_channel != null && _channel!.closeCode != null) { return true; @@ -63,17 +72,13 @@ class ApiProvider { log.info("Trying to connect to the backend $apiUrl!"); if (await _connectTo(apiUrl)) { - await authenticate(); - globalCallbackConnectionState(true); - _reconnectionDelay = 5; + onConnected(); return true; } if (backupApiUrl != null) { log.info("Trying to connect to the backup backend $backupApiUrl!"); if (await _connectTo(backupApiUrl!)) { - globalCallbackConnectionState(true); - await authenticate(); - _reconnectionDelay = 5; + onConnected(); return true; } } @@ -265,18 +270,16 @@ class ApiProvider { return await _sendRequestV0(req); } - Future?> uploadData(List uploadToken, Uint8List data) async { - log.shout("fragmentate the data"); - + Future uploadData( + List uploadToken, Uint8List data, int offset) async { var get = ApplicationData_UploadData() ..uploadToken = uploadToken ..data = data - ..offset = 0; - + ..offset = offset; var appData = ApplicationData()..uploaddata = get; var req = createClientToServerFromApplicationData(appData); final result = await _sendRequestV0(req); - return result.isSuccess ? uploadToken : null; + return result.isSuccess; } Future getUserData(String username) async { diff --git a/lib/src/providers/download_change_provider.dart b/lib/src/providers/download_change_provider.dart index 5f3fa20..954319e 100644 --- a/lib/src/providers/download_change_provider.dart +++ b/lib/src/providers/download_change_provider.dart @@ -2,15 +2,19 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; class DownloadChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { - final HashSet> _currentlyDownloading = HashSet>(); + final HashSet _currentlyDownloading = HashSet(); - HashSet> get currentlyDownloading => _currentlyDownloading; + HashSet get currentlyDownloading => _currentlyDownloading; void update(List token, bool add) { + debugPrint("Downloading: $add : $token"); + if (add) { - _currentlyDownloading.add(token); + _currentlyDownloading.add(token.toString()); } else { - _currentlyDownloading.remove(token); + _currentlyDownloading.remove(token.toString()); } + debugPrint("Downloading: $add : ${_currentlyDownloading.toList()}"); + notifyListeners(); } } diff --git a/lib/src/providers/messages_change_provider.dart b/lib/src/providers/messages_change_provider.dart index a9f3307..ee06d7e 100644 --- a/lib/src/providers/messages_change_provider.dart +++ b/lib/src/providers/messages_change_provider.dart @@ -21,6 +21,7 @@ class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { changeCounter[targetUserId] = 0; } changeCounter[targetUserId] = changeCounter[targetUserId]! + 1; + notifyListeners(); } void init() async { @@ -33,5 +34,6 @@ class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { _lastMessage[last.otherUserId] = last; } } + notifyListeners(); } } diff --git a/lib/src/views/chat_item_details_view.dart b/lib/src/views/chat_item_details_view.dart index fc71857..1bed225 100644 --- a/lib/src/views/chat_item_details_view.dart +++ b/lib/src/views/chat_item_details_view.dart @@ -26,13 +26,13 @@ class ChatListEntry extends StatelessWidget { MessageSendState state = message.getSendState(); bool isDownloading = false; - if (message.messageContent != null && - message.messageContent!.downloadToken != null) { - isDownloading = context - .watch() - .currentlyDownloading - .contains(message.messageContent!.downloadToken!); - } + // if (message.messageContent != null && + // message.messageContent!.downloadToken != null) { + // isDownloading = context + // .watch() + // .currentlyDownloading + // .contains(message.messageContent!.downloadToken!); + // } Widget child = Container(); @@ -148,7 +148,14 @@ class _ChatItemDetailsViewState extends State { _messages.insertAll(0, toAppend); } - setState(() {}); + try { + if (context.mounted) { + setState(() {}); + } + } catch (e) { + // state should be disposed + return; + } if (updateOpenStatus) { _messages.where((x) => x.messageOpenedAt == null).forEach((message) { diff --git a/lib/src/views/chat_list_view.dart b/lib/src/views/chat_list_view.dart index a2638bd..b222e0e 100644 --- a/lib/src/views/chat_list_view.dart +++ b/lib/src/views/chat_list_view.dart @@ -151,13 +151,13 @@ class _UserListItem extends State { MessageSendState state = widget.lastMessage.getSendState(); bool isDownloading = false; - if (widget.lastMessage.messageContent != null && - widget.lastMessage.messageContent!.downloadToken != null) { - isDownloading = context - .watch() - .currentlyDownloading - .contains(widget.lastMessage.messageContent!.downloadToken!); - } + // if (widget.lastMessage.messageContent != null && + // widget.lastMessage.messageContent!.downloadToken != null) { + // isDownloading = context + // .watch() + // .currentlyDownloading + // .contains(widget.lastMessage.messageContent!.downloadToken!); + // } return UserContextMenu( user: widget.user,