implement retransmission for encrypted text messages

This commit is contained in:
otsmr 2025-01-31 00:39:02 +01:00
parent dd31f18e07
commit 57309ae775
8 changed files with 110 additions and 63 deletions

View file

@ -72,10 +72,9 @@ class MessageSendStateIcon extends StatelessWidget {
bool isDownloading = false; bool isDownloading = false;
if (message.messageContent != null && if (message.messageContent != null &&
message.messageContent!.downloadToken != null) { message.messageContent!.downloadToken != null) {
isDownloading = context final test = context.watch<DownloadChangeProvider>().currentlyDownloading;
.watch<DownloadChangeProvider>() isDownloading =
.currentlyDownloading test.contains(message.messageContent!.downloadToken.toString());
.contains(message.messageContent!.downloadToken!);
} }
if (isDownloading) { if (isDownloading) {

View file

@ -194,6 +194,15 @@ class DbMessages extends CvModelBase {
return messages; return messages;
} }
static Future<List<DbMessage>> getAllMessagesForRetransmitting() async {
var rows = await dbProvider.db!.query(
tableName,
where: "$columnMessageAcknowledgeByServer = 0",
);
List<DbMessage> messages = await convertToDbMessage(rows);
return messages;
}
static Future<List<DbMessage>> getAllMessagesForUser(int otherUserId) async { static Future<List<DbMessage>> getAllMessagesForUser(int otherUserId) async {
var rows = await dbProvider.db!.query( var rows = await dbProvider.db!.query(
tableName, tableName,

View file

@ -17,6 +17,33 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/signal.dart' as SignalHelper; 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 // this functions ensures that the message is received by the server and in case of errors will try again later
Future tryTransmitMessages() async {
List<DbMessage> 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<Result> encryptAndSendMessage(Int64 userId, Message msg) async { Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
@ -25,18 +52,19 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
return Result.error(ErrorCode.InternalError); return Result.error(ErrorCode.InternalError);
} }
Logger("api.dart").shout( Box box = await getMediaStorage();
"TODO: store encrypted message and send later again. STORE: userId, bytes and messageId"); if (msg.messageId != null) {
debugPrint("putting=${msg.messageId}");
box.put("retransmit-${msg.messageId}", bytes);
}
Result resp = await apiProvider.sendTextMessage(userId, bytes); Result resp = await apiProvider.sendTextMessage(userId, bytes);
if (resp.isSuccess) { if (resp.isSuccess) {
if (msg.messageId != null) { if (msg.messageId != null) {
DbMessages.acknowledgeMessageByServer(msg.messageId!); 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; return resp;
@ -65,22 +93,6 @@ Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async {
await DbMessages.insertMyMessage(target.toInt(), MessageKind.image); await DbMessages.insertMyMessage(target.toInt(), MessageKind.image);
if (messageId == null) return; 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<int> uploadToken = res.value.uploadtoken;
Logger("sendImageToSingleTarget").fine("Got token: $uploadToken");
MessageContent content =
MessageContent(text: null, downloadToken: uploadToken);
Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target); Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target);
if (encryptBytes == null) { if (encryptBytes == null) {
await DbMessages.deleteMessageById(messageId); await DbMessages.deleteMessageById(messageId);
@ -88,14 +100,25 @@ Future sendImageToSingleTarget(Int64 target, Uint8List imageBytes) async {
return; return;
} }
List<int>? imageToken = Result res = await apiProvider.getUploadToken();
await apiProvider.uploadData(uploadToken, encryptBytes);
if (imageToken == null) { if (res.isError || !res.value.hasUploadtoken()) {
Logger("api.dart").shout("handle error uploading like saving..."); print("store encryptBytes in box to retransmit without an upload token");
return; Logger("api.dart").shout("Error getting upload token!");
return null;
} }
print("TODO: insert into DB and then create this MESSAGE"); List<int> 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( Message msg = Message(
kind: MessageKind.image, kind: MessageKind.image,
@ -140,7 +163,7 @@ Future tryDownloadMedia(List<int> mediaToken, {bool force = false}) async {
if (media != null && media.isNotEmpty) { if (media != null && media.isNotEmpty) {
offset = media.length; offset = media.length;
} }
globalCallBackOnDownloadChange(mediaToken, true); //globalCallBackOnDownloadChange(mediaToken, true);
apiProvider.triggerDownload(mediaToken, offset); apiProvider.triggerDownload(mediaToken, offset);
} }

View file

@ -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/client_to_server.pbserver.dart';
import 'package:twonly/src/proto/api/error.pb.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/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/api_utils.dart';
import 'package:twonly/src/providers/api/server_messages.dart'; import 'package:twonly/src/providers/api/server_messages.dart';
import 'package:twonly/src/utils/misc.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<bool> connect() async { Future<bool> connect() async {
if (_channel != null && _channel!.closeCode != null) { if (_channel != null && _channel!.closeCode != null) {
return true; return true;
@ -63,17 +72,13 @@ class ApiProvider {
log.info("Trying to connect to the backend $apiUrl!"); log.info("Trying to connect to the backend $apiUrl!");
if (await _connectTo(apiUrl)) { if (await _connectTo(apiUrl)) {
await authenticate(); onConnected();
globalCallbackConnectionState(true);
_reconnectionDelay = 5;
return true; return true;
} }
if (backupApiUrl != null) { if (backupApiUrl != null) {
log.info("Trying to connect to the backup backend $backupApiUrl!"); log.info("Trying to connect to the backup backend $backupApiUrl!");
if (await _connectTo(backupApiUrl!)) { if (await _connectTo(backupApiUrl!)) {
globalCallbackConnectionState(true); onConnected();
await authenticate();
_reconnectionDelay = 5;
return true; return true;
} }
} }
@ -265,18 +270,16 @@ class ApiProvider {
return await _sendRequestV0(req); return await _sendRequestV0(req);
} }
Future<List<int>?> uploadData(List<int> uploadToken, Uint8List data) async { Future<bool> uploadData(
log.shout("fragmentate the data"); List<int> uploadToken, Uint8List data, int offset) async {
var get = ApplicationData_UploadData() var get = ApplicationData_UploadData()
..uploadToken = uploadToken ..uploadToken = uploadToken
..data = data ..data = data
..offset = 0; ..offset = offset;
var appData = ApplicationData()..uploaddata = get; var appData = ApplicationData()..uploaddata = get;
var req = createClientToServerFromApplicationData(appData); var req = createClientToServerFromApplicationData(appData);
final result = await _sendRequestV0(req); final result = await _sendRequestV0(req);
return result.isSuccess ? uploadToken : null; return result.isSuccess;
} }
Future<Result> getUserData(String username) async { Future<Result> getUserData(String username) async {

View file

@ -2,15 +2,19 @@ import 'dart:collection';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class DownloadChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class DownloadChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
final HashSet<List<int>> _currentlyDownloading = HashSet<List<int>>(); final HashSet<String> _currentlyDownloading = HashSet<String>();
HashSet<List<int>> get currentlyDownloading => _currentlyDownloading; HashSet<String> get currentlyDownloading => _currentlyDownloading;
void update(List<int> token, bool add) { void update(List<int> token, bool add) {
debugPrint("Downloading: $add : $token");
if (add) { if (add) {
_currentlyDownloading.add(token); _currentlyDownloading.add(token.toString());
} else { } else {
_currentlyDownloading.remove(token); _currentlyDownloading.remove(token.toString());
} }
debugPrint("Downloading: $add : ${_currentlyDownloading.toList()}");
notifyListeners();
} }
} }

View file

@ -21,6 +21,7 @@ class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
changeCounter[targetUserId] = 0; changeCounter[targetUserId] = 0;
} }
changeCounter[targetUserId] = changeCounter[targetUserId]! + 1; changeCounter[targetUserId] = changeCounter[targetUserId]! + 1;
notifyListeners();
} }
void init() async { void init() async {
@ -33,5 +34,6 @@ class MessagesChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
_lastMessage[last.otherUserId] = last; _lastMessage[last.otherUserId] = last;
} }
} }
notifyListeners();
} }
} }

View file

@ -26,13 +26,13 @@ class ChatListEntry extends StatelessWidget {
MessageSendState state = message.getSendState(); MessageSendState state = message.getSendState();
bool isDownloading = false; bool isDownloading = false;
if (message.messageContent != null && // if (message.messageContent != null &&
message.messageContent!.downloadToken != null) { // message.messageContent!.downloadToken != null) {
isDownloading = context // isDownloading = context
.watch<DownloadChangeProvider>() // .watch<DownloadChangeProvider>()
.currentlyDownloading // .currentlyDownloading
.contains(message.messageContent!.downloadToken!); // .contains(message.messageContent!.downloadToken!);
} // }
Widget child = Container(); Widget child = Container();
@ -148,7 +148,14 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
_messages.insertAll(0, toAppend); _messages.insertAll(0, toAppend);
} }
setState(() {}); try {
if (context.mounted) {
setState(() {});
}
} catch (e) {
// state should be disposed
return;
}
if (updateOpenStatus) { if (updateOpenStatus) {
_messages.where((x) => x.messageOpenedAt == null).forEach((message) { _messages.where((x) => x.messageOpenedAt == null).forEach((message) {

View file

@ -151,13 +151,13 @@ class _UserListItem extends State<UserListItem> {
MessageSendState state = widget.lastMessage.getSendState(); MessageSendState state = widget.lastMessage.getSendState();
bool isDownloading = false; bool isDownloading = false;
if (widget.lastMessage.messageContent != null && // if (widget.lastMessage.messageContent != null &&
widget.lastMessage.messageContent!.downloadToken != null) { // widget.lastMessage.messageContent!.downloadToken != null) {
isDownloading = context // isDownloading = context
.watch<DownloadChangeProvider>() // .watch<DownloadChangeProvider>()
.currentlyDownloading // .currentlyDownloading
.contains(widget.lastMessage.messageContent!.downloadToken!); // .contains(widget.lastMessage.messageContent!.downloadToken!);
} // }
return UserContextMenu( return UserContextMenu(
user: widget.user, user: widget.user,