import 'dart:convert'; import 'dart:math'; import 'package:drift/drift.dart'; import 'package:hive/hive.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; 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/model/json/userdata.dart'; import 'package:twonly/src/model/protobuf/api/error.pb.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/providers/hive.dart'; import 'package:twonly/src/services/notification_service.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; import 'package:twonly/src/utils/storage.dart'; final lockSendingMessages = Mutex(); Future tryTransmitMessages() async { lockSendingMessages.protect(() async { Map retransmit = await getAllMessagesForRetransmitting(); if (retransmit.isEmpty) return; Logger("api.dart").info("try sending messages: ${retransmit.length}"); Map failed = {}; for (String key in retransmit.keys) { RetransmitMessage msg = RetransmitMessage.fromJson(jsonDecode(retransmit[key])); Result resp = await apiProvider.sendTextMessage( msg.userId, msg.bytes, msg.pushData, ); if (resp.isSuccess) { if (msg.messageId != null) { await twonlyDatabase.messagesDao.updateMessageByMessageId( msg.messageId!, MessagesCompanion( acknowledgeByServer: Value(true), ), ); } } else { failed[key] = retransmit[key]; } } Box box = await getMediaStorage(); box.put("messages-to-retransmit", jsonEncode(failed)); }); } class RetransmitMessage { int? messageId; int userId; Uint8List bytes; List? pushData; RetransmitMessage({ this.messageId, required this.userId, required this.bytes, this.pushData, }); // From JSON constructor factory RetransmitMessage.fromJson(Map json) { return RetransmitMessage( messageId: json['messageId'], userId: json['userId'], bytes: base64Decode(json['bytes']), pushData: json['pushData'] == null ? null : List.from(json['pushData'].map((item) => item as int)), ); } // To JSON method Map toJson() { return { 'messageId': messageId, 'userId': userId, 'bytes': base64Encode(bytes), 'pushData': pushData, }; } } Future> getAllMessagesForRetransmitting() async { Box box = await getMediaStorage(); String? retransmitJson = box.get("messages-to-retransmit"); Map retransmit = {}; if (retransmitJson != null) { retransmit = jsonDecode(retransmitJson); } return retransmit; } // this functions ensures that the message is received by the server and in case of errors will try again later Future encryptAndSendMessage( int? messageId, int userId, MessageJson msg, {PushKind? pushKind}) async { return await lockSendingMessages.protect(() async { Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); if (bytes == null) { Logger("api.dart").shout("Error encryption message!"); return Result.error(ErrorCode.InternalError); } String stateId = (messageId ?? (60001 + Random().nextInt(100000))).toString(); Box box = await getMediaStorage(); List? pushData; if (pushKind != null) { pushData = await getPushData(userId, pushKind); } { var retransmit = await getAllMessagesForRetransmitting(); retransmit[stateId] = jsonEncode(RetransmitMessage( messageId: messageId, userId: userId, bytes: bytes, pushData: pushData, ).toJson()); box.put("messages-to-retransmit", jsonEncode(retransmit)); } Result resp = await apiProvider.sendTextMessage(userId, bytes, pushData); if (resp.isSuccess) { if (messageId != null) { await twonlyDatabase.messagesDao.updateMessageByMessageId( messageId, MessagesCompanion(acknowledgeByServer: Value(true)), ); { var retransmit = await getAllMessagesForRetransmitting(); retransmit.remove(stateId); box.put("messages-to-retransmit", jsonEncode(retransmit)); } box.delete("retransmit-$messageId-textmessage"); } } return resp; }); } Future sendTextMessage( int target, TextMessageContent content, PushKind? pushKind, { int? responseToMessageId, }) async { DateTime messageSendAt = DateTime.now(); int? messageId = await twonlyDatabase.messagesDao.insertMessage( MessagesCompanion( contactId: Value(target), kind: Value(MessageKind.textMessage), sendAt: Value(messageSendAt), responseToOtherMessageId: Value(content.responseToMessageId), responseToMessageId: Value(responseToMessageId), downloadState: Value(DownloadState.downloaded), contentJson: Value( jsonEncode(content.toJson()), ), ), ); if (messageId == null) return; MessageJson msg = MessageJson( kind: MessageKind.textMessage, messageId: messageId, content: content, timestamp: messageSendAt, ); encryptAndSendMessage(messageId, target, msg, pushKind: pushKind); } Future notifyContactAboutOpeningMessage( int fromUserId, List messageOtherIds) async { for (final messageOtherId in messageOtherIds) { await encryptAndSendMessage( null, fromUserId, MessageJson( kind: MessageKind.opened, messageId: messageOtherId, content: MessageContent(), timestamp: DateTime.now(), ), ); } } Future notifyContactsAboutProfileChange() async { List contacts = await twonlyDatabase.contactsDao.getAllNotBlockedContacts(); UserData? user = await getUser(); if (user == null) return; if (user.avatarCounter == null) return; if (user.avatarSvg == null) return; for (Contact contact in contacts) { if (contact.myAvatarCounter < user.avatarCounter!) { twonlyDatabase.contactsDao.updateContact(contact.userId, ContactsCompanion(myAvatarCounter: Value(user.avatarCounter!))); encryptAndSendMessage( null, contact.userId, MessageJson( kind: MessageKind.profileChange, content: ProfileContent( avatarSvg: user.avatarSvg!, displayName: user.displayName, ), timestamp: DateTime.now(), ), ); } } }